Modele i formularze Zend Framework – automatyzacja zapisu
Na codzień pracuję w Zend Framework. Praca jest bardzo przyjemna, framework jest fajnie napisany, obiektowo, elastycznie (wiele klas można rozszerzyć, jest wiele miejsc, w których domyślne klasy są gotowe do uruchomienia naszego kodu, itp.). Ale ma też wiele wad i braków, które często są rozwiązane w innych frameworkach. Twórcy ZF wiedzą o wielu z nich i obiecują, że szykowana wersja 2.0 będzie lepiej przemyślana i napisana. Ale póki nie mamy wersji 2.0 (a nawet jak będzie, nie wiadomo, czy wszystkie problemy zostaną weliminowane) trzeba sobie jakoś radzić. Jednym z problemów, które spotykam w niemal każdym projekcie, jest mechanizm zapisywania danych wysłanych przez użytkownika.
Generalnie zapisywanie informacji w większości przypadków wygląda bardzo podobnie: po wejściu na stronę użytkownik otrzymuje formularz, który wypełnia i zatwierdza; jeśli dane przesłasne są poprawne są zapisywane do bazy (tworzony jest nowy obiekt lub aktualizowany istniejący); jesli niepoprawne – użytkownik otrzymuje ten sam formularz z wpisanymi już wcześniej danymi i komunikatami o błędnych danych. Bardzo często powtarzający się schemat. Czemu więc nie ułatwić sobie życia?
Co jest potrzebne? Formularz, widok, kontroler i model.
Formularz powinien zapewniać (poza wyświetlaniem formularza) pełną walidację i filtrowanie danych. Np. edycja danych kotantaktowych, która zawiera między innymi email i telefon powinna sprawdzać oba pola. Do emaila jest klasa w Zendzie, numer telefonu może być np. wyrażeniami regularnymi. Pola obowiązkowe, listy (np. państw czy województw) itp. – to wszystko sprawa formularza i wiele narzędzi przydatnych w tym temacie jest już w Zendzie. Klasę formularza do kontrolera można tworzyć osobno albo korzystać z plików konfiguracyjnych (ini, xml) na podstawie których Zend potrafi taki formularz przygotować (niżej opisany kontroler korzysta z pierwszego sposobu).
Widok – tu wystarczy
<?php echo $this->form; ?>
jeśli pod form przypisany jest obiekt klasy Zend_Form. To co przed i po formularzu można dopisać. Za wyświetlanie/renderowanie formularza powinnien odpowiadać obiekt i jego dekoratory. W ostateczności można uciec się do dekoratora ViewScript, ale nadal w widoku akcji kodu formularza nie ma.
Jeżeli w całej aplikacji będziemy stosowali podobne nazewnictwo akcji i parametrów, to każdy kontroler może dziedziczyć wszystko po jednym – wspólnym. Ja polecam napisanie takiego kontrolera abstrakcyjnego, który będzie działał z domyślnymi akcjami create i update, a w kluczowych miejscach będzie wykonywał opcjonalne metody, które w konkretnych kontrolerach można rozszerzać: (1) przed edycją, (2) po edycji z sukcesem, (3) po edycji z błędami.
Przykład kontrolera:
<?php abstract class Application_Controller_Action extends Zend_Controller_Action { /** * @var string */ protected $_suffixNameByController; /** * @return string */ protected function _getSuffixNameByController() { if(null === $this->_suffixNameByController) { $controllerName = $this->getRequest()->getControllerName(); $filter = new Zend_Filter_Word_UnderscoreToCamelCase(); $this->_suffixNameByController = $filter->filter($controllerName); } return $this->_suffixNameByController; } /** * @param array $nameParts * @return string */ protected function _getPrefixedNameByController($nameParts) { $nameParts = (array)$nameParts; $nameParts[] = $this->_getSuffixNameByController(); return implode('_', $nameParts); } /** * @return string */ protected function _getDbTableNameByController() { return $this->_getPrefixedNameByController(array('Application', 'Model', 'DbTable')); } /** * @return Application_Model_DbTable */ protected function _getDbTableByController() { $dbTableClass = $this->_getDbTableNameByController(); return new $dbTableClass; } /** * @return string */ protected function _getFormNameByController() { return $this->_getPrefixedNameByController(array('Application', 'Form')); } /** * @return Zend_Form */ protected function _getFormByController() { $formClass = $this->_getFormNameByController(); return new $formClass; } /** * @param $row Application_Model * @return void */ protected function _afterSave($row = null) { if(null === $row) { $url = $this->view->url(array( 'action' => 'index', 'id' => null )); } else { $url = $this->view->url(array( 'action' => 'read', 'id' => $row->getId() )); } $this->_redirect($url, array('code' => 303)); } /** * @return Application_Model */ protected function _loadObjectFromId() { $id = (int)$this->_getParam('id'); if($id < 1) throw new Zend_Exception('No valid ID found'); $table = $this->_getDbTableByController(); return $table->find($id)->current(); } /** * @return void */ public function createAction() { $form = $this->_getFormByController(); if($this->getRequest()->isPost()) { if($form->isValid($this->getRequest()->getPost())) { $dbTable = $this->_getDbTableByController(); $row = $dbTable->createRow(); $row->setFromArray($form->getValues()); $row->save(); $this->_afterSave($row); } } $this->view->assign(array( 'form' => $form )); } /** * @return void */ public function updateAction() { $row = $this->_loadObjectFromId(); $form = $this->_getFormByController(); if($this->getRequest()->isPost()) { if($form->isValid($this->getRequest()->getPost())) { $row->setFromArray($form->getValues()); $row->save(); $this->_afterSave($row); } } else { $form->populate($row->toValues()); } $this->view->assign(array( 'row' => $row, 'form' => $form )); } }
Klasa „zgaduje” nazwę formularza i modelu tabeli na podstawie nazwy kontrolera w zapytaniu, więc używając standardowych nazw i operacji, akcje create i update można w końcowych kontrolerach pominąć. Służą do tego pierwsze metody w klasie. Są rozbite na mniejsze, ale zawsze uważam, że dobrze być przygotowanym – sama nazwa klasy formularza może też być potrzebna.
Przy edycji obiektu, ID należy przekazać w parametrze id, np. http://www.example.com/article/update/id/563.
Na koniec zostaje jeszcze model.Powyższy kontroler wykorzystuje standardowe metody ZF – createRow() i setFromArray(array()). W klasie wiersza można nadpisać metodę setFromArray() gdyby np. było potrzeba zapisać dane z innych tabel (połączenie z inną tabelą, itp.) – rzeczy, które wyświetla i sprawdza formularz, a kontroler nie musi nie powinien się już tym zajmować.
Co po zapisaniu danych? Uruchamiana jest metoda _afterSave(), która domyślnie przekierowuje do akcji read z ID ustawionym na ID zapisanego właśnie wiersza (wymaga w nim metody (getId()), ale jest przygotowana na bycie nadpisaną i wykonanie cokolwiek będzie potrzebne w danym projekcie/kontrolerze.
Jak tego użyć? Jeżeli będziemy trzymali się w projekcie kilku zasad (dziedziczenie odpowiednich elementów po odpowiednich klasach) w naszym kontrolerze do edycji danego elementu (typu PostsController, UsersController, BooksController) możemy całkiem pominąć akcje create i update używając generycznych. W razie potrzeby poprawki – zmiany dokonujemy w jednym miejscu. Magia programowania obiektowego. Miłego używania!