Table of contents
Dlaczego szablony są ważne
Oddzielenie logiki od prezentacji
Fatalnie utrzymuje się i rozwija aplikacje, w których w tej samej linijce jest kod ustalający kolor pisma i zapisujący dane do bazy. Tą metodą wychodzą zlepki kodu pokroju osCommerce.
Do tego są specyficzne dla PHP problemy, jak “headers already sent” przy ustawianiu cookie, gdy został już wypluty nagłówek HTML. Wprawdzie da się to obejść ustawiając ob_start()
, ale i tak pozostają problemy — np. funkcje będące później w kodzie nie mogą zmienić tytułu strony ani dodać elementów do <head>
.
Oddzielenie logiki od prezentacji pozwala też mieć różne prezentacje tego samego dokumentu. Na przykład nie trzeba drugi raz klepać tych samych funkcji do użytku przez AJAX — można podmienić szablon HTMLowy na XMLowy albo zamiast systemu szablonów konwertować rezultat na JSON.
Syf w kodzie i ataki XSS
Porządny system szablonów może zapewnić poprawne escape'owanie tekstu, czyli zamieniać specyficzne dla HTML znaki w tekście na encje. Dzięki temu poprawne kodowanie <a href>
(o co walidator zawsze się czepia) z niewyobrażalnie mozolnego zadania nagle staje się rzeczą, która robi się sama.
Do tego szablony mogą zapewnić, że HTML/XHTML będzie (przynajmniej składniowo) poprawny. Przy generowaniu XHTML to jest konieczność.
Poprawne kodowanie encji wszędzie jest też ważnym elementem zabezpieczania się przed atakami XSS, czyli wrabianiem aplikacji w umieszczenie na stronie JavaScript, który np. wykradnie login albo cookie z sesją. Dziurawa strona może też być wykorzystana do spamowania wyszukiwarek. Istnieje przerażająco duża ilość przebiegłych sposobów na ominięcie filtrów próbujących usunąć JavaScript z danych wejściowych, więc trzeba się tym zająć na wyjściu.
Czym, a czym nie?
Dobry system szablonów to taki, w którym łatwiej jest zrobić poprawny dokument, niż niepoprawny.
Weźmy za zadanie: wygenerować odnośnik (X)HTML, który ma opcjonalny atrybut title i ma mieć tekst opcjonalnie umieszczony w tagu <em>
.
PHP jako język szablonów
Rozdzielenie kodu na PHP-obrabiający-dane i PHP-wyświetlający-dane to zawsze lepsze, niż nic, ale paradoksalnie PHP jest kiepski jako preprocesor hipertekstu.
Wypisanie odnośnika z tekstową etykietą wymaga strasznej kombinacji alpejskiej:
<a href="<?php echo htmlspecialchars($href);?>"<?php if ($title) { ?> title="<?php echo htmlspecialchars($title);?>"<?php }?>><?php if ($em) { ?><em><?php } echo htmlspecialchars($tytul); if ($em) ?></em><?php } ?></a>
Można by to nieco skrócić przerabiając wszystko na jedno złożone echo, ale dużo piękniejsze to się nie zrobi — domyślnie PHP nawet nie ma żadnego skrótu na wypisywanie HTML i co krok wymaga męczenia się z htmlspecialchars()
.
Do tego programista musi pamiętać o zamknięciu wszystkich tagów (a samego kodu PHP nie da się zwalidować, więc trzeba walidować różne rezultaty różnych warunków — nietrudno coś przegapić). Na dodatek jeśli element jest dodawany warunkowo — trzeba powtórzyć warunek przy tagu zamykającym.
Smarty, Open Power Template 1.x i dziesiątki podobnych
Lekka ulga dla PHPowej składni, ale problemy wszystkie te same:
<a href="{$href|escape}"{if $title} title="{$title|escape}"{/if}>{if $em}<em>{/if}{$tytul|escape}{if $em}</em>{/if}</a>
OPT 2.x to zupełnie inna bestia i zachowuje się poprawnie.
OPT 1.x można jeszcze przełączyć na bardziej XMLopodobną składnię, ale to tylko powierzchowna zmiana — mimo tego nadal nie będzie miał zielonego pojęcia o składni języka, który generuje, więc bez zająknięcia zrobi tagzupę tak samo, jak Smarty.
DOM
Generowanie kodu za pomocą DOM gwarantuje jego poprawność składniową — programista buduje elementy i atrybuty, a serializer zajmuje się przeróbką tego na tagi i encje. To jest wielką zaletą.
Natomiast wielką wadą DOM są niesamowicie długie nazwy funkcji (umyślnie długie, żeby na pewno nie kolidowały z funkcjami w jakimś języku programowania) i mozolne budowanie każdego detalu.
$doc = new DOMDocument();
$a = $doc->createElement('a'); $a->setAttribute('href',$href);
if ($title) $a->setAttribute('title',$title);
if ($em) { $temp = $doc->createElement('em'); $a->appendChild($temp); }
else $temp = $a;
$temp->appendChild($doc->createTextNode($tytul));
DOM staje się znośny dopiero, gdy napisze się kilka funkcji pomocniczych, które np. przerabiają tablicę asocjacyjną na element z atrybutami.
XSLT
XSLT wymaga danych wejściowych jako drzewo DOM, więc nie można się kompletnie pozbyć DOM, ale na dłuższą metę jest mniej roboty, bo wszystkie “dekoracyjne” elementy i atrybuty może dodać XSLT.
<x:template match="a">
<a href="{href}"><x:if test="string(title)"><x:attribute name="title"><x:value-of select="title"/></x:attribute></x:if><x:value-of select="tytul"/></a>
</x:template>
XSLT działa w unikalny sposób — bardziej jak CSS, niż imperatywny język programowania. Deklaruje się szablony, które za pomocą selektorów XPath same “złapią” i przerobią odpowiednie elmenty w wejściowym dokumencie. Dzięki temu łatwo jest upewnić się, że np. każdy adres e-mail jaki przekażesz do szablonu będzie zawsze miał odpowiedni kod HTML (nadaną klasę albo ukrywanie przed spambotami) i nie trzeba o tym za każdym razem pamiętać. Oczywiście na początku trudno przestawić się na taką filozofię.
XSLT może generować zarówno HTML, jak i XHTML. Stety lub niestety, XHTML który generuje XSLT jest bezlitośnie XMLowy i dla XSLT nie ma różnicy między notacją <img/>
, <img></img>
ani <xhtml:img xmlns:xhtml="http://www.w3.org/1999/xhtml"/>
. Trudno jest to kontrolować, więc zapomnij o serwowaniu pseudoXHTML “działającego” w Internet Explorerze i Google (to mała strata).
Następną zaleto-wadą jest konieczność podania wszystkich danych do szablonu. Wada, bo aplikacja musi przewidzieć wszystko, co będzie na stronie i co trzeba dla szablonu przygotować, bo bez hacków szablon nie ma jak zażądać dodatkowych danych. Zaleta, bo nie da się do szablonu wpakować za dużo logiki aplikacji (inaczej rozdzielenie logiki i prezentacji zamieniło by się na przeprowadzkę do innego języka programowania).
Zdecydowaną wadą XSLT jest składnia. Niektóre rzeczy wymagają dużo pisania. Niektóre strasznie dużo pisania. Bez edytora z makrami i auto-uzupełnianiem kodu będzie męczący.
PHPTAL
PHPTAL (polska strona) jest klonem systemu szablonów ZPT (składającego się z TAL, TALES i METAL) z pythonowego CMS-u Zope. Dużo nazw, jak na jeden produkt :)
<a href="${href}" tal:attributes="title title | nothing"><em tal:omit-tag="not:em" tal:content="tytul" /></a>
PHPTAL rozwiązuje bolączki każdego z konkurentów:
- Składnia jest zwięzła i konkurencyjna nawet ze Smarty.
- Dobrze chroni przed XSS. Domyślnie wszystkie dane są odpowiednio escape'owane i nie trzeba się o to dopraszać (a wstawienie “surowego” HTML jest równie proste, co wstawienie nie-HTML w innych szablonach).
- Pilnuje, aby wszystkie tagi były pozamykane. Ma atrybut
omit-tag
, który “ukrywa” tagi, które mają być opcjonalne, dzięki czemu nie trzeba powtarzać warunku w tagach zamykających. - TAL (Template Attribute Language) używa prawie wyłącznie atrybutów, więc nawet skomplikowane szablony wyglądają poprawnie w przeglądarce, dzięki czemu osoba klepiąca XHTML nie musi mieć serwera WWW na swoim kompie, można na szablonach używać narzędzi dla HTML/XML, itd.
- Można do szablonów przekazywać obiekty PHP oraz czytać ich pola i wyoływać metody, dzięki czemu szablon może pobrać tylko to, co niezbędne.
- Kompiluje szablony do PHP, dzięki czemu większość z tych rzeczy nic nie kosztuje.
- Jest bardzo rozszerzalny. Można robić własne atrybuty, np. dodałem cache'owanie HTML wygenerowanego ze wskazanych tagów (dostępne w PHPTAL 1.1.9) i modyfikatory wyrażeń (np. formatowanie daty).
Ale jak wszystko ma wady:
- Składnia nie ma eleganckiej konstrukcji na if-else. W przypadku elementów trzeba zapamiętać warunek przez
tal:define
i użyć dwóchtal:condition
. W przypadku atrybutów trzeba napisać sobie własny modyfikator wyrażen albo użyć wstawek PHP. - Wyrażenia TAL (TALES) mają ograniczone możliwości, żeby nie pozwalać na przerzucanie logiki do szablonów. Jednak te ograniczenia czasem przeszkadzają i potrzeba wstawek w PHP.
- PHP w szablonach ma niepotrzebnie udziwnioną składnię (upodobnioną do Pythona).
Obsługuje tylko składnię XHTML, ale pozwala trzymać się wytycznych Dodatku C specyfikacji, więc nie ma problemów z kompatybilnością z IE, Googlebot, itp.Wersja 1.2 generuje HTML5.
Inne?
XT
Nie testowałem jak to działa w praktyce.
XT ma radykalnie inne podejście do szablonów — zamiast używać rozkazów w szablonach, które wyciągają dane z programu, to używa selektorów CSS/XPath do wypełniania istniejącego dokumentu danymi.
patTemplate
Nie testowałem.
patTemplate jest XMLowym systemem szablonów, który stara się być zupełnie deklaratywnym. Nie ma if/else, nie ma pętli — szablony są dopasowywane do danych wejściowych i automatycznie powielane przy wyświetlaniu tablic.
Więcej?
Jak znasz jakieś inne systemy szablonów, które nie są zlepiaczką skrawków tekstu, ani nie są porzuone od kilku lat, to daj znać.
Forum dyskusyjne o szablonach.
Przerabianie aplikacji pod szablony
Do wykorzystania szablonów trzeba zmienić nieco architekturę aplikacji — funkcje zamiast wyrzucać swój rezultat prosto do użytkownika, powinny zwracać dane. Do tego wystarczy stary, dobry return i tablice (asocjacyjne).
zmiana:
echo "<h1>$foo</h1>";
echo "<ul>";
foreach(wczytaj_dane_z_bazy() as $costam)
{ echo "<li>$costam</li>"; }
echo "</ul>";
na:
return array(
'foo'=>$foo,
'cosie'=>wczytaj_dane_z_bazy()
);
i gdzieśtam później w szablonie:
<h1 tal:content="foo" />
<ul><li tal:repeat="cos cosie" tal:content="cos" /></ul>
Do tworzenia bardziej złożonych tablic w PHP wypada znać ich składnię, a w szczególności najprostszy sposób dodawania elementów (coby się z indeksami ani array_push
nie wygłupiać). W praktyce w kodzie mogą często pojawiać się podobne konstrukcje:
$rezultat = array();
foreach($dane_wejsciowe as $d)
{
$rezultat[] = array(
'bla'=>zrob_bla($d),
'baz'=>$d['baz'],
'quz'=>'quz' . $d['quz1'] . $d['quz2'],
);
}
return array(
'to'=>"to i owo",
'rezultat'=>$rezultat,
);
A główna część aplikacji może wyglądać tak:
$strona = wykombinuj_dane_strony();
$tpl = new PHPTAL($strona['szablon']);
$tpl->strona = $strona;
$tpl->uzyszkodnik = znajdz_kto_zalogowany();
$tpl->menu = wyczaruj_menu();
echo $tpl->execute(); // jedyne echo w całej aplikacji!