Piotr Rybałtowski

Programista PHP, Symfony

PHPProgramowanieZend Framework

Modele i formularze Zend Framework – automatyzacja zapisu

Edycja artykułu w serwisie wołomiński.netNa 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!

Skomentuj lub zadaj pytanie

Witryna wykorzystuje Akismet, aby ograniczyć spam. Dowiedz się więcej jak przetwarzane są dane komentarzy.