JustPaste.it

Zend Framework – rozszerzona architektura aplikacji

Artykuł przedstawia propozycję rozszerzonej architektury aplikacji opartej na Zend Framework. Skierowany jest do developerów budujących średnie i duże serwisy internetowe.

Artykuł przedstawia propozycję rozszerzonej architektury aplikacji opartej na Zend Framework. Skierowany jest do developerów budujących średnie i duże serwisy internetowe.

 

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 Effecta24_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.

 

Źródło: http://blog.modernweb.pl/2011/02/zend-framework-rozszerzona-architektura-aplikacji/

Licencja: Creative Commons