Zend Framework i Twig - integracja

by Mateusz Tymek — on Zend Framework, PHP

Head's up! This post was written back in 2011 and is very likely to contain outdated information.

Wtyczka do NetBeans kolorująca składnię plików .twig

PHPpowe systemy szablonów są teraz na topie: są szybsze i znacznie potężniejsze od swoich pierwowzorów sprzed kilku lat. Może już czas odejść od widoków tworzonych w czystym PHP?

Nie chcę wylewać własnych przemyśleń na temat wad i zalet różnych "szablonowych" rozwiązań. Przedstawiam gotowy przepis, jak zmusić Zenda do współpracy z systemem Twig. Dlaczego Twig? Opiekunem projektu jest Fabien Potencier - współautor Symfony, postać, której w światku PHP nie trzeba nikomu przedstawiać. Nazwisko to gwarantuje otrzymanie przemyślanego, wygodnego w użyciu i dobrze przetestowanego kodu.

Mechanizm rozwiązania

Integracja musi być przeprowadzona na tyle sprawnie, aby nie trzeba było zmieniać sposobu przekazywania zmiennych z kontrolera do widoku. W akcjach wszystko zostaje jak do tej pory:

public function indexAction()
{
    $this->view->userName = 'Mateusz';
    $this->view->books = array(
        array('title' => 'Fiasko', 'author' => 'Stanisław Lem'),
        array('title' => 'Wehikuł czasu', 'author' => 'Herbert George Wells'),
        array('title' => 'Nowy wspaniały świat', 'author' => 'Aldous Huxley'),
    );
}

Zmieni się tylko widok, od teraz będzie wyglądał tak:

Witaj, {{ userName }}. Oto lista twoich książek:
<ul>
{% for book in books %}
    <li>
        <em>{{ book.author }}</em> {{ book.title }}
    </li>
    {% endfor %}
</ul>

Od strony programistycznej moja propozycja polega na utworzeniu klasy potomnej od Zend_View_Abstract i nadpisaniu metody render(), tak aby zamiast dołączać widoki przy pomocy include (oto i cała magia stojąca za Zend_View), parsowała je przy pomocy Twiga. Rozwiązując ten pozornie trywialny problem, napotykamy kilka przeszkód: jak zmusić zendowe MVC do korzystania z własnej implementacji Zend_View? co zrobić z layoutem strony (dotąd obsługiwanym przez Zend_Layout)? I wreszcie, co z helperami typu baseUrl()?

Ładowarka szablonów

Pierwszym krokiem będzie napisanie ładowarki szablonów. Ładowarka jest klasą (implementacją Twig_LoaderInterface), odpowiedzialną za wczytywanie szablonów (z pliku, bazy danych..). Musi zawierać przynajmniej trzy metody:

  • getSource($name) - wczytuje i zwraca szablon
  • getCacheKey($name) - zwraca klucz pod jakim szablon będzie zapisany w cache
  • isFresh($name, $time) - zwraca informację czy dany szablon nie jest przeterminowany (czy istnieje potrzeba ponownej kompilacji)

Oto moja ładowarka

class Silique_View_TwigLoader implements Twig_LoaderInterface
{
    protected $_view;

    public function __construct(Silique_View $view)
    {
        $this->_view = $view;
    }

    public function getCacheKey($name)
    {
        return $name;
    }

    public function getSource($name)
    {
        $name = $this->_view->script($name);
        return file_get_contents($name);
    }

    public function isFresh($name, $time)
    {
        return filemtime($this->_view->script($name)) < $time;
    }
}

Metoda getSource() musi znać lokalizację szablonu na dysku. Zend_View potrafi szukać szablonów w kilku lokalizacjach (np application/views/scripts i application/layouts/scripts). Wykorzystuje do tego chronioną metodę _script(), którą moja klasa widoku (Silique_View) odsłoni poprzez funkcję script (bez podkreślnika na początku nazwy).

Metoda isFresh() po prostu porównuje czas modyfikacji pliku z czasem kompilacji szablonu.

Implementacja widoku

Ładowarka gotowa, piszemy klasę widoku:

class Silique_View extends Zend_View_Abstract
{

    protected $_twig;

    public function getTwig()
    {
        if (null === $this->_twig) {
            $loader = new Silique_View_TwigLoader($this);
            $twig = new Twig_Environment($loader, array(
                'cache' => APPLICATION_PATH . '/../data/cache',
                'auto_reload' => true
            ));
            $this->_twig = $twig;
        }
        return $this->_twig;
    }

    protected function _run()
    {
    }

    public function render($name)
    {
        $twig = $this->getTwig();
        $template = $twig->loadTemplate($name);
        return $template->render($this->getVars());
    }

    public function script($name)
    {
        return $this->_script($name);
    }

}

Metoda getTwig() tworzy obiekt klasy Twig_Environment, wstrzykuje ładowarkę, oraz wskazuje lokalizację skompilowanych szablonów (katalog data/cache). Parametr auto_reload ustawiony na true spowoduje sprawdzanie świeżości szablonów - można go pominąć w środowisku produkcyjnym.

Bootstrap

Pozostaje zmuszenie stosu MVC do współpracy z Twig-iem. Dopisujemy dwa wpisy do application.ini:

autoloaderNamespaces[] = Silique
autoloaderNamespaces[] = Twig

Następnie dodajemy metodę _initTwig() do Boostrapera:

protected function _initTwig()
{
    $twigView = new Silique_View();
    $viewRenderer = Zend_Controller_Action_HelperBroker::getStaticHelper("ViewRenderer");

    $viewRenderer->setView($twigView)->setViewSuffix('twig');
    $twigView->addScriptPath(APPLICATION_PATH . '/layouts/scripts');

    return $twigView;
}

Te kilka linijek wystarczy aby ZF rozpoznawał pliki z końcówką .twig i kompilował je przy pomocy klasy Silique_View. Ósma linia nakazuje szukać skryptów widoku w odpowiednim katalogu, co zastąpi funkcjonalność Zend_Layout.

Helpery widoku

Ostatnim problemem są Zendowe helpery widoku. Niektóre z nich (np partiale) można zastąpić funkcjonalnością Twiga, inne (np. baseUrl) są specyficzne dla Zenda i trzeba zatroszczyć sie o nie w inny sposób. Napisałem klasę w której zebrałem kilka z nich, w postaci metod statycznych:

class Silique_View_HelperSet
{
    protected static $_view;

    public static function setView(Silique_View $view)
    {
        self::$_view = $view;
    }

    public static function baseUrl($arg = null)
    {
        return Zend_Controller_Front::getInstance()->getBaseUrl() . $arg;
    }

    public static function url(array $urlOptions = array(), $name = null, $reset = false, $encode = true)
    {
        $router = Zend_Controller_Front::getInstance()->getRouter();
        return $router->assemble($urlOptions, $name, $reset, $encode);
    }

    public function navigation(Zend_Navigation_Container $container = null)
    {
        return self::$_view->navigation($container);
    }
}

Zanim będzie można ich użyć, trzeba poinformować Twiga o ich istnieniu. Wracamy do metody getTwig() w klasie Silique_View:

public function getTwig()
{
    if (null === $this->_twig) {
        $loader = new Silique_View_TwigLoader($this);
        $twig = new Twig_Environment($loader, array(
            'cache' => APPLICATION_PATH . '/../data/cache',
            'auto_reload' => true
        ));

        Silique_View_HelperSet::setView($this);

        $twig->addFunction('baseUrl', new Twig_Function_Function('Silique_View_HelperSet::baseUrl'));
        $twig->addFunction('url', new Twig_Function_Function('Silique_View_HelperSet::url'));
        $twig->addFunction('navigation', new Twig_Function_Function('Silique_View_HelperSet::navigation'));

        $this->_twig = $twig;
    }
    return $this->_twig;
}

Podsumowanie

Oto kilkadziesiąt linijek kodu które wystarczą aby zintegrować Twiga z dowolną aplikacją w Zendzie. Od kilku dni ekspertymentuję z tym rozwiązaniem i muszę przyznać że jestem bardzo zadowolony. Szablony wyglądają lepiej niż te pisane w czystym PHP, a sama składnia jest bardzo prosta. Znalazłem też wtyczkę do NetBeans, która koloruje składnię w plikach z rozszeżeniem .twig.

Tradycyjnie do wpisu dołączam przykładową aplikację. Aby zadziałała, trzeba umieścic lub podlinkować Zend-a do katalogu library.

Pobierz przykład do artykułu Pobierz przykład.


comments powered by Disqus