Freaky Friday: Zend Framework 2 und Validatoren mit Table Gateway in Form Collections

Schon wieder Freitag! Die Zeit rast schon wieder und es ist Zeit für einen neuen Beitrag der Sorte Freaky Friday! Heute soll es mal um selbst erstellte Validatoren mit Table Gateway im Zend Framework 2 gehen. Klingt ganz schön abgefahren, oder? Hattet ihr damit eigentlich auch Probleme? Ich schon! Formulare mit dem ZF2 sind eigentlich eine großartige Sache und sehr einfach gehalten. Problematisch fand ich allerdings die Sache mit selbsterstellten Validatoren, die eine Datenbankabfrage benötigten. Wie verdammt bekomme ich die Datenbankverbindung in die Validator Klasse? Und das auch noch in einer Form Collection im Zend Framework 2?

Ich werde es Euch heute zeigen. Gut, ich erhebe keinen Anspruch darauf, dass dies der einzig richtige Weg sein wird. Aber es funktioniert. Bevor wir anfangen, solltest Du Dich bereits eingehend mit dem Zend Framework 2, dem Model View Controller Prinzip und Factories beschäftigt haben.

Die module.config.php

1
2
3
4
5
6
7
8
9
10
'service_manager' => array(
    'factories' => array(
        'EingabeForm' => __NAMESPACE__ . '\Service\EingabeFormFactory',
        'FriendsTable' => __NAMESPACE__ . '\Model\Friends',
    ),
    'invokables' => array(
        'MyEntity' => __NAMEPSPACE__ . '\Entity\MyEntity',
    ),
),
...

Mal davon ausgehend, dass wir ein eigenes Modul „MyAplication“ in ZF2 programmieren, seht ihr hier einen Ausschnitt der module.config.php. In unserem Modul soll es ein Eingabeformular geben, welches über eine Factory generiert wird. Dazu haben wir eine Entity Klasse, welche die Inhalte unserer Datenbanktabelle beschreibt.

Die Form Klasse

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
namespace MyApplication\Form;
 
use Zend\Db\TableGateway\TableGateway;
use Zend\Form\Form;
 
class EingabeForm extends Form
{
    protected $tableGateway;
 
    public function __construct($name = null, array $options = array())
    {
        parent::__construct($name, $options);
 
        $tableGateway = $this->getOption('table');
        if ($tableGateway === null) {
            throw new Exception\InvalidArgumentException(sprintf(
                '%s benötigt die Option table mit einem TableGateway',
                __CLASS__
            ));
        }
 
        $this->setTableGateway($tableGateway);
 
        $this->add(array(
            'type' => 'Zend\Form\Element\Collection',
            'name' => 'hunde',
            'options' => array(
                'count' => 2,
                'should_create_template' => true,
                'template_placeholder' => '__index__',
                'allow_add' => true,
                'allow_remove' => true,
                'target_element' => array(
                    'type' => 'MyApplication\Form\FriendFieldset',
                    'options' => array(
                        'table' => $this->getTableGateway(),
                    ),
                ),
            ),
        ));
    }
 
    public function setTableGateway(TableGateway $tableGateway)
    {
        $this->tableGateway = $tableGateway;
    }
 
    public function getTableGateway()
    {
        return $this->tableGateway;
    }
}

Unsere Form Klasse beinhaltet die Eigenschaft $table, der wir über die in der module.config.php notierten Factory ein TableGateway übergeben werden. Dafür wurden auch gleich die entsprechenden Getter und Setter Methoden integriert. Zudem generieren wir über unsere Form Klasse einen Eingabeblock vom Typ FormCollection. Die Eingabefelder selbst werden wir über die Klasse FriendFieldset generieren.

Unser Fieldset

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
namespace MyApplication\Form;
 
use MyApplication\Entity\Friends;
 
use Zend\Form\Fieldset;
use Zend\InputFilterProviderInterface;
use Zend\Stdlib\Hydrator\ClassMethods as ClassMethodsHydrator;
 
class FriendsFieldset extends Fieldset implements InputFilterProviderInterface
{
    public function __construct($name = null, array $options = array())
    {
        parent::__construct('friends', $options);
 
        $this->setHydrator(new ClassMethodsHydrator(false))
            ->setObject(new Friends());
 
        $this->add(array(
            'name' => 'name',
            'options' => array(
                'label' => 'Name des Freundes',
            ),
            'attributes' => array(
                'required' => 'required',
            ),
        );
    }
 
    public function getInputFilterSpecification()
    {
        return array(
            'name' => array(
                'required' => true,
                'validators' => array(
                    array(
                        'name' => '\MyApplication\Validator\Friends',
                        'options' => array(
                            'table' => $this->getOption('table),
                        ),
                    ),
                ),
            ),
        );
    }
}

Unser Fieldset legt die Eingabefelder für die FormCollection (Eingabeblock) fest. Für das Fieldset gibt es nur ein Eingabefeld mit dem Namen „name“. Dieses Eingabefeld ist ein Pflichtfeld und es wird durch unseren Validator Friend, welcher unsere Datenbankverbindung benötigt, validiert. Die table Option kommt direkt aus unserer Form Klasse, die wir mit unserer Factory an unsere Form Klasse übergeben haben.

Unser Entity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace MyApplication\Entity;
 
class Friends
{
    protected $name;
 
    public function getName()
    {
        return $this->name;
    }
 
    public function setName($name)
    {
        $this->name = $name;
    }
}

Hier seht ihr eine ganz normale Entity Klasse, welche dazu dient die den gesammelten Eingabedaten eine gewisse Struktur zu verleihen. In unserem Beispiel haben wir nur das sich wiederholende Eingabefeld name, so dass wir hier auch nur die Getter und Setter Methode für den Namen benötigen. Unsere Getter Klasse wird vom Fieldset benötigt.

Unsere Validator Klasse

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
namespace MyApplication\Validator;
 
use Zend\Validator\AbstractValidator;
 
class Friends extends AbstractValidator
{
    const NOTVALID = 'notValid';
 
    protected $messageTemplates = array(
        self::NOTVALID => 'Dieser Name ist bereits in Deiner Freundesliste vorhanden',
    }
 
    public function __construct(array $options = array())
    {
        parent::__construct($options);
    }
 
    public function isValid($value)
    {
        $this->setValue($value);
 
        $table = $this->getOption('table');
        $result = $table->fetchByName($value);
 
        if ($result !== false) {
            $this->setError(self::NOTVALID);
            return false;
        }
 
        return true;
    }
}

Das hier ist also unser Validator. In der Methode isValid greifen wir auf die Option table zurück, die wir in unserem Fieldset an den Validator als Option übergeben haben. Somit haben wir das TableGateway an den Validator übergeben und können nun im Validator mit der Methode fetchByName prüfen, ob in der Datenbank ein Benutzer mit dem im Formular eingegebenen Namen vorhanden ist.

Die Factory für das Formular

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
namespace MyApplication\Service;
 
use MyApplication\Form\EingabeForm;
 
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
 
class EingabeFormFactory implements FactoryInterface
{
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        $table = $serviceLocator->get('FriendsTable');
 
        $form = new EingabeForm('Friends', array(
            'table' => $table
        ));
 
        return $form;
    }
}

Hier haben wir also die Factory, die wir ganz zu Beginn in der module.config.php im Service Manager festgelegt haben. Diese Factory erzeugt eine neue EingabeForm Klasse, die als Option das Friends Table Gateway über den Service Locator entgegen nimmt. Wie ihr in der module.config.php sehen könnt, wurde das Table Gateway als FriendsTable im Service Manager registriert. Unsere Form Klasse übergibt das Table Gateway dann an das Fieldset und dieses übergibt es dann an unseren Validator. Also eine ganze Kette an Injektionen. Abgefahren, oder?

Unser Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
namespace MyApplication\Controller;
 
use MyApplication\Entity\Friends;
 
use Zend\Mvc\Controller\AbstractActionController;
 
class EingabeController extends AbstractActionController
{
    public function indexAction()
    {
        $form = $this->getServiceLocator()->get('EingabeForm');
 
        $entity = new Friends();
        $form->bind($entity);
 
        ...
    }
}

In unserem Controller holen wir uns das Formular einfach über den Service Manager. Dieser gibt uns eine Form Instanz mit dem bereits injezierten Table Gateway zurück. Schon haben wir beim Absenden des Formulars einen Validator mit einer Datenbankverbindung.

Das war doch einfach, oder?

Fazit

Das Zend Framework 2 ist eine konsequente Umsetzung des Model View Controller Prinzips. Setzt man dieses Prinzip konsequent durch, ist man gut beraten, wenn man die Logik komplett aus dem Controller fern hält und alle Abhängigkeiten vom Service Manager über Factories erledigen lässt. Dies mag im ersten Moment als aufwendig und unkonventionell erscheinen. Im Endeffekt ist es aber schnell und komfortabel und entspricht der Logik des Model View Controller Prinzips.

Haben wir also wieder was gelernt. Lasst es einfach mal am Wochenende sacken. Solltet ihr Fragen haben, hinterlasst einfach einen Kommentar unter diesem Artikel. Ich antworte dann so schnell ich kann. Solltet ihr einen anderen Weg kennen, um Datenbankabfragen in einem Validator durchzuführen, bin ich für jeden Rat offen.

About Author: Marcel
Ich bin Senior PHP Developer bei MM Newmedia. Seit 2005 bin ich begeisterter Webentwickler und arbeite als Freelancer für namenhafte Firmen und entwickle jede Menge abgefahrenes Zeug und berichte darüber in meinem Blog.

Kommentar verfassen