7. Pierwsza podstrona

Stworzymy teraz pierwszą podstronę, wyświetlającą listę zdjęć. Zaczniemy od szablonu, kontynuując wątek poświęcony dziedziczeniu rozpoczęty w poprzednim rozdziale. Poniższą treść zapiszemy w pliku /templates/index.tpl:

<?xml version="1.0"?>
<opt:extend file="layout.tpl">
    <opt:snippet name="content">
        <opt:show name="items" cols="5">
            <table class="photos">
            <opt:grid>
                <tr>
                    <opt:item><td><a parse:href="'index.php?action=preview&id='~$items.id"><img parse:src="'photos/thumb/'~$items.filename" parse:alt="$items.title" /></a></td></opt:item>
                    <opt:emptyItem><td></td></opt:emptyItem>
                </tr>
            </opt:grid>
            </table>
        <opt:showelse><p>Brak zdjęć</p></opt:showelse>
        </opt:show>
 
        <p class="links"><a href="index.php?action=photo">Dodaj zdjęcie</a></p>
    </opt:snippet>
</opt:extend>

Szablon ten dziedziczy po layout.tpl, a zaznaczone jest to użyciem instrukcji opt:extend zamiast opt:root. Wewnątrz tego znacznika mogą znaleźć się wyłącznie instrukcje opt:snippet definiujące fragmenty. Pozostała zawartość będzie ignorowana. Ponieważ szablon bazowy wymaga, aby fragment z treścią nazywał się content, tak też właśnie go nazwaliśmy, zaś wewnątrz podaliśmy, co ma się wyświetlić na stronie głównej. W naszym wypadku będzie to lista miniaturek zdjęć wyświetlana w pięciu kolumnach. Zadanie to spędza sen z powiek wielu programistom, którzy muszą kodować stosowny algorytm w PHP. Nie jest co prawda skomplikowany, tylko po co się męczyć, skoro OPT potrafi napisać go za nas?

Za wyświetlanie list odpowiadają w OPT sekcje, które można traktować, jak inteligentne pętle. Istnieje kilka rodzajów sekcji w zależności od tego, jak chcemy wyświetlić każdy z elementów listy, ale wszystkie obsługuje się według tych samych reguł i wszystkie potrafią łączyć się ze sobą. W przeciwieństwie do PHP, nie musimy tutaj w ogóle martwić się o sposób działania takiej sekcji. Naszym zadaniem jest jedynie określenie wyglądu pojedynczego elementu i nadanie sekcji nazwy, aby OPT mógł znaleźć dla niej dane. Tutaj korzystamy z sekcji opt:grid, która wyświetli nam elementy listy ułożone elegancko w pięciu kolumnach (dokładną ilość określa atrybut cols). W jej wnętrzu musimy użyć jeszcze dwóch atrybutów: opt:item (pojedynczy element listy) oraz opt:emptyItem (pusty element jako ewentualne dopełnienie do pięciu kolumn dla ostatniego wiersza). Ponieważ chcemy, aby w razie braku zdjęć nie pokazywała nam się pusta tabelka, całość musimy dodatkowo opakować instrukcją opt:show. Znacznik opt:showelse na końcu dokumentu pozwala nam określić, co ma się wyświetlić zamiast tabelki, gdy nie będziemy mieli żadnych zdjęć.

Wróćmy jeszcze do wyglądu pojedynczego elementu listy, gdyż znajduje się tam przykład osadzania wyrażeń OPT wewnątrz atrybutów HTML. Nie wolno nam wtedy użyć klamerek. Zamiast tego, przestrzenią nazw parse: zaznaczamy, że dany atrybut będzie mieć dynamiczną wartość podaną właśnie w formie wyrażenia. Zwróćmy uwagę, jak prosto odwołujemy się do zmiennych pojedynczego elementu sekcji: $nazwaSekcji.nazwaZmiennej. Zapis: 'index.php?action=preview&id='~$items.id to zwykłe sklejanie ciągów tekstowych, tyle że zamiast kropki stosowanej do czego innego, odpowiednim operatorem jest tylda ~ (pomysł zaczerpnięty z języka D).

Podsumujmy, czego już dowiedzieliśmy się o szablonach. Nietrudno zauważyć, że na ich poziomie zupełnie nie musimy zajmować się dokładnym działaniem naszego szablonu, a nawet typami danych! Czy gdziekolwiek była mowa, że dane dla sekcji albo jej elementy będą tablicami? Nie, ponieważ po stronie szablonu jest to nam niepotrzebne. OPT zajmie się tym automatycznie na etapie kompilacji szablonu, korzystając z informacji dostarczonych przez skrypt. Tak samo, zamiast zastanawiać się, jak wyświetlać elementy w pięciu kolumnach, zwyczajnie mówimy, jak ma wyglądać wiersz, jak element w kolumnie oraz pusty element, a zadanie skonstruowania z tego algorytmu zostawiamy parserowi.

Po tym przydługim wstępie związanym z szablonami, powróćmy do kodu PHP oraz do Doctrine, gdyż tutaj czekają na nas podstawy języka DQL. Kod strony głównej zapiszemy w pliku /actions/list.php:

<?php
function action()
{
    $view = new Opt_View('index.tpl');
    $view->title = 'Lista zdjęć';
    $view->items = Doctrine_Query::create()
        ->select('id, title, filename')
        ->from('Photo')
        ->orderBy('id DESC')
        ->execute(array(), Doctrine::HYDRATE_ARRAY);
    return $view;
} // end action();

Od strony organizacyjnej okroiliśmy kod do minimum w postaci prostej funkcji o nazwie action. W jej wnętrzu tworzymy widok dla OPT, podając nazwę szablonu oraz wartości poszczególnych zmiennych, a całość na końcu zwracamy tak, by w pliku index.php można było ją wyświetlić. Najciekawszym fragmentem tego kodu jest niewątpliwie pobranie odpowiednich danych z bazy. Język DQL jest bardzo podobny do SQL-a, a główne różnice polegają na tym, że zamiast na tabelkach, operujemy tutaj na naszych modelach. DQL-a można zapisywać na dwóch poziomach: w postaci czysto tekstowej, co jednak nie jest polecane ze względu na niską wydajność (Doctrine musi przeparsować całe zapytanie i przetłumaczyć je na SQL), albo w postaci obiektowej, której właśnie tutaj użyliśmy. Metodą Doctrine_Query::create() tworzymy obiekt zapytania, a następnie za pomocą różnych metod dodajemy do niego różne elementy zapytania, np. select czy orderBy. W „klauzuli” from() nazwa „Photo” nie odnosi się do tabeli w bazie, ale do modelu, który zdefiniowaliśmy na samym początku artykułu w pliku YAML.

Ktoś może stwierdzić, że to tylko czysto kosmetyczna różnica. Otóż nie do końca tak jest. W przeciwieństwie do SQL-a, Doctrine pamięta o relacjach między modelami oraz istniejących między nimi kluczach obcych. Przykładowo, zapytanie korzystające z dwóch tabel równorzędnych mogłoby mieć postać:

Doctrine_Query::create()
    ->select('a.*, b.*')
    ->from('Tabela1 a')
    ->innerJoin('a.Relacja b');

Jeśli Doctrine wie, że między tabelami istnieje relacja, to zapis a.Relacja b zinterpretuje prawidłowo i nie zrobi nam w tym miejscu iloczynu kartezjańskiego, ale doda nam do zapytania warunek WHERE a.id = b.tabela1_id. Różnice będą jeszcze bardziej widoczne, gdy popatrzymy, jak wygląda pojedynczy rekord zbioru wyników. SQL zwraca płaskie dane, tymczasem Doctrine konwertuje je na drzewko uwzględniające już od razu istniejące w nim relacje.

Ostatnią rzeczą, jaką musimy wykonać na obiekcie zapytania, jest wywołanie metody execute(). Za pierwszy argument definiujący dodatkowe parametry dla zapytania podstawiamy pustą tablicę (nie mamy dodatkowych parametrów), zaś w drugim wybieramy rodzaj rzutowania, o którym wspominaliśmy przy okazji tworzenia modelu. Doctrine domyślnie opakowuje wyniki w obiekty, jednak z powodów wydajnościowych, gdy nie musimy modyfikować pobranych wierszy, powinniśmy dokonać ich rzutowania na tablice. Biblioteka zwróci nam wtedy rezultat podobny do poniższego:

array(0 =>
    array('id' => 1, 'title' => 'Zdjęcie 1', 'filename' => 'plik.jpg'),
    array('id' => 2, 'title' => 'Zdjęcie 2', 'filename' => 'plik.jpg'),
    array('id' => 3, 'title' => 'Zdjęcie 3', 'filename' => 'plik.jpg'),
    // itd.
);

Jest to jednocześnie w pełni poprawny domyślny format danych dla sekcji OPT.