6. Rozpoczęcie prac nad skryptem

Rozpoczniemy od napisania pliku index.php, który będzie inicjował obie biblioteki oraz ładował odpowiednią akcję na podstawie danych z adresu URL. Pierwszą rzeczą będzie skonfigurowanie autoloaderów oraz ścieżek dostępu:

<?php
// Inicjuj include_path i strefe czasowa
set_include_path(get_include_path().PATH_SEPARATOR.'./models'.
    PATH_SEPARATOR.'./models/generated'.PATH_SEPARATOR.'./libs');
date_default_timezone_set('Europe/Warsaw');
try
{
    // Inicjuj autoloader OPT
    require('./libs/Opl/Base.php');
    Opl_Loader::setDirectory('./libs/');
    Opl_Loader::register();
 
    // Inicjuj autoloader Doctrine
    require('./libs/Doctrine.php');
    spl_autoload_register(array('Doctrine', 'autoload'));

Na początku odpowiednio ustawiamy include_path, podając ścieżkę do katalogu z bibliotekami oraz modeli Doctrine. Następnie rozpoczynamy blok try, aby wyłapać ewentualne wyjątki i wczytujemy autoloader OPT. Tutaj kilka słów wyjaśnienia odnośnie tej biblioteki. OPT w rzeczywistości jest pierwszą z serii bibliotek określanych wspólną nazwą OPL. Aby zapewnić ich właściwą współpracę i nie duplikować tej samej funkcjonalności, wszystkie one będą korzystać ze wspólnego jądra zlokalizowanego właśnie w katalogu /libs/Opl – dostarcza ono autoloadera, systemu obsługi błędów, konfiguracji oraz mechanizmu wtyczek. Sam autoloader nie korzysta z include_path, ale za to daje użytkownikowi większą elastyczność w doborze lokalizacji poszczególnych bibliotek. My skorzystaliśmy z metody Opl_Loader::setDirectory(), aby poinformować, że wszystkie znajdują się w tym samym miejscu. Następnie metodą Opl_Loader::register() instalujemy autoloader w interpreterze. W drugiej kolejności, identycznie jak w interfejsie linii komend, inicjujemy autoloader Doctrine.

    // Wlacz rozszerzone komunikaty o bledach do celow diagnostycznych
    Opl_Registry::setState('opl_extended_errors', true);
 
    // Wczytaj konfiguracje
    require('./config.php');

Przy przenoszeniu aplikacji na serwer produkcyjny opcję w drugiej linijce należy wyłączyć, gdyż w przeciwnym razie każdy wyjątek będzie powodował dodatkowo wyświetlanie informacji, które mogą zagrozić bezpieczeństwu witryny lub serwera.

Doctrine inicjujemy w sposób analogiczny, co w pliku interfejsu linii komend:

    // Inicjuj Doctrine
    $manager = Doctrine_Manager::getInstance();
    $manager->setAttribute(Doctrine::ATTR_MODEL_LOADING, Doctrine::MODEL_LOADING_CONSERVATIVE);
    Doctrine::loadModels('./models/');
 
    $conn = Doctrine_Manager::connection($config['database']['dsn']);
    $conn->setCharset($config['database']['charset']);
    foreach($config['database']['attributes'] as $attributeName => $attributeValue)
    {
        $conn->setAttribute($attributeName, $attributeValue);
    }
    $conn->setAttribute(Doctrine::ATTR_AUTO_ACCESSOR_OVERRIDE, true);
    $conn->setAttribute(Doctrine::ATTR_QUOTE_IDENTIFIER, true);

Następną rzeczą jest skonfigurowanie OPT do pracy. Polega ono na utworzeniu obiektu Opt_Class i umieszczeniu w nim konfiguracji z pliku config.php:

    // Inicjuj OPT
    $tpl = new Opt_Class;
    $tpl->loadConfig($config['opt']);   
    // Zaladuj dodatkowe rzeczy
    require('./libs/stuff.php');
 
    $tpl->setup();

Dodatkowo, w pliku /libs/stuff.php znajduje się kod instalacji dodatkowych komponentów, które będziemy wykorzystywać w szablonach. Wrócimy do niego w późniejszej części artykułu. Po skonfigurowaniu, należy wywołać metodę Opt_Class::setup().

Open Power Template zorganizowany jest w sposób podobny do frameworków. Nie mamy tutaj jednej klasy „do wszystkiego”, lecz szereg mniejszych i bardziej wyspecjalizowanych, lepiej odwzorowujących rzeczywistość. Podstawą pracy są tzw. widoki, czyli obiekty klasy Opt_View. Można na nie patrzeć jako na dane ze skryptu wraz z towarzyszącym im szablonem. Jeden skrypt może zarządzać wieloma widokami jednocześnie, bez konieczności konfigurowania każdego z nich z osobna oraz obaw o kolizje nazw zmiennych. Aby przetworzyć widok, potrzebny jest nam z kolei system wyjścia, czyli obiekt dowolnej klasy implementującej Opt_Output_Interface. My skorzystamy z Opt_Output_Http, który powstały z szablonu kod wynikowy wysyła bezpośrednio do przeglądarki oraz pozwala na zarządzanie nagłówkami HTTP. Nasz skrypt będzie odbierać widok z akcji i wyświetlać go przy pomocy tego systemu wyjścia:

    $action = 'list';
    if(isset($_GET['action']) && ctype_alpha($_GET['action']))
    {
        $action = $_GET['action'];
    }
    require('./actions/'.$action.'.php');
    $view = action();
    $output = new Opt_Output_Http;
    $output->setContentType(Opt_Output_Http::XHTML, 'utf-8');
    $output->render($view);

Jak widać, biblioteka potrafi też całkiem nieźle zarządzać nagłówkiem Content-type. W naszym przypadku wysłana zostanie informacja o dokumencie XHTML tylko wtedy, gdy przeglądarka go obsługuje. W przeciwnym wypadku otrzyma ona zwyczajny nagłówek text/html.

Musimy jeszcze zadbać o prawidłową obsługę wyjątków. OPL dostarcza gotowy interfejs, wzbogacający wyjątek o szereg informacji pomocnych w odnalezieniu przyczyny błędu. Z Doctrine niestety tak fajnie nie ma, dlatego aby się nie przemęczać, opakujemy przechwycone stamtąd wyjątki w jedną z klas OPL-a i wyświetlimy je także przy pomocy tego samego systemu:

}
catch(Opt_Exception $e)
{
    $handler = new Opt_ErrorHandler;
    $handler->display($e);
}
catch(Doctrine_Exception $e)
{
    $wrapper = new Opl_Exception($e->getMessage());
    $handler = new Opl_ErrorHandler;
    $handler->display($wrapper);
}

Aby zakończyć tę część, musimy jeszcze dopisać do pliku config.php konfigurację OPT:

$config = array(
    'database' => array(
        'dsn' => 'mysql://user:password@host/dbname',
        'charset' => 'utf8',
        'attributes' => array(
            'use_native_enum' => true
        )
    ),
    'opt' => array(
        'sourceDir' => './templates/',
        'compileDir' => './templates_c/',
        'stripWhitespaces' => false,
        'charset' => 'utf-8'
    )
);

Dwie pierwsze dyrektywy, sourceDir oraz compileDir są niezbędne do prawidłowej pracy, gdyż wskazują one na położenie katalogów z szablonami. StripWhitespaces, jeśli jest włączona, usuwa z kodu HTML zbędne białe znaki, jednocześnie utrudniając odczytanie kodu wynikowego osobom postronnym. Na czas tworzenia skryptu warto ją jednak wyłączyć, byśmy mogli w razie potrzeby sprawdzić, jak wygląda generowany kod. Ostatnia z dyrektyw to ustawienie kodowania dla celów nagłówków HTTP.

Przejdźmy teraz do stworzenia szkieletowego szablonu HTML, definiującego ogólną strukturę kodu wynikowego i określającą miejsce wyświetlania właściwej treści. Nasz plik zapiszemy pod nazwą /templates/layout.tpl a jego treść będzie następująca:

<?xml version="1.0" ?>
<opt:root include="snippets.tpl">
<opt:prolog version="1.0" />
<opt:dtd template="xhtml10transitional" />
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="pl" lang="pl">
<head>
    <title>{$title} - galeria zdjęć</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <link rel="stylesheet" href="style.css" type="text/css" />
</head>
<body>
<div id="header">
    <h1>Galeria zdjęć</h1>
    <h2>{$title}</h2>
</div>
<div id="content">
    <opt:insert snippet="content">
        <p>Brak treści</p>
    </opt:insert>
</div>
<div id="footer">
    <p>Galeria stworzona na użytek artykułu <em>Piszemy galerię zdjęć w Doctrine i OPT</em> autorstwa Tomasza Jędrzejewskiego.</p>
    <p>Kod źródłowy dostępny na zasadach licencji X11</p>
</div>
</body>
</html>
</opt:root>

OPT jest de facto parserem XML i w domyślnym trybie musimy zachować maksymalną zgodność z regułami tego języka. Każdy szablon musi posiadać XML-owy prolog, który jednak nie będzie wyświetlany w przeglądarce. Potrzebujemy również głównego znacznika i jego rolę pełni tutaj opt:root. Przy okazji pozwala on ustawić dodatkowe atrybuty szablonu lub, jak w naszym przypadku, załadować inny plik, snippets.tpl. Jego przeznaczenie poznamy za chwilę.

Ogólnie, język szablonów OPT składa się z dwóch elementów:

  1. Instrukcje – mają postać znaczników i atrybutów w przestrzeni nazw opt. Sterują one wyświetlaniem szablonu i umożliwiają wykonywanie różnych manipulacji. Jedna instrukcja może składać się z kilku znaczników i atrybutów.

  2. Wyrażenia, np. $a+$b – pełnią identyczną rolę, jak w PHP i buduje się je według takich samych zasad. W stosunku do PHP zmienione zostały niektóre elementy składni, aby ułatwić ich stosowanie w obrębie dokumentu XML. Wyrażeń można używać jako wartości atrybutów instrukcji/innych znaczników lub osadzać w statycznym tekście przy pomocy klamerek – ich wartość zostanie wtedy wyświetlona we wskazanym miejscu.

Dwie pierwsze instrukcje, jakie coś wyświetlają, to opt:prolog oraz opt:dtd, generujące prolog oraz DTD dokumentu wynikowego, ale już dla przeglądarki. Opt:dtd pozwala dodatkowo na proste określenie rodzaju dokumentu, bez konieczności pamiętania całego, skomplikowanego ciągu. Dalej mamy dynamicznie generowany tytuł, ładowany ze zmiennej $title. W miejscu treści znajduje się natomiast instrukcja opt:insert.

OPT, jak każdy system szablonów, umożliwia składanie dokumentu wynikowego z kilku plików tak, abyśmy nie musieli przepisywać 200 razy nagłówka i stopki strony. Jednak w przeciwieństwie np. do Smarty'ego, musimy trzymać się reguł XML-a i nie wolno nam umieścić początku znacznika <HTML> czy <BODY> w jednym pliku, a zamknięcia w innym, dlatego konieczne jest patrzenie na szablony jak na pojemniki, w których możemy osadzać inne pojemniki. Jedną z dwóch głównych technik modularyzacji jest tzw. dziedziczenie szablonów, które przywędrowało z rozwiązań stosowanych w systemach dla języka Python. Nazwa pochodzi stąd, iż jest ono dość podobne do dziedziczenia stosowanego w programowaniu obiektowym, o czym zaraz się przekonamy.

Rolę klasy pełni tutaj pojedynczy szablon, zaś rolę metod – tzw. fragmenty (ang. snippets). Są to kawałki kodu XML-owego, którym została nadana unikalna nazwa. Gdy jeden szablon rozszerza inny, może nadpisać zdefiniowane przez tamten fragmenty oraz dołożyć nowe. Podobnie jak w przeciążonej metodzie, tak i tutaj przeciążony fragment może wciąż odwołać się do wersji bazowej. Jedyną różnicę między programowaniem obiektowym, a dziedziczeniem szablonów stanowi pierwszy szablon, z którego dziedziczymy. Zamiast fragmentów, umieszczamy w nim zwykłą treść i definiujemy miejsca, w których należy konkretne fragmenty szablonów dziedziczących wyświetlić. Na takiej zasadzie działa nasz layout.tpl – jest to szablon bazowy dla dziedziczenia i poprzez instrukcję opt:insert mówi, że fragment content ma się wyświetlić w obrębie znacznika <div id="content">. Dodatkowo, możemy zdefiniować zawsze treść domyślną, która zostanie wyświetlona, gdyby żaden szablon nie definiował fragmentu o takiej nazwie.