3. Budujemy szkielet aplikacji

Poznaliśmy już wszystkie mechanizmy pozwalające modularyzować nasze szablony, możemy zatem przystąpić do zastanowienia się, jak wykorzystać je do stworzenia szkieletu dla aplikacji WWW. Załóżmy, że wykorzystujemy typowy framework z kontrolerami oraz zawartymi w nimi akcjami, przy czym w ramach jednego żądania HTTP może zostać wykonanych kilka akcji jedna po drugiej. Chcielibyśmy mieć możliwość zdefiniowania ogólnego layoutu oraz wyglądów poszczególnych akcji. Najlepiej poradzi sobie tutaj instrukcja opt:include oraz widoki. Każda akcja w momencie rozpoczęcia otrzyma swój własny widok od razu ustawiony na odpowiedni szablon. Oprócz tego, skrypt posiadać będzie menedżera layoutów, który będzie zarządzać layoutem oraz kolekcjonować widoki od wszystkich wykonanych akcji. Na końcu poskłada je w całość.

Zacznijmy od przygotowania struktury katalogowej:

/templates
   /kontroler1
       /akcja1.tpl
       /akcja2.tpl
       /akcja3.tpl
   /kontroler2
       /akcja1.tpl
       /akcja2.tpl
       /akcja3.tpl
   /snippets.tpl
   /layout.tpl
   /baseLayout.tpl
   /error.tpl

Każdy kontroler otrzyma swój własny katalog, w którym umieszczone będą szablony dla jego akcji. Bezpośrednio w /templates umieścimy dostępne layouty, plik z często wykorzystywanymi snippetami oraz szablon dla błędów aplikacji.

Na stronie może wyświetlać się kilka widoków pochodzących od różnych akcji, lecz nie muszą one wszystkie wyświetlać się w jednym miejscu, przeznaczonym na treść. Nasz szkielet powinien uwzględniać, że akcja X chciałaby wyświetlić się pod menu, podczas gdy wszystkie pozostałe w polu treści. Rozwiązaniem jest stworzenie kilku miejsc na widoki w layoucie. Każde takie miejsce reprezentowane będzie przez sekcję, dzięki czemu będzie mógł pojawić się w nim więcej niż jeden widok:

<?xml version="1.0"?>
<opt:root>
<opt:prolog />
<opt:dtd template="html5" />
<html>
    <head>
        <meta http-equiv="content-type" content="text/html; charset=utf-8" />
        <title>{$title}</title>
    </head>
    <body>
        <div id="header">
            Domyślny nagłówek.
        </div>
 
        <div id="menu">
            <!-- miejsce pierwsze -->
            <opt:section name="menu">
                <opt:include from="menu" />
            </opt:section>
        </div>
 
        <div id="content">
            <!-- miejsce drugie -->
            <opt:section name="content">
                <opt:include from="content" />
            </opt:section>
        </div>
 
        <div id="footer">
            <p>© Moja Firma 2010</p>
        </div>
    </body>
</html>
</opt:root>

Konstrukcja opt:include from to zwykłe uproszczenie składni równoważne zapisowi <opt:include view="$menu.view">. Zauważmy, że nic nie stoi na przeszkodzie, aby do konstruowania layoutów wykorzystać dziedziczenie szablonów, tworząc różne warianty oparte wokół wspólnego szkieletu.

W naszym szablonie zdefiniowane są dwa miejsca: menu oraz content. Zarządzaniem ich zawartością zajmie się menedżer layoutów, który niebawem napiszemy. Jeśli chodzi o szablony akcji, to są to zwyczajne szablony, które wypełniamy według własnego uznania. Każda akcja posiadać będzie własny widok, zatem nie musimy martwić się o przypadkową kolizję nazw zmiennych.

To wszystko, jeśli chodzi o szablony. Pozostała część naszej pracy odbywać się będzie już po stronie skryptu. Nie będziemy pisać tutaj kompletnego frameworka, lecz założymy, że pewne jego elementy są już gotowe. Dostosowanie szkieletu do potrzeb Twojego skryptu pozostawiamy już Tobie. Zaczniemy od napisania menedżera layoutów. Będzie to klasa, która zgromadzi widoki wszystkich akcji, poukłada je w odpowiednich miejscach, a następnie wyrenderuje layout.

<?php
/**
 * Klasa menedżera layoutów.
 * @author Tomasz Jędrzejewski
 */
class LayoutManager
{
    /**
     * Lista miejsc wraz z ich zawartością.
     * @var array
     */
    private $_places = array();
    /**
     * Widok layoutu
     * @var Opt_View
     */
    private $_layoutView = null;
    /**
     * Nazwa layoutu
     * @var string
     */
    private $_layout = 'layout';
 
    /**
     * Dodaje widok do podanego miejsca.
     *
     * @param string $place Nazwa miejsca
     * @param Opt_View $view Widok przypisywany do danego miejsca
     */
    public function appendView($place, Opt_View $view)
    {
        if(!isset($this->_places[(string)$place]))
        {
            $this->_places[(string)$place] = array();
        }
        $this->_places[(string)$place][] = array('view' => $view, 'template' => $view->getTemplate());
        $view->appended = true;
    } // end appendView();
 
    /**
     * Ustawia nazwe layoutu do uzycia.
     *
     * @param string $layoutName Nazwa layoutu
     */
    public function setLayout($layoutName)
    {
        $this->_layout = (string)$layoutName;
        if($this->_layoutView !== null)
        {
            $this->_layoutView->setTemplate((string)$layoutName.'.tpl');
        }
    } // end setLayout();
 
    /**
     * Zwraca nazwe uzywanego layoutu.
     *
     * @return string
     */
    public function getLayout()
    {
        return $this->_layout;
    } // end getLayout();
 
    /**
     * Zwraca widok layoutu, w razie koniecznosci
     * tworzac go.
     *
     * @return Opt_View
     */
    public function getLayoutView()
    {
        if($this->_layoutView !== null)
        {
            $this->_layoutView = new Opt_View($this->_layout.'.tpl');
        }
        return $this->_layoutView;
    } // end getLayoutView();
 
    /**
     * Renderuje layout i widoki.
     */
    public function render()
    {
        $layout = $this->getLayoutView();
 
        // Przygotuj miejsca i przydziel widoki do layoutu
        foreach($this->_places as $name => $data)
        {
            $layout->assign($name, $data);
        }
 
        // Renderuj
        $output = new Opt_Output_Http;
        $output->setContentType(Opt_Output_Http::XHTML, 'utf-8');
        $output->render($layout);
    } // end render();
} // end LayoutManager;

Przy pomocy metod setLayout() oraz getLayout() możemy wybierać layout, jaki będzie używany przez naszą aplikację. Ponadto, aby skonfigurować sam widok layoutu (np. aby ustawić tytuł strony), mamy do dyspozycji metodę getLayoutView() zwracającą tenże widok. Przypisywanie widoku akcji realizujemy poprzez appendView(), gdzie dodatkowo określamy nazwę miejsca, w którym ma się on znaleźć. Menedżer automatycznie opakuje go w odpowiednią strukturę oraz doda zmienną appended, aby zapamiętać, że ten widok jest już przypisany. Zaraz dowiemy się, do czego się nam ona przyda.

Musimy teraz napisać prosty kontroler, który ułatwi nam życie poprzez automatyczne tworzenie widoków dla wykonywanej akcji.

/**
 * Kontroler bazowy.
 */
class Controller
{
    /**
     * Widok akcji.
     * @var Opt_View
     */
    public $view;
 
    /**
     * Nazwa kontrolera.
     * @var string
     */
    public $controllerName;
 
    /**
     * Wykonuje akcje o podanej nazwie.
     *
     * @throws Exception
     * @param string $name Nazwa akcji do wykonania
     */
    public function execute($name)
    {
        if(!method_exists($this, $name.'Action'))
        {
            throw new Exception('Podana akcja '.$name.' nie istnieje.');
        }
        // Tworzymy widok dla akcji.
        $this->view = new Opt_View($this->controllerName.'/'.$name.'.tpl');
 
        // Wykonajmy akcje.
        $name = $name.'Action';
        $this->$name();
 
        // Jesli akcja nie przypisala sobie widoku gdzies we wlasnym zakresie,
        // przypiszmy go do miejsca domyslnego.
        if(!$view->appended)
        {
            $layout = Opl_Registry::get('layout');
            $layout->appendView('content', $this->view);
        }
    } // end execute();
} // end Controller;

Kontroler powinien podpiąć widok akcji do menedżera jedynie wtedy, gdy nie zrobiła tego akcja we własnym zakresie. Tutaj właśnie przydaje nam się atrybut appended. Jeśli jest on ustawiony, oznacza to, że widok trafił już do menedżera layoutów i nie trzeba go ponownie podpinać. Na koniec musimy jeszcze poustawiać wszystko w pliku startowym:

// Tutaj konfigurujemy OPT
 
// Stworzmy menedzera layoutow
$layout = new LayoutManager;
Opl_Registry::register('layout', $layout);
 
// Odpalmy wszystko
$dispatcher->dispatch();
 
// Wyswietlanie
$layout->render();

To wszystko. Przetestujmy teraz nasz szkielet aplikacji oparty na OPT, tworząc jakiś przykładowy kontroler z akcją:

class IndexController
{
    public function indexAction()
    {
        $this->view->zmienna = 'Foo';
    } // end indexAction();
 
    public function customAction()
    {
        $this->view->dane = 'Jakieś dane';
 
        $layout = Opl_Registry::get('layout');
        $layout->appendView('menu', $this->view);
    } // end customAction();
} // end IndexController;

Podsumowując, w naszej aplikacji tworzymy tzw. menedżera layoutów. Ustawiamy w nim szablon layoutu definiującego szkielet strony. W szkielecie stworzyliśmy specjalne miejsca, gdzie rozmaite akcje mogą coś wyświetlać. Dla każdej wykonywanej akcji automatycznie tworzony jest widok, który wiązany jest z domyślnym szablonem nazwakontrolera/nazwaakcji.tpl. W trakcie wykonywania, akcja może przypisywać dane do swojego widoku, które chciałaby wyświetlić. Po zakończeniu działania widok trafia do menedżera szablonów, gdzie jest podpinany do jednego z dostępnych miejsc. Kiedy wszystkie akcje zostaną już wykonane, menedżer layoutów bierze widok layoutu, umieszcza w nim widoki akcji i renderuje go przy pomocy systemu wyjścia HTTP, a my odbieramy w przeglądarce gotowy kod HTML.