Zend Framework FAQ: Wo platziere ich am besten meine Validatoren und Filter, im Controller, im Formular oder im Model?

Bei der zweiten Abstimmung für die Zend Framework Fragestunde, wurde diese Frage mit 29% der Stimmen auf Platz 1 gewählt: Wo platziere ich am besten meine Validatoren und Filter, im Controller, im Formular oder im Model? Setze ich dabei Zend_Filter_Input ein? Es hat ein wenig gedauert (mehr als zwei Monate), bis ich nun endlich dazu gekommen bin, mich dieser Frage anzunehmen. Dies hatte mehrere Gründe, denn ich war sowohl beruflich als auch privat sehr eingespannt. Bin ich eigentlich immer noch, aber heute habe ich mir die Zeit genommen. Sonst wird die Fragestunde auch eher langweilig und wieder schnell vergessen.

Ein Grund, weshalb die Antwort so lange auf sich warten liess, war auch, dass diese Frage auf den ersten Blick gar nicht so leicht zu beantworten ist. Der Frage ist schon zu entnehmen, dass es viele Möglichkeiten gibt. Werden die Validatoren und Filter im Controller, im Formular oder im Model platziert? Verwendet man lieber Zend_Form oder Zend_Filter_Input? Ich werde hier nun zwei Lösungswege vorstellen, möchte aber auch darauf hinweisen, dass es weitere sinnvolle und gute Lösungen gibt. Und eigentlich ist die Frage auch etwas ungenau gestellt. Denn das Platzieren der Validatoren und Filter ist relativ klar. Unklar ist eher, wann und wo das Filtern und Validieren ausgeführt wird. Doch dazu gleich mehr.

Ansatz 1: Zend_Form zum Filtern und Validieren verwenden

Es ist nahe liegend, für das Filtern und Validieren von Eingabedaten Zend_Form zu verwenden. Diese Komponente bietet bereits alles, was man braucht. Es ist recht einfach, einem Formular beliebige Filter und Validatoren zuzuordnen. Dies ist auch meiner Meinung nach gut im Referenzhandbuch für Zend_Form erläutert, so dass ich dies hier nicht wiederholen möchte. Auch im Quickstart findet sich ein Beispiel. Komplizierter ist aber die Frage, wo in einer Anwendung die isValid() Methode einer Zend_Form Instanz aufgerufen wird.

Als erstes bietet sich der Einsatz im Action-Controller an. Dies ist auch nahe liegend, da der Controller unter anderem auch für die Verarbeitung und Weitergabe der Eingabedaten zuständig ist. Ein Model oder gar eine Formularinstanz sollte nie direkt auf die Daten zugreifen, die über den Request des Benutzers eingehen. In der Regel könnte solch ein Abfrage mehr oder minder wie im folgenden Beispiel aussehen. Es wird eine Instanz des Formulars erstellt, dann wird geprüft, ob das Formular gültig ist. Falls ja, wird erst die Instanz des Models erstellt und dann kann wie in diesem Beispiel ein neues Passwort für einen Benutzer erstellt werden.

PHP:
  1. function passwordAction()
  2. {
  3.   // Formular für neues Benutzerpasswort
  4.   $form = new App_Form_UserPassword();
  5.  
  6.   // prüfen ob Formular mit Button versandt wurde
  7.   if ($this->getRequest()->isPost() && !is_null($this->getRequest()->getPost('submit_user_password')))
  8.   {
  9.     // prüfen ob Eingaben gültig sind
  10.     if ($form->isValid($this->getRequest()->getPost()))
  11.     {
  12.       // Model Instanz erstellen
  13.       $user = new App_Model_Users();
  14.  
  15.       // Neues Passwort generieren
  16.       $user->generatePassword($form->getValue('user_email'));
  17.  
  18.       // umleiten auf Bestätigung
  19.       return $this->_redirect($this->getHelper('url')->url(array('action' => 'password-sent', 'id' => $user->getId())));
  20.     }
  21.  
  22.     // Daten sind ungültig
  23.     else
  24.     {
  25.       // Fehlermeldung festlegen
  26.       $form->setDescription('message_user_password_error');
  27.     }
  28.   }
  29.  
  30.   // Formular an View übergeben
  31.   $this->view->passwordForm = $form;
  32. }

Diese Verwendung ist nahe liegend und sieht auf den ersten Blick auch solide aus. Und es funktioniert auch soweit ganz gut. Das Problem ist nur, was man macht, wenn ein Formular z.B. an verschiedenen Stellen einer Anwendung zum Einsatz kommt. Dann muss in der jeweiligen Aktionsmethode des Action-Controller diese Prüfung laufend wiederholt wird. Ich denke da an den Ansatz der "thin controller and fat models" und möchte die Aktionsmethoden so einfach wie möglich halten. Deshalb möchte ich die eigentliche Prüfung der Daten lieber aus dem Controller heraus in das Model verlagern.

Dies hat auch den Vorteil, dass die Prüfung der Daten auch gewährleistet ist, falls ein Model einen neuen Datensatz anlegen soll, der nicht über ein Web-Formular in der Anwendung landet (z.B. beim Importieren von Daten aus einer externen Quelle). Und es macht keinen Sinn, die Prüfung der Eingabedaten redundant im Controller und im Model zu implementieren.

Zuerst schauen wir uns den Ausschnitt aus dem Model an. Es erstellt eine Instanz des Formulars und überprüft die Korrektheit der Daten, wenn das Formular abgeschickt worden ist. Wenn die Eingaben korrekt ware, wird ein neues Passwort erstellt und gespeichert, danach wird eine Mail mit dem Passwort versandt. Wenn dies nicht geklappt hat oder das Formular nicht versandt wurde, wird die Instanz des Formulars zurück gegeben.

PHP:
  1. class App_Model_Users
  2. {
  3.     public function generatePassword(array $data = array())
  4.     {
  5.         // Formular für neues Benutzerpasswort
  6.         $form = new App_Form_UserPassword();
  7.        
  8.         // prüfen ob Formular mit Button versandt wurde
  9.         if (isset($data['submit_user_password'])) {
  10.             // prüfen ob Eingaben gültig sind
  11.             if ($form->isValid($data)) {
  12.                 // Daten übernehmen, Passwort erstellen und speichern
  13.                 $this->setProperties($form->getValues());
  14.                 $this->setPassword(App_Model_Users::generatePassword());
  15.                 $this->save();
  16.                
  17.                 // Mail mit neuem Passwort aufbauen und versenden
  18.                 $mail = new Zend_Mail();
  19.                 $mail->setFrom('webmaster@mydomain.de');
  20.                 $mail->addTo($this->getEmail());
  21.                 $mail->setSubject('Neues Passwort');
  22.                 $mail->setBodyText('Passwort: ' . $this->getPassword());
  23.                 $mail->send();
  24.                
  25.                 // Registrierung erfolgreich
  26.                 return true;
  27.            
  28.             } else {
  29.                 // Fehlermeldung festlegen
  30.                 $form->setDescription('message_user_password_error');
  31.             }
  32.         }
  33.        
  34.         // Formularobjekt zurück geben
  35.         return $form;
  36.     }
  37. }

Als nächstes schauen wir uns den Action-Controller an. Hier wird nun nicht mehr die Instanz des Formulars, sondern die des Models erstellt. Es gibt eine spezielle Methode für das Erstellen eines neuen Passworts. Diese gibt entweder ein true oder eine Formularinstanz zurück.

PHP:
  1. class UserController extends Zend_Controller_Action   
  2. {
  3.     public function passwordAction()
  4.     {
  5.         // Model für Benutzer instanzieren
  6.         $user = new App_Model_Users();
  7.        
  8.         // Passwort erstellen
  9.         $form = $user->generatePassword($this->getRequest()->getPost());
  10.        
  11.         // Prüfen auf Redirect
  12.         if (true === $form) {
  13.             // umleiten auf Bestätigung
  14.             return $this->_redirect($this->getHelper('url')->url(array('action' => 'password-sent', 'id' => $user->getId())));
  15.         }
  16.        
  17.         // Übergebe Formular an den View
  18.         $this->view->passwordForm = $form;
  19.     }
  20. }

Wir verwenden nun also das Zend_Form Objekt innerhalb unseres Models und der Action-Controller muss sich um fast nichts mehr kümmern. Nur die Weiterleitung und die Übergabe an den View sind noch für ihn zu erledigen.

Ansatz 2: Zend_Filter_Input zum Filtern und Validieren verwenden

Verzichtet man aus welchen Gründen auch immer auf den Einsatz von Zend_Form, kann man das Filtern und Validieren alternativ auch von Zend_Filter_Input erledigen lassen. Die Funktionsweise ist im Referenzhandbuch erklärt, so dass ich mir die Einführung von Zend_Filter_Input an dieser Stelle sparen möchte.

Der Einsatz innerhalb eines Models ist aber recht schnell dargestellt. Die Unterschiede zum Einsatz von Zend_Form sind marginal.

PHP:
  1. class App_Model_Users
  2. {
  3.     public function generatePassword(array $data = array())
  4.     {
  5.         // Zend_Filter_Input Instanz
  6.         $input = new App_Filter_UserPassword();
  7.        
  8.         // prüfen ob Formular mit Button versandt wurde
  9.         if (isset($data['submit_user_password'])) {
  10.             // prüfen ob Eingaben gültig sind
  11.             if ($input->isValid($data)) {
  12.                 // Daten übernehmen, Passwort erstellen und speichern
  13.                 $this->setProperties($input->getEscaped());
  14.                 $this->setPassword(App_Model_Users::generatePassword());
  15.                 $this->save();
  16.                
  17.                 // Mail mit neuem Passwort aufbauen und versenden
  18.                 $mail = new Zend_Mail();
  19.                 $mail->setFrom('webmaster@mydomain.de');
  20.                 $mail->addTo($this->getEmail());
  21.                 $mail->setSubject('Neues Passwort');
  22.                 $mail->setBodyText('Passwort: ' . $this->getPassword());
  23.                 $mail->send();
  24.                
  25.                 // Registrierung erfolgreich
  26.                 return true;
  27.         }
  28.        
  29.         // Nicht erfolgreich
  30.         return false;
  31.     }
  32. }

Da ich selber Zend_Form sehr gerne und viel einsetze, habe ich Zend_Filter_Input noch nicht so häufig in der Praxis eingesetzt. Aber es ist durchaus möglich, wie das Beispiel zeigt.

Fazit

Das Filtern und Validieren von Eingabedaten ist im Model meistens am besten aufgehoben. Ob man nun Zend_Form oder Zend_Filter_Input einsetzt, bleibt einem selbst überlassen. Beides ist möglich.

Wenn es Fragen gibt oder ihr dies oder jenes völlig anders macht, stehen dafür nun die Kommentare bereit. Ich freue mich auf euer Feedback!

Tweet this via redir.ec

6 Antworten für “Zend Framework FAQ: Wo platziere ich am besten meine Validatoren und Filter, im Controller, im Formular oder im Model?”

  1. Paul Parenko sagt:

    Man könnte die Ansätze auch zusammen führen. Zum Beispiel in dem man der Methode generatePassword einen zweiten Parameter übergibt.

    Dann überprüft man von welcher Instanz $input ist und nutzt dementsprechende Methoden.

    PHP:
    1. class App_Model_Users
    2. {
    3.     public function generatePassword(array $data = array(), $input = null)
    4.     {
    5.         // wenn $input nicht angegeben, standardmäßig ein vorgeben
    6.         // oder prüfen ob Instanz von Zend_Form ist
    7.         if (null === $input || $input instanceof Zend_Form) {
    8.             // Formular für neues Benutzerpasswort
    9.             $input = new App_Form_UserPassword();
    10.             $isForm = 1;
    11.         } else {
    12.             // prüfen ob es Instanz von Zend_Filter_Input ist
    13.             if ($input instanceof Zend_Filter_Input) {
    14.                 // Zend_Filter_Input Instanz
    15.                 $input = new App_Filter_UserPassword();
    16.             } else {
    17.                 // Werfe ein Fehler
    18.                 throw new App_Model_Exception('Kein Filter/Form Objekt übergeben');
    19.             }
    20.             $isForm = 0;
    21.         }
    22.  
    23.         // prüfen ob Formular mit Button versandt wurde
    24.         if (isset($data['submit_user_password'])) {
    25.             // prüfen ob Eingaben gültig sind
    26.             if ($input->isValid($data)) {
    27.                 // richtige methode nutzen
    28.                 if ($isForm) {
    29.                     // Daten übernehmen, Passwort erstellen und speichern
    30.                     $this->setProperties($form->getValues());
    31.                 } else {
    32.                     // Daten übernehmen, Passwort erstellen und speichern
    33.                     $this->setProperties($input->getEscaped());
    34.                 }
    35.                 $this->setPassword(App_Model_Users::generatePassword());
    36.                 $this->save();
    37.                 // Mail mit neuem Passwort aufbauen und versenden
    38.                 $mail = new Zend_Mail();
    39.                 $mail->setFrom('webmaster@mydomain.de');
    40.                 $mail->addTo($this->getEmail());
    41.                 $mail->setSubject('Neues Passwort');
    42.                 $mail->setBodyText('Passwort: ' . $this->getPassword());
    43.                 $mail->send();
    44.                 // Registrierung erfolgreich
    45.                 return true;
    46.             }
    47.             if ($isForm) {
    48.                 // Fehlermeldung festlegen
    49.                 $input->setDescription('message_user_password_error');
    50.                 // Formularobjekt zurück geben
    51.                 return $input;
    52.  
    53.             } else {
    54.                 // Nicht erfolgreich
    55.                 return false;
    56.             }
    57.         }
    58.     }
    59. }

  2. Ralf sagt:

    Danke Paul. Dabei sollte man aber beachten, dass man die Filter und Validatoren auch in einem Zend_Form und einem Zend_Filter_Input Objekt vorhalten müsste. Sonst würde diese Erweiterung keinen Sinn machen. Aber ansonsten wäre man damit schon etwas flexibler, wenn man das braucht.

  3. Ulf sagt:

    Hm, ich bin klar gegen die Verwendung von Form-Objekten (Filter-Objekten) innerhalb des Models. Es verstößt meiner Meinung nach gegen "Separation of Concerns".

    Eine Methode generatePassword einer Model-Klasse sollte ein Passwort generieren und einen booleschen Wert zurück geben. Damit stimme ich überein, aber wieso kann das Model nur ein Passwort auf Basis eines Formulars generieren? Somit kann ich diese Methode nicht wiederverwenden, wenn ich z.B. automatisch ein Passwort generiere bzw. es zurücksetze, da ich meine Logik viel zur sehr an das Formular gekapselt habe. Grundsätzlich gehört die Verarbeitung von Formulardaten in den Controller. Wenn man gleiche Formulare über mehrere Controller benötigt und Code nicht duplizieren möchte, sind doch ActionHelper der viel bessere Lösungsansatz. Entitäten einer Anwendungen werden doch nicht zwangsläufig nur mit Formularen erstellt (und wenn dann kann sich diese Anforderung doch schnell ändern eben durch bsw. Imports).

    Meine Antwort zu der eigentlichen Frage sähe so aus:
    1) Filter / Validatoren gehören in das Formular an die eigentlichen Formular-Elemente damit eben nie invalide / ungefilterte Daten verarbeitet werdne können.
    2) Die Verarbeitung / Validierung von Daten gehört in den Controller.
    3) Das Model nimmt die Daten vom Controller entgegen. Da diese schon gefiltet / validiert sind, braucht man diese dort nicht zu überprüfen. Das Model hält sie eben nur (persistent) vor.

    P.S. Und führt nicht Zeile 14 der App_Model_Users Klasse zu einer Endlosschleife. Ich rufe doch dort die selbe Funktion statisch auf ohne eine Abbruchbedingung zu haben.

  4. Ulrich Berkmüller sagt:

    @Ulf: eine Endlosschleife ist das zwar nicht, dennoch stimmt da was nicht, da beim Aufruf der selben Methode ohne Parameter false zurückkommt und nicht das Passwort. Auch der statische Aufruf einer nicht statischen Methode ist an dieser Stelle wohl nicht so gewollt.

    Generell muss ich Ulf an dieser Stelle auch zustimmen, dass das Formular an sich nichts im Model zu suchen haben sollte. Das Model an sich sollte eigentlich Businesslogik von Technik/Frameworks/... so gut wie es geht trennen, um keine Abhängigkeiten zu erzeugen. Damit kann dann die Businesslogik an sich auch komplett in einem anderen Kontext wiederverwendet werden. Also auch beispielsweise leicht mit einem anderen Framework oder nur einem Konsolenscript.

    Ralfs Ansatz ist allerdings keinesfalls schlecht. Das Problem der Frage ist allerdings nicht einfach zu lösen, wenn man wirklich nach dem Prinzip separation of concerns gehen will. Ich denke, dass das Problem an sich viel weiter auf PHP selbst bzw. dessen Community zurückzuführen ist. So einen sauberen Trennungsansatz bekommt man nur besten Gewissens hin, wenn "standardisierte" Interfaces von PHP selbst, beispielsweise in der SPL gestellt werden bzw allgemein mit PHP eine offizielle Bibliothek mitgeliefert wird, die sich um solche Basisdinge wie Collections, Wichtige Standard-Exception Hierarchie und eben ein paar wichtige Interfaces mitliefert, die dann ALLE Frameworks, Bibliotheken usw. benutzen.

    Alternativ kann man selbst seine eigenen Interfaces für die Businesslogik erstellen und Adapterklassen implementieren, die das Zend Framework nutzen. So kann man beispielsweise ein Validateable Interface erstellen mit einer isValid($data) Methode und eine Adapterklasse, die dann mein Interface Implementiert und isValid auf der Zend_Form Instanz aufruft.

    Ein weiteres Problem wäre dann wieder, dass die einzelnen Validatoren, die ich dann vom ZF benutze auch wieder mit dem ZF verdrahtet sind und somit ja eigentlich in meinem Model nichts zu suchen haben, aber eigentlich auch dort hingehören, da sie Teil der Anwendungslogik und nicht des Formulars/Controllers/... sind.

    Wie man sieht, ist das Problem an sich alles andere als einfach und fordert hier und da Kompromisse.

    Ausblicke geben allerdings Doctrine 2 und ZF2. Warum? Doctrine2 bringt einen schönen Ansatz mit, der zeigt, wie man Anwendungslogik und Technik (also Persistenzlogik) voneinander sauber trennen kann. Code-Generierung, Metainformationen in Dateien/Annotationen und Reflection sind hier wohl die Dinge, die einem dabei weiterhelfen können. Somit bleibt dann mein Code sauber, was technische Abhängigkeiten betrifft. ZF2 verspricht, mehr gegen Interfaces zu programmieren.

    Zusammenfassend behaupte ich von meiner Seite aus jetzt einmal:
    Validatoren gehören in die Business-Logik. Filter zu den Formularen und die werden in den Controllern behandelt. Die Validatoren können auch noch in den Formularen sein, müssen aber auf jeden fall in der Anwendungslogik verankert sein, um die Daten darin immer Konsistent halten zu können.

  5. PHPGangsta sagt:

    Die Zeile
    $this->setPassword(App_Model_Users::generatePassword());
    wollte ich auch gerade ankreiden, aber das habt ihr ja nun schon getan.

    Ich bevorzuge auch das erste Beispiel mit Zend_Form.

    Ich verstehe auch nicht ganz, warum im App_Model_Users geprüft wird:
    // prüfen ob Formular mit Button versandt wurde
    if (isset($data['submit_user_password'])) {

    Du hattest doch extra geschrieben, dass du den Code in das Model verschieben willst, um später bei weiteren Aufrufen (zB beim Importieren von Daten aus externer Quelle, Cronjobs etc) nicht Code doppelt schreiben zu müssen. In dem Fall macht die Prüfung des Sumit-Buttons aber keinen Sinn. Ich glaube, das müßte im Action-Controller bleiben.

  6. Ulf sagt:

    Ich glaube Ralf hat den Rumpf der Model-Klasse Methode generatePassword() einfach nur vom Controller in das Model kopiert und es vergessen entsprechend anzupassen. Darum der statische reskurse Aufruf und das Überprüfen des geglickten Buttons.

    Im übrigen schon beim Schreiben meines ersten Kommentares habe ich kurz überlegt ob die Validierung der Daten nicht doch eher ins Model gehört. Ich habe mich dann aber dagegen entschieden aus verschiedenen Gründen:
    1) empfinde ich den Ansatz des Defensiven Programmierens (http://de.wikipedia.org/wiki/Defensives_Programmieren) als schwierig einsetzbar, da man somit sehr oft, sehr häufig redundant prüft. Dazu wird der Code schnell unübersichtlich und aufgebläht.
    2) Ich kann nicht Vorteile wie die native __set Funktion nutzen was besonders in Hinsicht auf automatisches Generieren von Models sinnvoll ist (Doctrine nutzt es in Version 1.* ja auch).
    3) Validatoren müssen auf jeden Fall bei Zend and Zend_Form_Elementen gebunden werden um die Vorteile von Zend_Form auch nutzen zu können (Decorator und Validierung). Somit würden Validatoren mehrfach genutzt werden insbesondere bei Regex-Prüfungen würde das teuer werden.
    4) PHP ist nicht typ sicher was die Validierung von Daten natürlich extrem erschwert. In Java lässt sich das alles durch Typsicherheit leichter umsetzen. Somit muss man für Typsicherheit bei Model-Daten einen ganzen "Rattenschwanz" mit nachziehen.

    Der Ausblick auf ZF2 zusammen mit Doctrine2 ist großartig. Verbunden mit PHP 5.3 wird es wohl der Meilenstein des jahres für die PHP-Community. ich bin fest davon überzeugt, dass man mit diesem Schritt Produktivität & Sicherheit & Skalierbarkeit usw. langfristig steigern wird. Ich würde es am Liebsten heute als morgen einsetzen. :)

Hinterlasse eine Antwort


Better Tag Cloud