REST w ZF2 - metody PUT i DELETE w formularzach

Architektura dostępu do danych REST staje się coraz bardziej popularna w Internecie. Coraz częściej trafiam na serwisy działające w ten sposób, coraz więcej frameworków obsługuje ten wzorzec. W Internecie dużo już napisano na temat samego sposobu implementacji, ale w skrócie chodzi w głównym stopniu o sposobie dostępu dodanych przez protokół HTTP wykorzystując 4 typy (metody) zapytań i odpowiednie formatowanie adresów URI. Zazwyczaj standardowe serwisy korzystają z zapytań GET i POST. REST proponuje wykorzystanie też PUT i DELETE (HTTP definiuje jeszcze kilka kolejnych, jeszcze rzadziej wykorzystywanych).

wykop.pl facebook.com twitter.com

Routery Part w Zend Framework 2 i sprawdzanie nazwy domeny

Przykładowa domena w aplikacji ZF2Zend Framework 2 jest już w wersji beta 3 i wielkimi krokami zbliża się faza RC i w końcu oficjalny release. Mam okazję pracować w tym frameworku przy kilku projektach i poznawać mniejsze i większe nowości, które nowe wydanie oferuje programistom. Jedną z tych mniejszych, ale bardzo ciekawych zmian jest wprowadzenie nowego typu routera adresów URL - Part.

Routery w aplikacjach webowych, to mechanizmy odwzorowujące adres żądania klienta (czyli adres wprowadzony do przeglądarki WWW) na odpowiednie zasoby systemu (w przypadku aplikacji MVC w ZF są to kontrolery akcji). Router typu Part umożliwia tworzenia zagnieżdżonych grup routingu, dzięki czemu można sprawdzać fragmenty adresów. Dokumentacja oficjalna pokazuje jak można testować część adresową URL, żeby w każdym przypadku nie duplikować początkowych fragmentów adresów. Jeszcze ciekawsze rzeczy można osiągnąć łącząc ten router z routerem domenowym.

Wyobraźmy sobie, że mamy aplikację, która ma osobny frontend dla odwiedzających użytkowników i backend do zarządzania treścią oraz obie te części znajdują się w różnych domenach (np. www.example.com i admin.example.com). Żeby móc konfigurować osobno kontrolery dla front- i backendu możmy skorzystać właśnie z kombinacji routerów part, hostname i pozostałych, bardziej standardowych. Dodatkowo, aby w jeden router połączyć niezależne ścieżki możemy skorzystać z RouteStacka - specjalnej klasy, która działa podobnie jak router, ale nie ma żadnych własnych reguł tylko sprawdza kolejne niezależne routery. Dzięki temu nie musimy tworzyć wspólnego początku ściężki i już na poziomie domeny możemy mieć zupełnie różne adresy.

W kodzie poniżej zostaną użyte następujące klasy:

use ZendMvcRouterHttpPart, // router fragmentów adresów
    ZendMvcRouterRouteBroker, // umożliwia automatyczne tworzenie obiektów na podstawie nazw klas
    ZendMvcRouterHttpTreeRouteStack; // umożliwia zagnieżdżanie wielu routerów

Aby skonfigurować obiekt RouteStack kolejno tworzymy routing dla frontendu:

$routeFrontend = Part::factory(array(
    'route' => array(
        'type' => 'Zend\Mvc\Router\Http\Hostname',
        'options' => array(
            'route' => 'www.example.com',
        ),
    ),
    'route_broker' => new RouteBroker,
    'child_routes' => array(
        'home' => array(
            'type' => 'Zend\Mvc\Router\Http\Literal',
            'options' => array(
                'route' => '/',
                'defaults' => array(
                    'controller' => 'Frontend\Controller\HomeController',
                    'action' => 'index',
                ),
            ),
        ),
    ),
));

routing dla backendu:

$routeBackend = Part::factory(array(
    'route' => array(
        'type' => 'Zend\Mvc\Router\Http\Hostname',
        'options' => array(
            'route' => 'admin.example.com',
        ),
    ),
    'route_broker' => new RouteBroker,
    'child_routes' => array(
        'dashboard' => array(
            'type' => 'Zend\Mvc\Router\Http\Literal',
            'options' => array(
                'route' => '/',
                'defaults' => array(
                    'controller' => 'Backend\Controller\DashboardController',
                    'action' => 'index',
                ),
            ),
        ),
    ),
));

i RouteStack:

$route = TreeRouteStack::factory(array(
    'route_broker' => new RouteBroker,
    'routes' => array(
        'frontend' => $routeFrontend,
        'backend' => $routeBackend,
    ),
));

Pozstałe części adresów (np. www.example.com/products) konfirugujemy wg schematu z wyżej wspomnianej dokumentacji.

Po wykonaniu $route->match($request); dostaniemy obiekt RouteMatch, który jako nazwę pasującej ścieżki będzie zawierał kolejne nazwy oddzielone znakiem /, czyli w powyższym przykładzie frontend/home lub backend/dashboard. Analogicznie w drugą stronę tworzyć adresy możemy przez $route->assemble(array(), array('name' => 'backend/dashboard')); gdzie w rezultacie otrzymamy pełny adres URI z protokołem i domeną (w przykładzie http://admin.example.com/).

wykop.pl facebook.com twitter.com

Uruchamianie serwera deweloperskiego na wirtualnej maszynie

VirtualBox ze skonfigurowanymi serwerami wirtualnymiW mojej codziennej pracy przy programowaniu aplikacji webowych mam wiele lokalnie skonfigurowanych wirtualnych hostów do obsługi poszczególnych projektów. Niestety wszystkie aplikacje działają w tym samym środowisku (wersja Apache, PHP, RoR, MySQL, itp.), a ewentualne zróżnicowanie jest problematyczne. Niedawno przeniosłem jednak projekty do wirtualnej maszyny, gdzie mogę mieć dowolną konfigurację środowiska nie zmieniając mojej lokalnej.

Co dokładnie daje mi takie rozwiązanie? Mam zainstalowany serwer linuksowy (Ubuntu w moim przypadku) na podstawowej maszynie i tam skonfigurowane wszystkie projekty. Są tam też uruchomione domyślne wersje serwerów z repozytorium. Jeżeli jednak potrzebuję sprawdzić działanie serwisu w innej wersji oprogramowania (np. PHP 6.0), robię klon wirtualnej maszyny, konfiguruję takie oprogramowanie, jakie potrzebuję i... voila, moje aplikacje uruchamiane są w nowym środowisku. Mój lokalny system jest niezmienony, a powrót do poprzedniej konfiguracji to poprostu wyłączenie nowego serwera i włączenie poprzedniego. Bardzo wygodne!

Największą zaletą tego rozwiązania jest to, że nie muszę zmieniać mojego środowiska programistycznego. Wszystkie pliki umieszczone są cały czas na mojej stacji roboczej i mogę na niej uruchamiać dowolne oprogramowanie do tworzenia kodu (np. NetBeans, RadRails czy gedit) oraz testowania (przeglądarki WWW).  I to wszystko za pomocą darmowego oprogramowania! Czy może być lepiej?

wykop.pl facebook.com twitter.com

PHP 5.4: wywołanie metody przy tworzeniu obiektu!

Logo PHPKrótko i na temat: najnowsze wydanie PHP 5.4 RC1 dostało nową funkcjonalność - wywoływanie metody od razu przy tworzeniu instancji obiektu. Bardzo często w kodzie pojawia się utworzenie obiektu tylko po to, żeby jedną metodę wywołać, np. pobrać dane z bazy:

$dbArticles = new DbArticles;
$articles = $dbArticles->fetch(array('page' => 3));

Teraz można to ubrać w jedno polecenie:

$articles = (new DbArticles)->fetch(array('page' => 3));

Zmiana kosmetyczne i głównie dla "upiększenia" kodu (chociaż przy okazji nie jest tworzona zmienna), ale moim zdaniem świeta rzecz. Zdarzało mi się wcześniej nawet robić metodę statyczną do pobierania instancji, żeby mniej więcej coś takiego zasymulować, podczas gdy np. JavaScript i innę języki mają to od dawna. Teraz ma to też PHP!

wykop.pl facebook.com twitter.com

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!

wykop.pl facebook.com twitter.com

AJAX i JSON w Zend Framework

Zacząłem niedawno nowy projekt. Kolejny oparty o Zend Framework, a tym razem w znacznym stopniu wykorzystujący tzw. AJAX. Jednak zamiast zwracać XML wolę dane dostawać jako JSON. Jak to robię, wykorzystując dobrodziejstwa ZF? Pokażę na przykładzie akcji logowania.

Akcja logowania nie generuje u mnie żadnych formularzy ani tekstu. Służy tylko do wywołania przez zapytanie asynchroniczne i zwrócenia danych jako JSON.

Oto kod akcji:

  1. class AccountController extends Zend_Controller_Action
  2. {
  3.     function signinAction()
  4.     {
  5.         if($this->view->checkLogin())die();
  6.         $this->_helper->layout->setLayout('json');
  7.         $this->view->json = array('error'=>0, 'signin'=>false, 'message'=>'');
  8.         if($this->_request->isPost())
  9.         {
  10.             $f = new Zend_Filter_StripTags();
  11.             $login = $f->filter($this->_request->getPost('login',''));
  12.             $password = $f->filter($this->_request->getPost('password',''));
  13.             if(!empty($login))
  14.             {
  15.                 $authAdapter = new Zend_Auth_Adapter_DbTable(Zend_Db_Table::getDefaultAdapter(), 'users', 'email', 'passwd', 'MD5(?) AND is_deleted = 0');
  16.                 $authAdapter->setIdentity($login);
  17.                 $authAdapter->setCredential($password);
  18.                 $auth = Zend_Auth::getInstance();
  19.                 $result = $auth->authenticate($authAdapter);
  20.                 if($result->isValid())
  21.                 {
  22.                     $data = $authAdapter->getResultRowObject(array('id_user', 'email', 'name', 'user_role'));
  23.                     $auth->getStorage()->write($data);
  24.                     $this->view->json['signin']=true;
  25.                 }
  26.             }
  27.         }
  28.         if(!$this->view->json['signin'])
  29.             $this->view->json['message']='Podane e-mail i hasło nie pasują do siebie.';
  30.     }
  31. }

Po kolei

Cała magia zaczyna się od

  1. $this->_helper->layout->setLayout('json');
  2. $this->view->json = array('error'=>0, 'signin'=>false, 'message'=>'');

czyli wybrania layoutu dla tej akcji i zainicjowania tablicy json w obiekcie view. Dalej odbywa się logowanie za pomocą modułu Zend_Auth (o tym może przy innej okazji) i odpowiednie wypełnianie tablicy json.

A po co był ten wybór layoutu? To nowość w wersji 1.5 frameworka. W pliku bootstrap (zazwyczaj index.php) należy go zainicjować dodając linijkę:

  1. Zend_Layout::startMvc(array('layoutPath'=>ROOT_DIR.'/application/views/layouts/'));

Domyślny layout należy umieścić w pliku /application/views/layouts/layout.phtml - będzie on wczytywany dopóki go dla danej akcji nie wyłączymy lub nie zmienimy. To drugie zrobiłem właśnie w akcji logowania. A w pliku /application/views/layouts/json.phtml wrzuciłem tylko:

  1. <?=$this->json(isset($this->json)?$this->json:array())?>

i wtedy cała odpowiedź akcji to przerobiona tablica json za pomocą zendowego helpera json(). Reszta to już kwestia JS i odpowiedniego odczytania zwróconego obiektu. Korzystam z mootools i tamtejszej metody Json.evaluate(); pobieranie danych za pomocą Json.Remote generowało błędy.

  1. /*
  2.  * url - adres akcji logowania - w przykładzie /account/signin/
  3.  * params - informacje logowania
  4.  */
  5. new Ajax(url, {'data':params, 'method': 'post', 'onComplete': function(t){
  6.         var data = Json.evaluate(t);
  7.         if(!data)
  8.                 alert('Wystąpił błąd podczas logowania!');
  9.         if(data.error)
  10.                 alert('Wystąpił błąd podczas logowania!');
  11.         if(data.signin)
  12.                 alert('Zalogowany!');
  13.         else
  14.                 alert(data.message);
  15. }

Podsumowanie

Oczywiście logowanie to tylko przykład tworzenia wyniku JSON. Najważniejsze elementy to:

  • zainicjowanie Zend_Layout;
  • utworzenie tablicy json (tylko ten krok należy wykonać w każdej akcji, pozostałe wykonają się automagicznie);
  • zdefiniowanie layoutu jsonowego;
  • przekształcenie tablicy do JSON w layoucie;
  • skrypt akcji w views może zostać pusty.
wykop.pl facebook.com twitter.com

dekoderek-jogg