Endlich wieder Freitag und somit auch Zeit für einen neuen Artikel aus der Freaky Friday Reihe. Neben ein paar kleineren WordPress Projekten arbeite ich momentan eigentlich ausschließlich mit dem Zend Framework in den Versionen 2 und 3. In diesem Framework gibt es so genannte Form Collections, welche im Grunde genommen sich wiederholende Eingabefelder in einem Formular sind. Wenn man es runterbrechen möchte, sind es Arrays in Formularen. Wie man diese Form Collections vernünftig filtert, validiert und hydriert zeige ich Euch im heutigen Artikel.
Zuerst die Collection
Ich persönlich fange immer mit der Form Collection selbst an. Hierin sind die Eingabefelder enthalten, die sich wiederholen können. Im heutigen Beispiel gehen wir einfach mal von Telefonnummern aus, die zu einer Person gespeichert werden können. Wir kennen das ja. Da gibt es private, geschäftliche und mobile Nummern, die man einer Person zuordnen kann. Genau deswegen gibt es im Zend Framework die Form Collections. Mit diesen Collections lässt sich eine solche Aufgabe relativ leicht erledigen.
namespace Application\Form;
use Zend\Filter\StringTrim;
use Zend\Filter\StripTags;
use Zend\Filter\ToInt;
use Zend\Form\Fieldset;
use Zend\Form\Element\Select;
use Zend\Form\Element\Text;
use Zend\InputFilter\InputFilterProviderInterface;
class PhoneNumberCollection extends Fieldset implements InputFilterProviderInterface
{
public function init()
{
$this->add([
'name' => 'art',
'type' => Select::class,
'attributes' => [
'id' => 'art-__index__',
'required' => true,
],
'options' => [
'empty_option' => 'Bitte wählen Sie',
'label' => 'Art der Telefonnummer',
'value_options' => [
1 => 'privat',
2 => 'geschäftlich',
3 => 'mobil',
],
],
]);
$this->add([
'name' => 'nummer',
'type' => Text::class,
'attributes' => [
'id' => 'nummer-__index__',
'pattern' => '[0-9\+\s]'
'required' => true,
],
'options' => [
'label' => 'Telefonnummer',
],
]);
}
public function getInputFilterSpecification()
{
return [
'art' => [
'required' => true,
'filters' => [
[ 'name' => StripTags::class ],
[ 'name' => ToInt::class ],
],
],
'nummer' => [
'required' => true,
'filters' => [
[ 'name' => StripTags::class ],
[ 'name' => StringTrim::class ],
],
],
];
}
}
Diese Klasse stellt unsere wiederkehrenden Eingabefelder dar. Im Grunde genommen handelt es sich hierbei um ein einfaches Fieldset, welches zwei Eingabefelder Art der Nummer und die Telefonnummer selbst enthält. Durch die Implementierung des InputFilterProviderInterface legen wir auch gleich die Validierung für die beiden Eingabefelder fest. Die Implementierung fügt die Methode getInputFilterSpecification hinzu, in der ich einfach nur ein paar Filter für die Eingaben festgelegt habe. Diese könnte man theoretisch noch um Validatoren erweitern. Aber wir wollen das hier ja nicht ausarten lassen. Es ist schließlich Freitag.
Das Entity für die Collection
Entitäten sind in der Zend Framework Community irgendwie ein zweischneidiges Schwert. Einige scheinen sie hassen und andere widerum vergöttern sie. Ich gehöre da eher zur letzten Gruppe. Ich habe mir angewöhnt für alles Entitäten zu programmieren. Entitäten machen ein Projekt einfach skalierbarer. Alles, was nicht in der Entität notiert ist, gibt es einfach nicht. Aus meiner Sicht ein riesiger Vorteil. Genau aus diesem Grund wird es für alles, was wir hier an Daten erfassen, eine Entität geben.
namespace Application\Entity;
class PhoneNumberCollection implements \JsonSerializable
{
protected $art;
protected $nummer;
public function getArt() : integer
{
return $this->art;
}
public function setArt(int $art) : PhoneNumberCollection
{
$this->art = $art;
return $this;
}
public function getNummer() : string
{
return $this->nummer;
}
public function setNummer(string $nummer) : PhoneNumberCollection
{
$this->nummer = $nummer;
return $this;
}
public function jsonSerialize() : array
{
return get_object_vars($this);
}
}
Diese Entität hält einfach die beiden Werte Art der Telefonnummer und die Telefonnummer selbst fest. Zusätzlich implementiert die Entität das JsonSerializable Interface, so dass wir die Daten später serialisiert speichern können.
Wieviele Telefonnummern hat der eigentlich?
Das ist die Frage, die jetzt im Raum steht. Wir wissen es nämlich erstmal nicht. Unsere Collection ist nämlich dynamisch. Wir wollen später Nummern hinzufügen und auch wieder entfernen. Allerdings brauchen wir die Anzahl der Nummern, um die Collection zum Beispiel zum Bearbeiten wieder mit Daten füllen zu können. Ich selbst habe mir angewöhnt ein weiteres Fieldset für die Darstellung der Collection anzulegen, um später alles weitere vernünftig steuern zu können. Wie sieht dieses Fieldset also aus?
namespace Application\Form;
use \Application\Form\PhoneNumberCollection;
use Zend\Filter\ToInt;
use Zend\Form\Fieldset;
use Zend\Form\Element\Collection;
use Zend\Form\Element\Select;
use Zend\InputFilter\InputFilterProviderInterface;
class PhoneNumberFieldset extends Fieldset implements InputFilterProviderInterface
{
public function init()
{
$this->add([
'name' => 'anzahl',
'type' => Hidden::class,
'attributes' => [
'id' => 'anzahl',
],
]);
$this->add([
'name' => 'telefonnummern',
'type' => Collection::class,
'options' => [
'count' => 1,
'should_create_template' => true,
'template_placeholder' => '__index__',
'allow_add' => true,
'allow_remove' => true,
'target_element' => [
'type' => PhoneNumberCollection::class,
],
],
]);
}
public function getInputFilterSpecification()
{
return [
'anzahl' => [
'required' => true,
'filters' => [
[ 'name' => ToInt::class ],
],
],
];
}
}
In diesem Fieldset integrieren wir unsere Collection. Man kann sich dieses Fieldset einfach als einen Abschnitt von vielen in einem Formular vorstellen, in dem die Telefonnummern abgefragt werden. Für die Anzahl der verschiedenen Telefonnummern haben wir ein verstecktes Element eingefügt, in welchem wir später die Anzahl der verschiedenen Nummern darstellen. Die Collection selbst wird mit telefonnummern benannt. Die Anzahl setzen wir mit dem count Wert erstmal auf 1. Wie schon erwähnt wollen wir Telefonnummern hinzufügen und entfernen können. Hierzu nutzen wir die Template Funktion der Collection und lassen mit der Option should_create_template ein Template generieren, welches wir dynamisch ein und ausblenden können. Ein kleines Highlight ist hier auch die Validierung. Wir validieren hier lediglich die Anzahl der Collections. Alles andere haben wir ja schon in unserem Collection Fieldset definiert. Hierzu aber später mehr. Erstmal brauchen wir eine Entität für unser Fieldset.
Das Entity für das Fieldset
Ja genau, wir brauchen auch für das Fieldset eine Entität, welches später unsere gefilterten und validierten Daten halten wird.
namespace Application\Entity;
class PhoneNumberFieldset implements \JsonSerializable
{
protected $anzahl;
protected $telefonnummern;
public function getAnzahl() : int
{
return $this->anzahl;
}
public function setAnzahl(int $anzahl) : PhoneNumberFieldset
{
$this->anzahl = $anzahl;
return $this;
}
public function getTelefonnummern() : array
{
return $this->telefonnummern;
}
public function setTelefonnummern(array $telefonnummern) : PhoneNumberFieldset
{
$this->telefonnummern = $telefonnummern;
return $this;
}
public function jsonSerialize() : array
{
return get_object_vars($this);
}
}
Dieses Entity nimmt noch mal die Daten unseres Fieldsets auf. Die Telefonnummern werden am Ende ein Array mit PhoneNumberCollection Entitäten sein.
Eine Factory, die alles zusammenfasst
Bis hierhin haben wir im Grunde genommen zwei Fieldsets und zwei Entitäten programmiert, die wir nun irgendwie zusammenbringen müssen. Hierzu programmieren wir einfach eine Factory, die diesen Job für uns erledigen wird.
namespace Application\Form\Service;
use Application\Hydrator\Strategy\Telefonnummern;
use Interop\Container\ContainerInterface;
use Zend\Hydrator\ClassMethods;
class PhoneNumberFieldset
{
public function __invoke(ContainerInterface $container)
{
$entity = new Application\Entity\PhoneNumberFieldset();
$hydrator = (new ClassMethods(false))
->addStrategy('telefonnummern', new Telefonnummern());
$fieldset = (new Application\Form\PhoneNumberFieldset())
->setHydrator($hydrator)
->setObject($entity);
return $fieldset;
}
}
Unsere Factory liefert uns unser Telefonnummern Fieldset. Dieses Fieldset benutzt den ClassMethods Hydrator und wird unsere Entität mit den entsprechenden Daten füttern. Das kleine Highlight in dieser Factory ist die Hydrator Strategy, die wir dem Hydrator hinzufügen. Diese Strategy reagiert auf den Schlüssel telefonnummern. Wenn dieser Wert im POST Array vorhanden ist, wird die Strategy ausgeführt. Was diese Strategy genau macht, erkläre ich im nächsten Schritt.
Was ist eigentlich eine Hydrator Strategie?
Das Zend Framework ist in Sachen Hydrierung ziemlich gut aufgestellt. Unter anderem bringt das Zend Framework die Option einer Hydrator Strategy. Sobald ein vorher festgelegter Wert im POST Array erscheint, sind wir somit in der Lage eine gesonderte Hydrierung dieses Wertes vorzunehmen. Da der Wert telefonnummern ein Array mit einer unbestimmten Anzahl aus den Werten Art der Telefonnummer und der Telefonnummer selbst ist, müssen wir dieses Array auch besondert behandeln. Am Ende wollen wir ja ein Array aus unseren Collection Entitäten haben.
namespace Application\Hydrator\Strategy;
use Application\Entity\PhoneNumberCollection;
use Zend\Hydrator\Strategy\DefaultStrategy;
use Zend\Hydrator\ClassMethods;
class Telefonnummern extends DefaultStrategy
{
public function hydrate($value)
{
$entities = [];
if (is_array($value)) {
foreach ($value as $key => $data) {
$entities[] = (new ClassMethods())->hydrate($data, new PhoneNumberCollection());
}
}
return $entities;
}
}
Diese Strategy nimmt ein Array entgegen und hydriert es mit unserer Collection Entität und liefert danach ein Array mit Entitäten. Genau so, wie wir es haben wollen. Ich mag Hydrator Strategies. Sie machen komplexe Angelegenheiten ziemlich einfach.
So viel programmiert und noch kein Formular?
Wir ihr sicherlich bemerkt habt, haben wir uns bis hier hin lediglich mit der Collection beschäftigt. Wir haben aber noch kein Formular, in dem die Collection bzw. das Fieldset, welches unsere Collection enthält, dargestellt wird. Genau das werden wir jetzt erledigen.
namespace Application\Form;
use Zend\Form\Form;
class Formular extends Form
{
public function __construct($name = null, $options = [])
{
parent::__construct($name, $options);
$this->add([
'name' => 'kontakt',
'type' => PhoneNumberFieldset::class,
]);
}
}
Natürlich könnte man dieser Formular Klasse noch weitere Eingabefelder hinzufügen. Um es kurz zu halten, habe ich hier lediglich unser Fieldset hinzugefügt, welches unsere Collection beinhaltet. Gerade wegen der guten Vorarbeit, die wir mir unseren Fieldsets, Entitäten und Hydratoren geleistet haben, ist das implementieren der Collection in einem Formular so einfach geworden. Ihr ahnt sicherlich, was jetzt noch kommt. Richtig! Natürlich benötigen wir auch für dieses Formular eine Entität.
Das Formular – Noch eine Entität
Wenn wir es strukturiert und ordentlich haben möchten, programmieren wir natürlich auch für das Formular eine Entität.
namespace Application\Entity;
class Formular implements \JsonSerializable
{
protected $kontakt;
public function getKontakt() : PhoneNumberFieldset
{
return $this->kontakt;
}
public function setKontakt(PhoneNumberFieldset $kontakt) : Formular
{
$this->kontakt = $kontakt;
return $this;
}
public function jsonSerialize() : array
{
return get_object_vars($this);
}
}
Lange Rede kurzer Sinn: Da unser Formular in diesem Beispiel nur ein Element enthält, fällt die Entität auch sehr gering aus. Es gibt hier lediglich unser Kontakt Fieldset, in das wir unsere Telefonnummern eintragen. In einem praktischen Beispiel würden hier weitaus mehr Elemente vorhanden sein. Gerade in Enterprise Entwicklungen enthalten solche Formulare mehrere Fieldsets und Collections.
Das Formular – Die allerletzte Factory
Selbstverständlich lässt sich dieses komplexe Formular nicht einfach so initialisieren. Um alles zusammen zu führen, benötigen wir eine Factory für das Formular, in der wir das Formular, die Entität, den Hydrator und einen eventuellen Input Filter für die Validierung des Formulars zusammenführen.
namespace Application\Form\Service;
use Application\Hydrator\Strategy\Kontakt;
use Interop\Container\ContainerInterface;
use Zend\Hydrator\ClassMethods;
use Zend\InputFilter\InputFilter;
class Formular
{
public function __invoke(ContainerInterface $container)
{
$filter = new InputFilter();
$hydrator = (new ClassMethods(false))->addStrategy('kontakt', new Kontakt());
$entity = new Application\Entity\Formular();
$form = (new Application\Form\Formular())
->setHydrator($hydrator)
->setInputFilter($filter)
->setObject($entity);
return $form;
}
}
Da wir keine weiteren Formular Elemente definiert haben, benutzen wir einen einfachen Input Filter für unser Formular. In einer realeren Umgebung würden wir natürlich jedes im Formular enthaltene Element filtern und validieren. Die Collection und das Fieldset implementieren das InputFilterProviderInterface und bringen somit schon ihre eigenen Filter und Validatoren mit. Aus diesem Grund brauchen wir hier keine weiteren Definitionen von Filtern und Validatoren. Der Hydrator benutzt eine weitere Hydrator Strategy, um die Entität für das Kontakt Fieldset mit Daten zu füttern. Wir weisen unserer Formular Klasse dann nur noch Filter, Hydrator und Entität zu und voilá! Wir haben ein vollständiges Formular.
Das Formular – Die letzte Hydrator Strategy
Da wir ein Fieldset als Wrapper für die Collection benutzen, benötigen wir noch eine letzte Hydrator Strategy, die die Eigenschaft Kontakt in unserer Formular Entität mit Daten befüllt. Das Prinzip von Hydrator Strategies habe ich bereits erklärt. Es folgt lediglich die Strategy Klasse.
namespace Application\Entity;
class Kontakt extends DefaultStrategy
{
public function hydrate($value)
{
return (new ClassMethods())
->addStrategy('telefonnummern', new Application\Hydrator\Strategy\Telefonnummern())
->hydrate($value, new Application\Entity\PhoneNumberFieldset());
}
}
Die Strategy für unser Fieldset, welches wir mit dem Namen kontakt im Formular festgelegt haben, sorgt dafür, dass die abgesendeten Daten in die entsprechenden Entitäten gelangen.
Wie sieht sowas dann im Controller aus?
Entgegen der Zend Framework Dokumentation für Form Collections haben wir unsere Collection in ein Fieldset gelegt. Aus diesem Grund binden wir auch keine Entitäten an ein Formular, obwohl die Dokumentation auf die bind Methode des Formulars hinweist. Diesen Job lassen wir allein von Hydratoren und den damit verbundenen Strategien erledigen. Aus diesem Grund sieht der Code im Controller auch wesentlich aufgeräumter aus.
namespace Application\Controller;
use Zend\Mvc\Controller\AbstractActionController;
class IndexController extends AbstractActionController
{
protected $form;
public function __construct(Formular $form)
{
$this->form = $form;
}
public function indexAction()
{
$request = $this->getRequest();
if ($request->isPost()) {
// POST Daten an das Formular übergeben
$this->form->setData($request->getPost());
// Validierung des Formulars ausführen
if ($this->form->isValid()) {
// gefilterte Daten des Formulars
$data = $this->form->getData();
// Telefonnummern ausgeben
foreach ($data->getKontakt()->getTelefonnummern() as $telefonnummer) {
echo $telefonnummer->getArt() . ': ' . $telefonnummer->getNummer();
}
}
}
return [
'form' => $this->form,
];
}
}
Dieser beispielhafte Controller wird natürlich über eine Factory erzeugt, in der wir dem Controller die Formular Instanz übergeben. Im direkten Vergleich zur Dokumentation lassen wir die ganze Initialisierung von Fieldsets und das Binding an das Formular einfach weg. Wir brauchen es einfach nicht, da dies die bisher erstellten Factories übernehmen. Der Code im Controller ist dadurch sehr viel aufgeräumter. Zusätzlich bekommen wir die gefilterten Daten vom Formular zurück und können die Daten mittels Chaining direkt als Entität und nicht als einfach strukturiertes Array benutzen. Frei nach Objektorientierung: Alles ist ein Objekt.
Und wie sieht das jetzt in der View Instanz aus?
Das Zend Framework bietet die Option ein Template für die Collection zu nutzen. Als wir unsere Collection programmiert haben, haben wir die Option zum Erstellen des Templates auf true gesetzt. Im View sieht das dann wie folgt aus.
< ?php
$form = $this->form;
$form->setAttribute('action', $this->url('home'));
$form->prepare();
?>
< ?= $this->form()->openTag($form); ?>
< ?= $this->formCollection($form->get('kontakt')->get('telefonnummern'));
< ?= $this->formCollection()->renderTemplate($form->get('kontakt')->get('telefonnummern')); ?>
< ?= $this->form()->closeTag(); ?>
Im Grunde genommen bietet die View Instanz alles schon von Hause aus. Wir müssen hier nichts zusätzlich programmieren. Die Collection selbst können wir mit dem View Helper formCollection darstellen. Dieser durchläuft die Collection als Schleife und endet erst, wenn die maximale Anzahl der Collections erreicht ist. Die Anzahl der Collections haben wir mit der Option count in unserer Collection auf 1 gesetzt. Zusätzlich können wir über die Collection ein Template generieren. Dieses Template wird als Data Attribut eines Span Tags abgelegt. Mit dem Template und der Anzahl der Collections als Hidden Element in unserem Formular können wir nun per JavaScript Collections hinzufügen oder auch wieder entfernen. Das Template enthält unseren ebenfalls in der Collection festgelegten Platzhalter __index__, den wir komfortabel per Javascript mit der Anzahl der Collection ersetzen können.
So könnte eine JavaScript Funktion zum dynamischen Hinzufügen einer weiteren Collection aussehen.
Fazit
Das Beispiel in der Zend Framework Dokumentation ist sehr dürftig, wenn es um den wirklich praktikablen Einsatz von Form Collections geht. Das Beispiel erwähnt zum Beispiel nicht, dass man ein Fieldset als Wrapper für die Collection benötigt, um verschiedene Collections in einem Formular unterbringen zu können. Darüber hinaus arbeitet das Zend Framework Beispiel in der Dokumentation mit dem bind an das Formular, welches in meinen Augen nur mit einer einzelnen Collection in einem Formular funktionieren kann. Allerdings führen viele Wege nach Rom und Collections sind weitaus dynamischer, als es uns die Zend Framework Dokumentation vermitteln mag. Collections sind ein hilfreiches Mittel, um mit komplexen Formulardaten umgehen zu können. Gerade in Verbindung mit Entitäten, Input Filtern und Hydratoren und den damit einhergehenden Strategien ist der Einsatz von Collections sehr praktikabel. Allerdings ist der Aufwand bis zur entgültigen Fertigstellung eines Formulars mit einer Collection sehr aufwendig. Allein für dieses kleine Beispiel wurden zehn Klassen ins Leben gerufen. Ich für meinen Teil kann aber immer noch sagen, dass sich dieser Aufwand lohnt, wenn es um komplexe Formulare geht.
Bildquelle: Pixabay / CC0 Licence
Was passier bei den ::class ? bzw welchen Sinn haben diese?
Guten Morgen Marcel,
leider ist Dein Kommentar in den Weiten des Spam Filters gelandet und ich habe ihn erst heute Morgen gesehen. Eine Antwort möchte ich Dir dennoch nicht schuldig bleiben. Das ::class Keyword ist seit PHP 5.5 verfügbar und gibt Dir den voll qualifizierten Klassennamen inklusive des verwendeten Namespaces für die Klasse zurück. Das Zend Framework akzeptiert voll qualifizierte Klassennamen und arbeitet damit minimal schneller, da es den qualifizierten Klassennamen für das Autoloading nicht erst anhand von den sonst benutzen Short Handles, wie z.B. ‚Text‘, ermitteln muss. Zudem ist es in der IDE einfacher auf die entsprechende Klasse zuzugreifen.
Quelle: http://php.net/manual/en/migration55.new-features.php#migration55.new-features.class-name