Budowa średnich i dużych aplikacji wymaga przemyślanej architektury. Opierając się na Zend Framework‚u możemy skorzystać z zestawu gotowych rozwiązań. Poza oczywistym wzorcem architektonicznym MVC mamy zalecaną przez ZF modułową strukturę projektu oraz komponent Zend_Application, który rozwiązuje kilka podstawowych problemów konstrukcji i uruchamiania systemu o budowie komponentowej. Całość posiada jednak jedno, poważne ograniczenie.
Każda witryna będąca czymś więcej niż tzw. landing page poza prezentacją treści często musi mieć możliwość jej zarządzania. To wymusza na nas przygotowanie „miejsca” w którym umieścimy funkcjonalności pozwalające na administrację stroną. Najczęściej stosowaną praktyką, którą można znaleźć w sieci, jest tworzenie modułu „admin”. Takie podejście jest akceptowalne do pewnego poziomu złożoności całego systemu. Budując serwis składający się z wielu modułów umieszczanie całości panelu administracyjnego w jednym miejscu szybko okaże się mało przejrzyste a na dłuższą metę bardzo trudne w utrzymaniu.
Jeśli do tego przyjdzie nam dostarczyć część funkcjonalności via Web Service i/lub przygotować wersję mobilną naszego serwisu – mamy nie lada orzech do zgryzienia.
Artykuł ten przedstawia propozycję rozwiązania powyższego problemu. Skierowany jest przede wszystkim do developerów dobrze znających Zend Framework oraz budujących, jak wspomniałem na początku, średnie i duże aplikacje webowe.
Przedstawione rozwiązanie zostało przygotowane i testowane na ZF w wersji 1.11.3. Powinno działać bez większych problemów na wersjach >= 1.8.0 (w tej wersji wprowadzono Zend_Application
). Paczkę z gotowym rozwiązaniem (na licencji BSD) można pobrać tutaj.
Na początek skonkretyzujmy wymagania, które powinna spełniać oczekiwana architektura systemu:
- zachowanie budowy modułowej (hermetyzacja funkcjonalności)
- wprowadzenie podziału systemu na dwie aplikacje (część prezentacyjna i administracyjna)
- współdzielenie modelu pomiędzy aplikacjami (idea „Skinny Controller, Fat Model„)
- możliwość dodania kolejnej aplikacji – np. wersja mobilna, web service (np. SOAP), wersja flash (via AMF)
Dla uproszczenia nasz system będzie posiadał dwie aplikacje (dodanie kolejnych aplikacji nie powinno stanowić problemu, może jednak negatywnie wpłynąć na czytelność tego artykułu):
- website – witryna internetowa
- admin – zaplecze administracyjne
Zacznijmy od rozszerzenia komponentu Zend_Application w taki sposób aby potrafił skonfigurować i uruchomić aplikację o określonej nazwie:
class Modern_Application extends Zend_Application { /** * Nazwa uruchomionej aplikacji. * * @var string */ protected $_name; /** * Przeciążony konstruktor pozwala na podanie nazwy uruchamianej aplikacji. * * @param string $environment * @param string|array|Zend_Config $options * @param string $name * @throws Zend_Application_Exception */ public function __construct($environment, $options = null, $name = null) { if(null !== $name) { $this->setName($name); } parent::__construct($environment, $options); } /** * Ustawia nazwę aplikacji. * * @param string $name * @return Modern_Application */ public function setName($name) { $this->_name = $name; return $this; } /** * Zwraca nazwę aplikacji. * * @return string */ public function getName() { return $this->_name; } /** * Ustawia konfigurację aplikacji. * * Nadpisuje metodę rodzica w celu obsłużenia konfiguracji specyficznej * dla bieżącej aplikacji - jeśli została ustawiona. * * @param array $options * @return Zend_Application */ public function setOptions(array $options) { if (!empty($options['config'])) { $options = $this->mergeOptions($options, $this->_loadConfig($options['config'])); unset($options['config']); } if( null !== $this->_name && !isset($options['applications']) && !is_array($options['applications']) ) { require_once 'Zend/Application/Exception.php'; throw new Zend_Application_Exception("Nie określono listy dostępnych aplikacji"); } // łączenie konfiguracji specyficznej dla aplikacji foreach($options as $key => &$config) { if(null !== $this->_name && isset($config[$this->_name])) { $config = $this->mergeOptions($config, $config[$this->_name]); } } // usuwanie opcji specyficznych aplikacji foreach($options['applications'] as $application) { foreach ($options as $key => &$config) { if(isset($config[$application])) { unset($config[$application]); } } } return parent::setOptions($options); } }
Przeciążeniu uległ konstruktor klasy umożliwiając podanie nazwy uruchamianej aplikacji. Dodatkowo przesłonięta została metoda setOptions()
, która w sytuacji określenia nazwy aplikacji, wyszukuje i uwzględnia specyficzną dla niej konfigurację. Ponieważ Zend_Application
automatycznie interpretuje każdą opcję konfiguracyjną poszukując odpowiednich zasobów (Zend_Application_Resource_*
) musimy zdefiniować listę wszystkich aplikacji naszego systemu i usunąć z konfiguracji odpowiadające im ustawienia. Zanim przejdziemy do konfigurowania poszczególnych aplikacji rozbudujmy główny bootstrap /public/index.php
$application = new Modern_Application( ENVIRONMENT, ROOT_PATH . "/configs/application.ini", 'website' ); $application->bootstrap()->run();
oraz stwórzmy dodatkowy dla drugiej aplikacji /public/admin.php
:
$application = new Modern_Application( ENVIRONMENT, ROOT_PATH . "/configs/application.ini", 'admin' ); $application->bootstrap()->run();
Teraz możemy dodać mechanizm, który skieruje określone żądania na właściwy bootstrap. Do tego celu użyjemy reguł mod_rewrite serwera Apache. Mamy do dyspozycji dwie opcje zależne od konfiguracji naszego serwera:
- podkatalog, np: http://www.example.com/admin/
- subdomena, np: http://admin.example.com
Użycie subdomeny jest prostsze z perspektywy pełnej implementacji naszego systemu, jednak w niektórych środowiskach może być problematyczne. Wymaga ustawienia opcji wildcard serwera DNS, który utrzymuje domenę oraz dodania aliasu w konfiguracji VHOST’a. Dodatkowo, jeśli chcemy mieć szyfrowane połączenie w panelu administracyjnym, może wiązać się z zakupem droższej (wildcard’owej) wersji certyfikatu SSL.
Zakładam, że większość z Was wybierze opcję z podkatalogiem
W katalogu głównym naszego projektu umieszczamy plik .htaccess
o następującej treści:
RewriteEngine On RewriteBase / # aplikacja admin RewriteCond %{REQUEST_URI} ^/admin [NC] RewriteRule ^(.*)$ public/admin.php/$1 [L] # aplikacja website RewriteRule ^(.*)$ public/index.php/$1 [L]
Przedstawiony przykład jest odmienny względem tego co proponuje ZF w swojej dokumentacji. Nie wymaga ustawiania VHOST’a na katalog /public
projektu, co w niektórych środowiskach developerskich może być zaletą. Do poprawnego działania wymaga jedynie umieszczenia w katalogu /public
pliku .htaccess
zawierającego:
RewriteEngine On
Możemy przejść do konfiguracji naszego systemu (/configs/application.ini
):
[production] ; lista dostępnych aplikacji applications[] = website applications[] = admin phpSettings.display_startup_errors = 0 phpSettings.display_errors = 0 phpSettings.date.timezone = "Europe/Warsaw" bootstrap.path = "Modern/Application/Bootstrap.php" bootstrap.class = "Modern_Application_Bootstrap" resources.frontController.defaultmodule = index resources.frontController.prefixDefaultModule = On ; ustawienia specyficzne dla określonych aplikacji resources.website.frontController.controllerDirectory.index = ROOT_PATH "/modules/index/apps/website/controllers" resources.admin.frontController.controllerDirectory.index = ROOT_PATH "/modules/index/apps/admin/controllers" resources.admin.frontController.baseurl = "/admin" [staging : production] [testing : production] phpSettings.display_startup_errors = 1 phpSettings.display_errors = 1 [development : production] phpSettings.display_startup_errors = 1 phpSettings.display_errors = 1
Poza wymaganą listą aplikacji istniejących w naszym systemie (linie 4-5) mamy możliwość zdefiniowania opcji specyficznych dla poszczególnych aplikacji (linie 18-21). Wprowadzając podział na aplikacje musimy zdefiniować odrębne ścieżki dla kontrolerów, co wpływa na strukturę katalogu modułów:
/modules /index /apps /admin /controllers /views /website /controllers /views /configs /model
Dodatkowo decydując się na opcję panelu w „podkatalogu” musimy poinformować FrontController o bazowym adresie aplikacji admin
.
Wadą powyższego rozwiązania jest konieczność zdefiniowania ręcznie ścieżek do kontrolerów dla wszystkich modułów systemu. Rozsądniejszą opcją jest stworzenie zasobu (Modern_Application_Resource_Modules
), który zrobi to za nas. Temat jednak dotyka bardziej złożonej kwestii instalowania/ładowania modułów i kwalifikuje się na oddzielny artykuł.
Po odpowiednim przebudowaniu struktury modułu teoretycznie kończy się nasza „wycieczka”. Patrząc na zdefiniowany na początku zbiór wymagań mamy:
- zachowaną budowę modułową,
- podział na dwie aplikacje,
- miejsce na klasy tworzące spójny model modułu oraz specyficzne dla aplikacji kontrolery/widoki dostarczające funkcjonalności,
- dodanie kolejnej aplikacji wiąże się z dodaniem reguły w .htaccess, pliku bootstrap, specyficznej konfiguracji oraz klas dostarczających określonych funkcjonalności modelu.
Pozostał tylko jeden „drobiazg” – nazewnictwo klas kontrolerów. W obecnej konstrukcji będą się nazywały dokładnie tak samo dla aplikacji admin
jak i website
. Może to stanowić problem w sytuacji gdy będziemy chcieli stworzyć dokumentację na pomocą phpDocumentor’a. Pomimo tego, że znajdują się w oddzielnych katalogach jedna z nich nie zostanie prawidłowo udokumentowana.
Rozsądne jest w tej sytuacji zmodyfikowanie domyślnego schematu nazewnictwa klas kontrolerów. W tym celu potrzebujemy rozszerzyć klasę Zend_Controller_Dispatcher_Standard
.
class Modern_Controller_Dispatcher_Standard extends Zend_Controller_Dispatcher_Standard { /** * Formatuje nazwę klasy akcji z uwzględnieniem nazwy bieżącej aplikacji. * * @param string $moduleName * @param string $className * @return string */ public function formatClassName($moduleName, $className) { $applicationName = $this->getParam('bootstrap')->getApplication()->getName(); return $this->formatModuleName($moduleName) . '_' . $this->formatApplicationName($applicationName) . '_' . $className ; } /** * Formatuje nazwę aplikacji. * * @param string $unformatted * @return string */ public function formatApplicationName($unformatted) { return ucfirst($this->_formatName($unformatted)); } }
Powyższa modyfikacja wprowadza następujący schemat nazewnictwa: Module_Application_FooController
, na przykład: News_Admin_EntryController
.
Aby nasz dispatcher zastąpił domyślny musimy go zarejestrować w Zend_Controller_Front
rozszerzając zasób Zend_Application_Resource_Frontcontroller
:
class Modern_Application_Resource_Frontcontroller extends Zend_Application_Resource_Frontcontroller { /** * Inicjuje Front Controller. * * @return Zend_Controller_Front */ public function init() { foreach ($this->getOptions() as $key => $value) { switch (strtolower($key)) { case 'dispatcherclass': Zend_Loader::loadClass($value); $this->getFrontController()->setDispatcher(new $value()); break; } } return parent::init(); } }
Aby zmodyfikowany zasób oraz dispatcher zostały użyte dodajemy w konfiguracji:
pluginPaths.Modern_Application_Resource = "Modern/Application/Resource/" resources.frontController.dispatcherClass = "Modern_Controller_Dispatcher_Standard"
A na koniec zostało nam tylko pozmieniać nazwy klas kontrolerów.
Witam
Świetny artykuł!
Czy będzie więcej wpisów o ZF i budowie dużych aplikacji ?
Mnie interesuje np struktura modelu, tabel ?
Ile logiki w controlerze ? Tu spotykam się z bardzo podzielonymi zdaniami… Szczególnie od kolegi który koduje w RoR
Co jest wydajniejsze Zend_Db_Table czy Zend_Db_Adapter ?
Pozdrawiam i trzymam kciuki za nowe wpisy.
RT
Mam w planach napisać kontynuację tego artykułu i rozwinąć kwestię budowy, konfiguracji i ładowania modułów. Nie wiem jednak kiedy znajdę czas aby go napisać…
W temacie struktury modelu i logiki w kontrolerach zdecydowanie zalecam stosowanie (wspomnianej w artykule) zasady „Skinny Controller, Fat Model„. Zamykanie logiki na poziomie standardowych kontrolerów akcji ogranicza nas do podstawowych metod protokołu HTTP (GET, POST). Mamy problem jeśli okaże się, że nastąpiła zmiana planów i np. musimy wyprowadzić funkcjonalność przez serwer AMF lub jako metody webservice…
Co do wydajności Zend_Db_Table vs. Zend_Db_Adapter to raczej ciężko je porównywać. Zend_Db_Table korzysta z Zend_Db_Adapter aby wykonywać zapytania, więc z natury jest wolniejszy. Zend_Db_Table daje „namiastkę” mapowania relacyjno-obiektowego (ORM) i w wielu przypadkach jest wystarczający. Dobrym uzupełnieniem jest Zend_Db_Select, którym można realizować bardziej złożone zapytania. Umiejętne rozszerzanie klas Table, Rowset i Row jest jednym ze sposobów budowy modelu.
Pozdrawiam,
Rafał