2. Techniki modularyzacji

Pojedynczy szablon praktycznie nigdy nie zawiera pełnego kodu HTML. Zamiast tego, generowany jest on z kilku mniejszych szablonów zawierających fragmenty kodu: szkielet, menu, treść. Takie rozbicie będziemy nazywać dalej modularyzacją, gdyż najczęściej każdy moduł dostaje swój własny szablon. Moduł to pewien umowny element Twojego skryptu, któremu powinieneś nadać własne znaczenie. Może to być akcja kontrolera, jakaś wtyczka lub dowolna inna rzecz, której potrzebujesz i która powinna mieć swój własny szablon.

Najprostszą techniką modularyzacji jest wzięcie nożyczek i przecięcie kodu szablonu w kilku miejscach, tworząc pliki w stylu naglowek.tpl, stopka.tpl, a następnie parsowanie ich w odpowiedniej kolejności. W Open Power Template nie można jej jednak stosować, a biblioteka będzie się aktywnie przed nią bronić. Są dwa powody, dla których zdecydowano się to uniemożliwić. Pierwszy to sama budowa szablonów OP, które są dokumentami XML. Oznacza to, że nie można otworzyć znacznika <body> w jednym szablonie, a zamknąć go w innym. Szkielet strony musi stanowić jedną całość i dopiero w jego wnętrzu musimy osadzić dodatkowe instrukcje mówiące, gdzie powinien pojawić się kod poszczególnych modułów. Drugim powodem wprowadzenia zakazu jest mała elegancja i elastyczność takiego sklejania. Miejsce wyświetlenia menu jest na sztywno zapisane w kodzie aplikacji, zaś same linie cięcia także często zależą od konkretnych potrzeb. Jeżeli chcielibyśmy kiedyś wprowadzić nową szatę graficzną, której twórca przewidział menu w zupełnie innym miejscu, mielibyśmy problem, gdyż do przerobienia byłaby część aplikacji, a tego przecież chcieliśmy uniknąć.

Skoro nie możemy sklejać wyjścia z mniejszych szablonów pociętych nożyczkami, biblioteka musi udostępniać jakiś inny sposób na rozwiązanie tego problemu. Tak jest w istocie - Open Power Template udostępnia dwie podstawowe techniki, które można dostosować do własnych potrzeb: dołączanie oraz dziedziczenie szablonów.

Dołączanie szablonów

Dołączanie szablonów wykorzystuje instrukcję opt:include, której przekazujemy utworzony w skrypcie widok lub nazwę szablonu. Następnie OPT ładuje określony szablon i wyświetla go we wskazanym miejscu w bardzo podobny sposób, jak to robi komenda include() z PHP. Oczywiście wszystko odbywa się dynamicznie, zatem z poziomu skryptu możemy w dowolnym momencie kontrolować, co aktualnie ma się pokazać. Poniżej prezentowany jest przykład zastosowania tego elementu:

$layoutView = new Opt_View('layout.tpl');
$layoutView->layoutData = 'Dane layoutu';
 
if($action == 'foo')
{
    $view = new Opt_View('foo.tpl');
    $view->data = 'Jakieś dane';
}
else
{
    $view = new Opt_View('bar.tpl');
    $view->data = 'Inne dane';
}
$layoutView->module = $view;
 
$output->render($layoutView);

Plik layout.tpl będzie wyglądać następująco:

<?xml version="1.0" ?>
<opt:root>
 
<p>To jest moja strona.</p>
 
<!-- dolacz widok zapisany w zmiennej $module -->
<opt:include view="$module" />
 
<p>Stopka</p>
</opt:root>

W zależności od tego, jaką akcję wybierzemy, skrypt utworzy inny widok - albo z szablonem foo.tpl, albo z bar.tpl. Przekazujemy go następnie do szablonu, a tam opt:include wczytuje go i wyświetla we wskazanym miejscu. Zwróćmy uwagę, że widok modułu posiada swoje własne zmienne, zatem w normalnej sytuacji nie będzie widzieć zmiennych z widoku layoutu. Możesz się o tym przekonać, tworząc następujący szablon foo.tpl:

<?xml version="1.0" ?>
<opt:root>
<p>Dane moje: {$data}</p>
<p>Dane layoutu: {$layoutData}</p>
</opt:root>

Pierwsza zmienna wyświetli się bez problemu, ponieważ utworzyliśmy ją w naszym widoku. Jednak $layoutData istnieje jedynie w widoku layoutu i moduł jej nie widzi. Jest to zatem podobna sytuacja, jak z funkcjami w PHP, gdzie każda z nich ma własną przestrzeń zmiennych i nie widzi ani zmiennych globalnych, ani zmiennych należących do innej funkcji.

Jednak nie zawsze widoki muszą być całkowicie izolowane od siebie. W niektórych sytuacjach chcielibyśmy móc przekazać jakieś argumenty. Jest to szczególnie ważne w sytuacji następującej:

<opt:include file="szablon.tpl" />

W tym przypadku dla jawnie określonego pliku szablonu OPT tworzy nowy, tymczasowy widok, tyle że nie będzie on zawierać żadnych zmiennych! Dodatkowe dane możemy przekazać jako atrybuty opt:include:

<opt:include file="szablon.tpl" foo="$zmienna" />

Teraz w dołączanym szablonie będziemy mogli wykorzystywać zmienną $foo, do której zostanie podstawiona wartość wyrażenia. Argumenty działają także dla wczytywania widoku (opt:include view). W końcu, możemy zaznaczyć, że chcemy współdzielić wszystkie zmienne z wczytywanym widokiem:

<opt:include file="szablon.tpl" import="yes" />

Atrybut import powoduje, że dołączany szablon może bez problemu korzystać ze wszystkich zmiennych dostępnych w widoku dołączającym. Należy pamiętać, że w lokalnym widoku otrzymujemy kopie oryginalnych zmiennych, dlatego wywołujący widok nie będzie widzieć zmian przez niego wprowadzonych.

Atrybut file domyślnie oczekuje podania ciągu tekstowego. Jeśli chcemy wczytać nazwę szablonu ze zmiennej, musimy zastosować konstrukcję <opt:include parse:file="$zmienna" />.

Ostatnia ciekawa właściwość opt:include to radzenie sobie z sytuacjami wyjątkowymi. Gdybyśmy próbowali odwołać się do nieistniejącego szablonu, OPT domyślnie wygeneruje wyjątek, który skrypt będzie musiał przechwycić. Jednak możliwa jest inna reakcja - załadowanie szablonu alternatywnego lub zdefiniowanie treści domyślnej:

<opt:include view="$widok" default="brakSzablonu.tpl" />
 
<opt:include view="$widok">
    <p>Nie można załadować podanego szablonu.</p>
</opt:include>

Snippety

Snippety są rodzajem makr. Często wykorzystywany fragment kodu możemy zapakować w snippet, a później osadzać w wielu różnych miejscach i szablonach. Gdyby zaszła potrzeba jego modyfikacji, wystarczy, że poprawimy kod snippetu, a zmiany będą natychmiast widoczne we wszystkich miejscach. Snippety przetwarzane są w czasie kompilacji, dlatego mają dwie charakterystyczne właściwości:

  1. Samoistnie dopasowują się do sytuacji panującej w miejscu wywołania. Przykładowo, jeśli w snippecie używamy zmiennej $foo i wkleimy go do sekcji o nazwie "foo", zmienna ta zacznie się odwoływać w tym miejscu do aktualnego elementu sekcji. W innym miejscu $foo będzie zwykłą zmienną. Dotyczy to także formatów danych.
  2. Nie można dynamicznie wybrać snippetu do wyświetlenia, wczytując jego nazwę ze zmiennej. Wszystko musi być znane w momencie kompilacji i później nie może być zmienione.

Spójrzmy na przykład wykorzystania snippetów. Często stosowaną techniką jest stworzenie jakiegoś pliku w stylu snippets.tpl i umieszczenie tam wszystkich często wykorzystywanych kawałków szablonu. My zamieścimy tam wygląd stronicowania:

<?xml version="1.0"?>
<opt:root>
 
 <opt:snippet name="pagination">
    <div id="pagination">
        <div class="info">Strona {$pageNumber} z {$pageCount}</div>
        <div class="list">Przejdź do strony:
            <opt:selector name="pages">
                <opt:page><a parse:href="$pages.url">{$pages.number}</a></opt:page>
                <opt:active><a parse:href="$pages.url" class="active">{$pages.number}</a></opt:active>
            </opt:selector>
        </div>
    </div> 
 </opt:snippet>
 
</opt:root>

Następnie w szablonie, w którym chcielibyśmy takie stronicowanie wykorzystać, musimy jedynie poinformować, gdzie znajdują się oryginalne źródła:

<?xml version="1.0"?>
<opt:root include="snippets.tpl">
 
<h1>Lista produktów</h1>
 
<p>Tutaj jakaś lista</p>
 
<opt:insert snippet="pagination" />
 
</opt:root>

I gotowe. Zwróćmy uwagę na sposób informowania o lokalizacji snippetu - odbywa się to poprzez argument include w instrukcji opt:root. Nie jest to ten sam include, który spotkaliśmy wcześniej. OPT wczytuje taki plik w momencie kompilacji i szuka znajdujących się w nim snippetów. Pozostała treść takiego szablonu jest całkowicie ignorowana. Możliwe jest także podmienianie treści jakiegoś znacznika zawartością snippetu:

<div opt:use="snippet">
    <p>Treść domyślna</p>
</div>

Gdyby snippet nie był zdefiniowany, OPT użyje treści domyślnej. W przeciwnym wypadku nadpisze ją zawartością snippetu.

Począwszy od wersji OPT 2.1, snippety mogą także pobierać argumenty:

<?xml version="1.0"?>
<opt:root>
 
    <opt:snippet name="mojSnippet" arg="required">
        <p>Oto argument: {$arg}</p>
    </opt:snippet>
 
    <opt:use snippet="mojSnippet" arg="$zmienna" />
</opt:root>

Zauważmy, że opt:insert został w celu ujednolicenia składni przemianowany na opt:use. Takich modyfikacji w wersji 2.1 jest kilka, ale nie trzeba się ich obawiać dzięki trybowi kompatybilności oraz konwerterowi szablonów.

Pamiętaj, że kompilator nie widzi snippetów, które są tworzone w szablonie dołączanym instrukcją opt:include.

Procedury

Nowością wprowadzaną przez Open Power Template 2.1 są procedury. Na pierwszy rzut oka są one bardzo podobne do snippetów. Zamykamy w nich kawałek kodu, określamy argumenty, nadajemy mu nazwę, a później możemy wykorzystywać wszędzie, gdzie to potrzebne. Różnica polega na tym, że procedury są przetwarzane w czasie wykonywania, co skutkuje następującymi konsekwencjami:

  1. Formaty danych i sposób działania procedury jest określony raz na trwałe. Wszystkie miejsca, w których procedura jest używana, muszą się do tego dostosować.

  2. Możemy dynamicznie wybierać procedurę do wykonania.

Poniżej przedstawiony jest przykład działania, w którym skrypt ma wpływ na to, jaka procedura będzie użyta do wyświetlenia zawartości pętli:

<?xml version="1.0"?>
<opt:root>
    <opt:procedure name="sposob1" data="required">
        <p>{@data.title}</p>
        {u:@data.body}
    </opt:procedure>
 
    <opt:procedure name="sposob2" data="required">
        <h1>{@data.title}</h1>
        {u:@data.body}
    </opt:procedure>
 
    <opt:section name="list">
        <opt:use procedure="$list.procedureName" data="$list" />
    </opt:section>
</opt:root>

Zauważmy, że w przeciwieństwie do snippetów, argumenty procedury rejestrowane są jako zmienne lokalne szablonu (poprzedzone znakiem małpy, zamiast dolara). Oprócz tego procedura ma dostęp do wszystkich zmiennych szablonu widoku, w którym została wywołana.

Jeśli nie potrzebujesz dynamicznego wywoływania, zalecamy używanie snippetów. Ich przetwarzanie odbywa się w czasie kompilacji, dlatego takie szablony wykonują się dużo szybciej.

Dziedziczenie szablonów

Dziedziczenie szablonów to najbardziej rozbudowany mechanizm modularyzacji dostępny w Open Power Template. Bazuje on w dużej mierze na snippetach, zatem także jest obliczany w czasie kompilacji (aczkolwiek jest tu użyta pewna sztuczka dodająca nieco dynamizmu). Koncepcja dziedziczenia szablonów zaczerpnięta została z programowania obiektowego. Rolę klas pełnią tutaj szablony ze snippetami w miejsce metod, które można rozszerzać. Szablon potomny może przesłonić już istniejące snippety oraz dodać własne. Jedyna różnica występuje w szablonie bazowym. Wyjątkowo nie zawiera on żadnego snippetu, lecz szkielet kodu, w którym takie snippety z szablonów potomnych można osadzać. Opis ten może brzmieć dość abstrakcyjnie, dlatego zerknijmy na przykład pokazujący, jak zbudować szkielet kodu HTML w oparciu o dziedziczenie. Zaczniemy od napisania szablonu bazowego base.tpl:

<?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 opt:use="titleSnippet">Domyślny tytuł</title>
 
        <opt:insert snippet="extraHeaders">
        <link rel="stylesheet" type="text/css" href="design/style.css" media="all"  />
        </opt:insert>
    </head>
    <body>
        <div id="header" opt:use="header">
            Domyślny nagłówek.
        </div>
 
        <div id="content" opt:use="content">
            Domyślna treść.
        </div>
 
        <div id="footer" opt:use="footer">
            <p>© Moja Firma 2010</p>
        </div>
    </body>
</html>
</opt:root>

Przy pomocy instrukcji opt:insert oraz jej atrybutowej wersji opt:use zdefiniowaliśmy zestaw miejsc, które można porozszerzać dowolną treścią zależną od naszego modułu. Jeśli szablon potomny nie zdefiniuje snippetu dla danego miejsca, OPT użyje domyślną treść z szablonu bazowego. Zobaczmy zatem, jak rozszerzyć nasz szablon (plik index.tpl):

<?xml version="1.0"?>
<opt:extend file="base.tpl">
    <opt:snippet name="title">Moja strona</opt:snippet>

    <opt:snippet name="extraHeaders">
        <opt:parent />
        <link rel="stylesheet" type="text/css" href="design/news.css" media="all"  />
    </opt:snippet>

    <opt:snippet name="content">
        <h1>Witaj na mojej stronie</h1>

        <p>Witaj na mojej stronie!</p>
    </opt:snippet>
</opt:extend>

W szablonie potomnym zamiast opt:root używamy opt:extend, zaś w atrybucie file określamy, jaki szablon pragniemy rozszerzyć. Wewnątrz opt:extend mogą znajdować się wyłącznie snippety, które następnie zostaną osadzone w odpowiednich miejscach szablonu bazowego. Zauważmy, że nadpisując jakiś snippet, wciąż mamy dostęp do oryginalnej treści dzięki znacznikowi opt:parent. Za oryginalną treść uznawana jest również domyślna zawartość miejsca, w którym snippet próbujemy osadzić, dzięki czemu możemy już w szablonie bazowym zdefiniować część plików CSS bez obawy o ich utratę po nadpisaniu. Oczywiście index.tpl również może być rozszerzony przez kolejny szablon, który zmodyfikuje część snippetów i tak dalej. Powstaje w ten sposób łańcuch plików, które po złożeniu przez kompilator wyprodukują kompletny kod HTML naszej strony.

Dziedziczenie szablonów ma jednak pewne ograniczenia. Nie ma tu podziału na widoki - wszystko wykonywane jest w ramach jednego widoku, zatem szablony współdzielą dokładnie te same zmienne. Co więcej, z racji obliczania wszystkiego w czasie kompilacji, dynamiczny wybór szablonu, który chcemy rozszerzyć, musi opierać się na pewnej sztuczce. Nie możemy wczytać nazwy pliku ze zmiennej, ale możemy stworzyć specjalny kanał do konfigurowania takich rzeczy. Po stronie szablonów musimy jedynie poinformować OPT, że ma się spodziewać dynamicznej nazwy szablonu:

<?xml version="1.0"?>
<opt:extend file="base.tpl" dynamic="yes">
...

Po stronie skryptu na widoku musimy użyć metodę inherit(szablon rozszerzający, szablon rozszerzany):

$view->inherit('index.tpl', 'innyLayout.tpl');

W ten sposób sprawimy, że index.tpl rozszerzy innyLayout.tpl zamiast base.tpl, jak to jest podane w szablonie. Rozszerzany szablon możemy w dowolnym momencie zmieniać, lecz pamiętajmy, że każda kombinacja jest kompilowana oddzielnie. Jeśli zaoferujemy zbyt dużo wyborów, OPT wyprodukuje ogromną liczbę skompilowanych szablonów dla wszystkich użytych do tej pory przypadków.

Istnieje również mechanizm pośredni, czyli gałęzie:

<?xml version="1.0"?>
<opt:extend file="base.tpl" simplified="simplified.tpl">
...

Wywołanie w szablonie:

$view->setBranch('simplified');

Tym razem wciąż zachowujemy możliwość wyboru, ale pozwalamy szablonowi zdecydować, jak konkretnie ma nazywać się rozszerzany plik. Gdyby w jakimś opt:extend nie istniał atrybut odpowiadający podanej gałęzi, OPT użyje szablonu określonego przez file.