Agavi - Wprowadzenie

Agavi jest frameworkiem MVC napisanym w PHP5. Jest on stosunkowo słabo znany w środowisku programistów PHP. Przyczyną tego jest to iż dopiero od niedawna dostępne jest Agavi oznaczone wersją 0.11, które ma zamrożone API. Jednak przede wszystkim framework ten nadal nie posiada dokumentacji, co właściwie go dyskwalifikuje. Developerzy Agavi obiecują że dokumentacja się wreszcie pojawi, ja tymczasem postaram się częściowo załatać dziurę serią artykułów o tym świetnym frameworku.

Model View Controller

Tak jak pisałem we wstępie - Agavi jest frameworkiem którego działanie opiera się na wzorcu projektowym - MVC - Model View Controller.

Wg. tego wzorca aplikacja dzieli się na 3 części. Nie zagłębiając się w szczegóły odpowiednie warstwy odpowiadają za

  • Model - pozyskiwanie danych (Baza danych, Web Services etc.)
  • View (Widok) - przedstawienie pozyskanych danych użytkownikowi (zwrócenie html, xml, pdf etc.)
  • Controller (Kontroler) - analiza żądania użytkownika i uruchomienie odpowiedniego modelu i widoku.

W Agavi znajdują się dodatkowo akcje które to są wywoływane przez kontroler. Akcja natomiast może posiadać kilka widoków.

Instalacja

Zacznijmy od utworzenia struktury folderów na naszym serwerze. Składać się na nią będą:

  • /agavi_application
    • /app - nasza aplikacja
    • /dev - narzędzia które będą pomoce w momencie tworzenia aplikacji
    • /libs - biblioteki (agavi, doctrine, smarty etc.)
    • /pub - publiczny folder

Możemy ściągnąć najnowszą wersję Agavi ze strony frameworka. W momencie pisania artykuły najnowszą wersją jest 0.11.1. Po ściągnięciu, należy rozpakować paczkę, zmienić nazwę folderu na “agavi” i przenieść go do folderu libs.

Następnie przygotujmy sobie generatora. Skrypt ten używa phing’a, aby go zainstalować należy wykonać następujące komendy w konsoli (musisz mieć skonfigurowany pear).

  1. $ pear channel-discover pear.phing.info
  2. $ pear install phing/phing

Skopiujmy skrypt agavi.bat-dist, który znajduje się w /libs/agavi/etc, do /dev zmieniając jego nazwę na agavi.bat. Używając naszego ulubionego edytora tekstowego zmieńmy ścieżki znajdujące się w skrypcie:

  1. set PHING_COMMAND=C:\Server\php\phing.bat
  2. set AGAVI_INSTALLATION=C:\Server\www\agavi_application\libs\agavi\src

Tak wyglądają ścieżki w moim przypadku. Pierwsza wskazuje plik skryptu phing, druga źródła agavi.

Mając już gotowe narzędzie możemy uruchomić konsole i wykonać skrypt agavi.bat:

  1. C:\Server\www\agavi_application> dev\agavi.bat project

Skrypt będzie nas pytał o różne ciekawe rzeczy. My grzecznie za każdym razem wciskamy enter. Po ok 45 sek. generator powinien poinformować nas że zakończył swoją pracę sukcesem, natomiast my po uruchomieniu aplikacji - http://localhost/agavi_application/pub/ powinniśmy zobaczyć logo agavi. Oznaczać to będzie że wszystko generator stworzył za nas pierwszą aplikację opierającą się na Agavi.

Budowa aplikacji

Aplikacje pisane na Agavi mają budowę modularną. Każdy moduł składa się z akcji. Akcja może mieć kilka widoków, widok natomiast może używać różnych szablonów. Przykładowo: moduł News, posiada akcję Add, która posiada widoki Input (formularz dodawania newsa) oraz Success (wiadomość o dodaniu newsa).

Tak w dużym skrócie wyglądają moduły. Moduły dodatkowo mogą posiadać własną konfigurację, pliki validatora, lub inne biblioteki które są używane przez moduł.

Stwórzmy sobie przykładowy moduł Notes, który będzie modułem notek bloga. Wygenerujmy sobie ten moduł używając generatora agavi:

  1. $ dev\agavi.bat module

Generator będzie pytał nas o:

  1. Nazwa modułu: Notes
  2. Akcje: Index,ShowOne,Add
  3. Widoki dla akcji Index,ShowOne: Success (obie akcje tylko po 1 widoku), dla akcji Add: Success,Input
  4. Modele: Notes

Generator powinien stworzyć za nas wszystkie foldery, pliki z szablonami klas, oraz puste pliki szablonów. Pozostaje nam teraz dopisać logikę aplikacji.

Konfiguracja

Agavi jest frameworkiem który w dużym stopniu opiera się na konfiguracji. Konfiguracja znajduje się w plikach XML, składa się na nią 13 podstawowych plików oraz dodatkowe pliki które może posiadać każdy z modułów. Ilość plików na początku może przerażać, jednak zapewniam że gdy dobrze poznamy ten framework, konfiguracja stanie się bardzo prosta i intuicyjna. Generator stworzył nam domyślną konfigurację i na niej będziemy się opierać. Zmienimy jedynie 4 pliki.

settings.xml

Ustawiamy akcję Index modułu Notes jako domyślną.

  1. <system_action name="default">
  2.                                 <module>Notes</module>
  3.                                 <action>Index</action>
  4.                         </system_action>

oraz włączamy komponent bazy danych:

  1. <setting name="use_database">true</setting>

databases.xml

Deklarujemy driver którego będziemy używać (w naszym przypadku PDO ale nic nie stoi na przeszkodzie abyś użył Propela, Doctrine lub cokolwiek innego) oraz dane potrzebne do nawiązania połączenia z bazą danych.

  1. < ?xml version="1.0" encoding="UTF-8"?>
  2. <configurations xmlns="http://agavi.org/agavi/1.0/config">     
  3.         <configuration>
  4.                 <databases default="pdo_mysql">   
  5.                         <database name="pdo_mysql" class="AgaviPdoDatabase">
  6.                                 <parameter name="dsn">mysql:host=localhost;dbname=agavi_application</parameter>
  7.                                 <parameter name="username">root</parameter>
  8.                                 <parameter name="password">pass</parameter>
  9.                         </database>               
  10.                 </databases>
  11.         </configuration>
  12. </configurations>

routing.xml

Plik z drogami. Zadeklarowaliśmy 3 scieżki. Pierwsza zostanie wybrana, gdy adres będzie wyglądał np. tak:
http://strona.pl/notes/123/html. Wykonana zostanie wtedy akcję ShowOne modułu Notes, przekazując parametr id o wartości 123. Druga pokaże nam formularz dodawania notki, a trzecia domyślną akcję.

  1. < ?xml version="1.0" encoding="UTF-8"?>
  2. <configurations xmlns="http://agavi.org/agavi/1.0/config">
  3.         <configuration>
  4.                 <routes>                       
  5.                         <route name="note_one" pattern="^/note/(id:\d+).html$" module="Notes" action="ShowOne" />
  6.                         <route name="note_add" pattern="^/add.html$" module="Notes" action="Add" />
  7.                         <route pattern="^/$" module="%actions.default_module%" action="%actions.default_action%" />
  8.                 </routes>
  9.         </configuration>
  10. </configurations>

output_types.xml

W sekcji layouts, dodajemy jedną warstwę. Dekorator, czyli nasz “główny” szablon.

  1. <layer name="decorator">
  2.       <parameter name="template">%core.app_dir%/templates/Master</parameter>
  3. </layer>

Model

Pisanie aplikacji zaczniemy od modelu. Zaimplementujemy w nim metody które będą potrzebne dla akcji/widoków. Będą to metody: getRecent (zwrócenie ostatnich kilku notek), getById (zwrócenie notki o danym id) oraz save (zapis):

  1. < ?php
  2.  
  3. class Notes_NotesModel extends ProjectBaseModel
  4. {
  5.   public $dbLayer = null;
  6.  
  7.         public function initialize(AgaviContext $context, array $parameters = array())
  8.         {
  9.                 parent::initialize($context, $parameters);           
  10.                 $this -> dbLayer = $this -> getContext() -> getDatabaseConnection();
  11.                 $this -> dbLayer -> exec("SET NAMES utf8");
  12.         }
  13.        
  14.         public function getById($id)
  15.         {
  16.                 $stmt = $this -> dbLayer -> prepare("SELECT * FROM notes WHERE id = :id");
  17.                 $stmt -> bindParam("id", $id, PDO::PARAM_INT);
  18.                 $stmt -> setFetchMode(PDO::FETCH_OBJ);
  19.                 $stmt -> execute();
  20.        
  21.                 return $stmt -> fetch();
  22.         }
  23.        
  24.         public function getRecent()
  25.         {
  26.                 $stmt = $this -> dbLayer -> prepare("SELECT * FROM notes ORDER BY date DESC LIMIT 10");
  27.                 $stmt -> setFetchMode(PDO::FETCH_OBJ);
  28.                 $stmt -> execute();
  29.        
  30.                 return $stmt -> fetchAll();
  31.         }
  32.        
  33.         public function save(array $data)
  34.         {
  35.                 $stmt = $this -> dbLayer -> prepare("INSERT INTO notes VALUES(NULL, :title, :intro, :text, NOW(), :author)");
  36.                 $stmt -> execute($data);
  37.         }
  38. }
  39.  
  40. ?>

Tak wygląda nasz model. Myślę że zbytnio nie ma co się rozpisywać, są to podstawowe operacje na baze danych używając PDO.

Akcje

Wygenerowane akcje mają obszerny komentarza. Warto go przeczytać i zapamiętać wskazówki w nim zawarte. Oprócz komentarza (po przeczytaniu możemy go wywalić ;) mamy jedną metodę: getDefaultViewName, zwraca ona domyślny widok który ma być wykonany jeżeli nie wskazano innego.

Najważniejszymi metodami akcji są metody executeRead oraz executeWrite. Są one wykonywane odpowiednio gdy wysłane są dane metodą GET i POST. Można zaimplementować jedną metodę - execute - w której ręcznie sprawdzali byśmy sobie skąd otrzymujemy dane. Nie jest to zalecane rozwiązanie jednak może zaistnieć sytuacja kiedy będziemy musieli z tej możliwości skorzystać. Wszystkie z tych 3 metod muszą zwrócić nazwę widoku który ma być wykonany. W przeciwnym wypadku nie zostanie wykonany żaden widok.

Jeżeli chodzi o naszą małą aplikację, a właściwie nasz jeden mały moduł to interesuje nas jedynie akcja Add. Inne akcje nie będą posiadały żadnej logiki. Będą jedynie zwracać widok, ten natomiast będzie prezentował dane pobrane od modelu.

Akcja Add domyślnie będzie uruchamiać widok Input, który będzie zawierał formularz. Jeżeli jednak zostaną przesłane jakieś dane metodą POST to zostanie wywołana metoda executeWrite.

Oto jak będzie wyglądać nasza akcja:

  1. < ?php
  2.  
  3. class Notes_AddAction extends ProjectBaseAction
  4. {
  5.         public function executeWrite(AgaviRequestDataHolder $rd)
  6.         {
  7.                 $model = $this -> getContext() -> getModel("Notes", "Notes");
  8.                 $model -> save($rd -> getParameter("note"));
  9.                
  10.                 return ‘Success’;
  11.         }
  12.         public function getDefaultViewName()
  13.         {
  14.                 return ‘Input’;
  15.         }
  16. }
  17.  
  18. ?>

Kod bardzo prosty. Pobranie modelu, następnie wywołanie metody save, podając jako argument tablicę z danymi z formularza ($rd jest obiektem typu AgaviRequestDataHolder, są tam przechowywane dane z requestu)

Widoki

Została nam ostania warstwa aplikacji, warstwa prezentacyjna czyli widok. W tym przypadku pomijamy widoku akcji Add, ponieważ nie potrzebujemy wciągać żadnych danych. Pokażemy jedynie formularz (Input) lub wiadomość o tym że dodano wiadomość (Success). Tak będą natomiast będą wyglądać widoki:

IndexSuccessView

  1. < ?php
  2.  
  3. class Notes_IndexSuccessView extends ProjectBaseView
  4. {
  5.         public function executeHtml(AgaviRequestDataHolder $rd)
  6.         {
  7.                 parent::setupHtml($rd);
  8.                
  9.                 $model = $this -> getContext() -> getModel("Notes", "Notes");
  10.                 $this->setAttribute("notes", $model -> getRecent());
  11.         }
  12. }
  13.  
  14. ?>

ShowOneSuccessView

  1. < ?php
  2.  
  3. class Notes_ShowOneSuccessView extends ProjectBaseView
  4. {
  5.         public function executeHtml(AgaviRequestDataHolder $rd)
  6.         {
  7.                 parent::setupHtml($rd);
  8.  
  9.                 $model = $this -> getContext() -> getModel("Notes", "Notes");      
  10.                 $this->setAttribute("note", $model -> getById($rd -> getParameter("id")));
  11.         }
  12. }
  13.  
  14. ?>

Są one prawie takie same. Różnią się one jedynie metodą modelu którą wykonujemy. W widoku ShowOne pobieramy z modelu jedną notkę o id którego wartość bierzemy z requestu, tam natomiast trafiło z routera, czyli z adresu url :)

Pozostało nam już tylko napisanie szablonów. Zacznijmy od głównego szablonu, czyli Master.php który musimy utworzyć w app/templates:

app/templates/Master.php

  1. < !DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
  2. <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="pl">
  3.  
  4.  <title>Notki</title>
  5.  
  6.  <meta http-equiv="content-type" content="text/html; charset=utf-8" />
  7.  <meta http-equiv="content-language" content="pl" />
  8.  
  9.  <base href="<?php echo $ro->getBaseHref(); ?>" />
  10.  
  11. </base></head>
  12. <body>
  13.         < ?php echo $inner; ?>
  14. </body>
  15. </html>

Ustawiając Master.php jako dekorator, wszystko co zwróci widok dane akcji (html), będzie później dostępne poprzez zmienną $inner. Szkielet strony wtedy trzymamy w Master.php a w miejscu, gdzie treść będzie sie zmieniać wyświetlamy zawartość $inner.

IndexSuccess.php

  1.         <h1>Notki</h1>
  2.         <p><a href="<?php echo $ro -> gen("note_add"); ?>">Dodaj</a></p>
  3.                 <ul>
  4.                 < ?php foreach($template['notes'] as $note){ ?>
  5.                         <li>
  6.                                 <div><a href="<?php $ro -> gen("note_one", array("id" => $note -> id)); ?>">< ?php echo $note -> title; ?></a></div>
  7.                                 <p>< ?php echo $note -> intro; ?></p></li></ul></div>
  8.                        
  9.                 < ?php } ?>

ShowOneSuccess.php

  1. < ?php $note = $template[‘note’]; ?>
  2.         <h2>< ?php echo $note -> title; ?></h2>
  3.         <div>   
  4.                 <p>< ?php echo $note -> intro; ?></p>
  5.                 <p>< ?php echo $note -> text; ?></p>
  6.                 <p>< ?php echo $note -> author; ?> / < ?php echo $note -> date; ?></p>   
  7.         </div>

AddInput.php

  1. <h2>Dodaj notkę</h2>
  2.         <form action="<?php echo $ro -> gen("note_add"); ?>" method="post">
  3.           <div>
  4.                 <label for="title">Tytuł</label>
  5.                 <input type="text" name="note[title]" id="title" />
  6.           </div>
  7.           
  8.           <div>
  9.                 <label for="intro">Wstęp</label>
  10.                 <textarea name="note[intro]" id="intro"></textarea>
  11.           </div>
  12.           
  13.           <div>
  14.                 <label for="text">Treść</label>
  15.                 <textarea name="note[text]" id="text"></textarea>
  16.           </div>
  17.           
  18.           <div>
  19.                 <label for="author">Autor</label>
  20.                 <input type="text" name="note[author]" id="author" />
  21.           </div>
  22.           
  23.           <div>
  24.                 <input type="submit" value="Dodaj" />
  25.           </div>
  26.         </form>

AddSuccess.php

  1. <p>Dodano notkę!</p>

Nad szablonami myślę że nie ma za bardzo co się rozwodzić. Zwykły HTML, wyświetlający dane otrzymane z widoku.

Zakończenie

W kilku szybkich krokach, nie zagłębiając się w szczegóły, stworzyliśmy bardzo prostą aplikację. Coś w stylu “Jak napisać bloga w 15 minut” używając mojego kochanego frameworka ;) Notka ta miała na celu bardziej zachęcić do zainteresowania się Agavi, niż szczegółowo opisać cały framework. Wszystko w swoim czasie ;)

Polecam przejrzeć jeszcze raz kod aplikacji (teoretycznie powinna ona działać, po uruchomieniu ;) w aby lepiej zrozumieć zasadę działania. Warto również zajrzeć do przykładowej aplikacji znajdującej się w paczce Agavi w folderze samples.

Komentarze

  1. LBO (#) 05.16.2008 22:54

    Bardzo ładnie @strzałku, tylko, czy od pewnego czasu nie przyjęło się, że w layerach używa się $inner zamiast slotu?

    I kosmetyka: “<base href=”getBaseHref(); ?>” />”

    Pozdrawiam, Alan.

  2. LBO (#) 05.16.2008 22:57

    Hmmmm, blog uciął znacznik zamykający “base”…

  3. splatch (#) 05.17.2008 10:25

    Bardzo się cieszę, że poruszyłeś temat Agavi. Jest to framework który prezentuje nieco inne podejście niż istniejące rozwiązania, w szczególności Zend Framework czy Symfony. Z jednej strony wiele wyniósł z Mojavi, z drugiej developerzy dodali gro możliwości, które sprawiają że praca jest rzeczywiście przyjemnością. Wystarczy wspomnieć konfigurację z użyciem XLink, wsparcie dla SOAP, rozbudowane mapowanie adresów (bije na głowę każde, które znam) i na końcu - najlepsze - Form Population Filter. :)

    Artykuł bardzo fajny. Tak trzymaj!

    Pozdrawiam,
    Łukasz

  4. Strzałek (#) 05.17.2008 12:13

    Z tym kolorowanie kodu i w ogóle wklejaniem kodu w notce i komentarzach coś jest nie tak. Jak będę miał chwile powalczę z tym.

    LBO: Racja. Nie wiedziałem że tak można, a jest dostępne to już od jakiegoś czasu (ticket #655). Zmieniłem w notce. Nadal jednak można odwoływać się “po staremu” czyli używając slotów.

  5. Whisller (#) 06.14.2008 14:50

    Bardzo fajny artykuł. Szkoda że już od bardzo dawna nie miałem czasu na programowanie z użyciem tego frameworka.

    Mam nadzieję że to nie ostatni artykuł na temat Agavi :)

  6. LBO (#) 06.26.2008 10:20

    Hej strzałku przyjrzałem się jeszcze raz i czy uważasz, że pobieranie danych w widoku to dobry pomysł?
    Co zrobisz w przypadku błędu w modelu?
    Wiem, że to tylko taka pokazówka, ale dobrze uczyć czytelników dobrych nawyków od samego poczatku.

    dodatkowo: php specjalnie do zastosowania takiego jak w widoku udostepnia dodatkową notację bez klamerek.

    To samo tyczy sie innych konstrukcji np. “if :”, “endif” - kod jest czytelniejszy.

  7. LBO (#) 06.26.2008 10:22

    Cholera zjadło mi kod php.

    Pisałem dokładnie o tym: http://php.net.pl/manual/pl/control-structures.alternative-syntax.php

Dodaj komentarz

wymagane
wymagane (nie będzie publikowane)