PHP/Open Power Template

Z Wikibooks, biblioteki wolnych podręczników.
< PHP
Poprzedni rozdział: Savant
Następny rozdział: PHPTAL

Open Power Template[edytuj]

Omówiony w poprzednim rozdziale Savant to doskonały przykład biblioteki wykorzystującej PHP jako język szablonów. W Open Power Template sytuacja odwraca się o 180 stopni. Ten system udostępnia swój własny, XML-owy język, którego cechą szczególną jest deklaratywne podejście do tworzenia szablonów. Zamiast skupiać się na szczegółach implementacyjnych, twórca szablonu powinien jedynie powiedzieć, co chce osiągnąć i zlecić kompilatorowi wygenerowanie odpowiedniego kodu, który spełni jego życzenia. Pomaga w tym rozumienie struktury HTML szablonu oraz unikalna cecha, tzw. formaty danych. Aby nie być gołosłownym, zaprezentujemy poniżej przykładowy szablon:

<?xml version="1.0" ?>
<opt:root>
<opt:prolog />
<opt:dtd template="html5" />
<html>
 <head>
  <title>{$pageTitle}</title>
 </head>
 <body>
  <p>Polecamy nasze produkty:</p>
  <ul>
   <li opt:section="products"><a parse:href="$products.url">{$products.name}</a></li>
  </ul>
 </body>
</html>
</opt:root>

Oprócz tego, Open Power Template udostępnia obiektowy interfejs programistyczny dla PHP 5.2/5.3 dostosowany do potrzeb integracji z popularnymi frameworkami, które niebawem poznamy. Do naszej dyspozycji są trzy najważniejsze elementy interfejsu:

  1. Opt_Class - główna klasa zarządzająca konfiguracją.
  2. Opt_View - klasa reprezentująca tzw. widok, czyli określony szablon i towarzyszące mu dane ze skryptu:
  3. Opt_Output_Interface - interfejs systemu wyjścia, który mówi, gdzie wysłać wygenerowany kod.

Podobnie jak w naszym prostym systemie edukacyjnym, nasza aplikacja WWW tworzy sobie odpowiednią liczbą widoków i przekazuje do nich dane. Jednak zamiast wykonywać je od razu, przekazuje je do jednego z systemów wyjścia, który zajmuje się wykonaniem szablonu i wysłaniem wyniku w określone miejsce. Przykładowo, jeśli skorzystamy z systemu wyjścia Opt_Output_Http, dane polecą do przeglądarki, a oprócz tego możemy też stworzyć sobie system wyjścia Email, który podany szablon potraktuje jako np. treść wiadomości e-mail, którą trzeba wysłać pod określony adres.

Instalacja[edytuj]

Bibliotekę można pobrać ze strony www.invenzzia.org - pobieramy najnowsze wydanie z gałęzi 2.0.x, które jest omówione w tym podręczniku. W ściągniętym archiwum znajdziemy katalog /lib z właściwym kodem źródłowym biblioteki. Pozostałe katalogi to przykłady, dokumentacja oraz testy. Przenosimy wspomniany katalog gdzieś do drzewa katalogowego naszej aplikacji WWW.

Druga czynność to stworzenie folderów na szablony. Pierwszy z nich nazwijmy /templates - będą tam źródłowe wersje szablonów. W drugim, /templates_c Open Power Template będzie zapisywać skompilowane wersje, dlatego upewnijmy się, że PHP posiada do niego prawa zapisu.

Open Power Template jest częścią większej rodziny bibliotek Open Power Libs, dla których potrzeb opracowane zostało wspólne mikrojądro oferujące m.in. automatyczną ładowarkę czy system obsługi błędów. Znajdziemy je w katalogu /lib/Opl, podczas gdy właściwy kod systemu szablonów będzie w /lib/Opt. Gdybyśmy chcieli użyć w przyszłości innych bibliotek z tej rodziny, wystarczy wgrać je tuż obok, gdyż będą one korzystać z tego samego jądra.

Kolejna rzecz to ustawienie automatycznej ładowarki:

<?php
require('/sciezka/do/opl/Opl/Base.php');
Opl_Loader::setDirectory('/sciezka/do/opl/');
Opl_Loader::register();

Autoloader OPL ma charakter uniwersalny, tzn. można go także wykorzystać do ładowania innych bibliotek z kompatybilnym schematem nazewnictwa (np. Doctrine), aczkolwiek powyższy sposób jest dobry jedynie dla bibliotek OPL. Poniżej jest przedstawione bardziej uniwersalne podejście:

<?php
require('/sciezka/do/bibliotek/Opl/Base.php');
Opl_Loader::addLibrary('Opl', array('basePath' => '/sciezka/do/bibliotek/'));
Opl_Loader::addLibrary('Opt', array('basePath' => '/sciezka/do/bibliotek/'));
Opl_Loader::addLibrary('InnaBiblioteka', array('basePath' => '/sciezka/do/bibliotek/', 'handler' => null));
Opl_Loader::register();

Opcja handler ustawiona na null pozwala wyłączyć dodatkowe opcje ładowania klas specyficzne dla OPL-a, które mogą nie znaleźć zastosowania przy innych bibliotekach. I to wszystko. Jeśli poustawialiśmy dobrze ścieżki, możemy już zacząć korzystać z OPT.

Pierwszy szablon[edytuj]

Przypomnijmy sobie jeszcze raz szablon z początku rozdziału:

<?xml version="1.0" ?>
<opt:root xmlns:opt="http://xml.invenzzia.org/opt">
<opt:prolog />
<opt:dtd template="html5" />
<html>
 <head>
  <title>{$pageTitle}</title>
 </head>
 <body>
  <p>Polecamy nasze produkty:</p>
  <ul>
   <li opt:section="products"><a parse:href="$products.url">{$products.name}</a></li>
  </ul>
 </body>
</html>
</opt:root>

Możemy tu zauważyć kilka charakterystycznych dla OPT zachowań. Szablon jest domyślnie traktowany jak dokument XML, dlatego powinien posiadać prolog <?xml version="1.0" ?>, a co więcej - jest on podany wyłącznie do wiadomości parsera. Jeśli chcemy wygenerować prolog w kodzie wyjściowym, korzystamy z instrukcji opt:prolog, która podana bez argumentów nadaje im domyślne wartości. Oprócz tego chcielibyśmy dodać odpowiedni DTD. Komendy DTD także idą na cele parsera biblioteki, stąd też bierze się instrukcja opt:dtd. Ma ona predefiniowanych kilka szablonów DTD dla najpopularniejszych języków, np. html5, dzięki czemu ich wstawianie jest łatwiejsze.

XML wymaga od nas jeszcze jednej ważnej rzeczy, mianowicie w dokumencie nie może być więcej niż jednego głównego znacznika. W tradycyjnym HTML-u rolę głównego znacznika pełnił element <html>, ale ponieważ mamy obok niego jeszcze dwa inne, OPT oddaje nam do dyspozycji opt:root. Oprócz bycia głównym znacznikiem, można w nim poustawiać kilka rzeczy odnoszących się do całego szablonu.

Jak widać, przejście na język XML-owy w momencie, gdy XHTML oraz HTML same albo wywodzą się, albo są podobne do niego, spowodowało konieczność specjalnego traktowania niektórych elementów, jednak w praktyce musimy je wykonać tylko raz, na samym początku. Gdy zaczniemy pracować z właściwą treścią, sytuacja znacząco się odwraca. Zerknijmy na linijkę 7. Pokazane jest tam, jak osadzać zmienne ze skryptu w tekście międzyznacznikowym. Używamy do tego klamerek, w którym zapisujemy nasze wyrażenie. Zapis ten oznacza, że tytuł strony zostanie załadowany ze zmiennej $pageTitle.

W linijce 12 pragniemy wyświetlić kilka produktów jako listę wypunktowaną. Do wyświetlania list służą tzw. sekcje, czyli rodzaj inteligentnych pętli. Jednak zamiast obudowywać element <li> pętlą, wystarczy że dokleimy do niego atrybut opt:section z podaną nazwą sekcji. Jest to prosty sposób powiedzenia: ten kawałek ma być użyty jako wzorzec do wyświetlenia pojedynczego elementu listy, przy czym nie obchodzi mnie teraz, jak ta lista będzie dokładnie działać. Nazwa sekcji umożliwia nam później dostęp do zmiennych elementu listy, np. {$products.name} - zauważmy, że do separacji służy nam kropka.

W tej samej linijce widać też, że aby wczytać zawartość atrybutu ze zmiennej, nie używamy klamerek, lecz do nazwy doklejamy parse:, a wyrażenie zapisujemy bezpośrednio w cudzysłowach. Plik zapisujemy do katalogu ./templates/.

A oto i kod PHP, który powyższy szablon uruchomi:

<?php
require('/sciezka/do/bibliotek/Opl/Base.php');
Opl_Loader::addLibrary('Opl', array('basePath' => '/sciezka/do/bibliotek/'));
Opl_Loader::addLibrary('Opt', array('basePath' => '/sciezka/do/bibliotek/'));
Opl_Loader::register();

$pdo = new PDO('mysql:host=localhost;dbname=test', 'root', 'root');

try
{
   // Konfiguracja OPT
   $tpl = new Opt_Class;
   $tpl->sourceDir = './templates/';
   $tpl->compileDir = './templates_c/';
   $tpl->stripWhitespaces = false;
   $tpl->setup();

   // Tworzymy widok
   $view = new Opt_View('szablon.tpl');
   $view->pageTitle = 'Tytuł strony';

   $list = array();
   $stmt = $pdo->query('SELECT * FROM produkty');
   while($row = $stmt->fetch(PDO::FETCH_ASSOC))
   {
      $list[] = array(
         'name' => $row['nazwa'],
         'url' => 'produkt.php?id='.$row['id']
      );
   }
   $stmt->closeCursor();
   $view->products = $list;

   // Renderujemy
   $output = new Opt_Output_Http;
   $output->setContentType(Opt_Output_Http::HTML, 'utf-8');
   $output->render($view);
}
catch(Opt_Exception $exception)
{
   $handler = new Opt_ErrorHandler;
   $handler->display($exception);
}

Do raportowania błędów Open Power Template używa mechanizmu wyjątków, dlatego cały kod zawarty jest w bloku try...catch. Błędów nie musimy wyświetlać samodzielnie - do dyspozycji jest specjalna klasa Opt_ErrorHandler, która dodatkowo potrafi podawać różne informacje o błędzie, dzięki czemu łatwiej jest usunąć przyczynę. Sam skrypt składa się z trzech części. W pierwszej musimy skonfigurować bibliotekę, tworząc obiekt Opt_Class i ustawiając niektóre właściwości:

  1. sourceDir - katalog z szablonami źródłowymi (tj. ./templates/)
  2. compileDir - katalog ze skompilowanymi szablonami (tj. ./templates_c/)
  3. stripWhitespaces - czy usuwać ze źródeł niepotrzebne białe znaki, utrudniając analizę kodu wynikowego. Wartość false wyłącza usuwanie.

Po skonfigurowaniu wywołujemy metodę setup() i możemy zabrać się za tworzenie widoków. Obsługuje się je podobnie, jak w naszym edukacyjnym systemie z tą różnicą, że nie używamy tutaj metody fabrycznej, lecz samodzielnie tworzymy cały obiekt. W argumencie konstruktora podajemy nazwę szablonu, a później możemy do widoku przypisywać różne dane przy pomocy getterów i setterów. Przy okazji spójrzmy na linijki 23 do 33, gdzie wypełniana jest lista produktów dla sekcji. Lista jest zwykłą tablicą z elementami numerowanymi od zera, a każdy element jest tablicą asocjacyjną zawierającą kilka zmiennych.

Na samym końcu musimy szablon wykonać. W tym celu potrzebny jest nam jakiś system wyjścia, który zdecyduje, co zrobić z wynikiem. OPT dostarcza domyślnie dwóch takich systemów:

  1. Opt_Output_Return - zwraca kod jako wynik wykonania z powrotem do skryptu.
  2. Opt_Output_Http - wysyła kod do przeglądarki i udostępnia opcje zarządzania nagłówkami.

Skorzystamy z tego drugiego. Ma on dodatkową pomocniczą metodę setContentType(), która wysyła nagłówek Content-type z informacją dla przeglądarki, co dokładnie przysyłamy z serwera i ew. w jakim kodowaniu. Później wystarczy już tylko wywołać metodę render(), której podajemy za argument widok i to wszystko.

Sekcje[edytuj]

Sekcje to rodzaj inteligentnych pętli i służą do wyświetlania różnego rodzaju list. Zacznijmy od wyświetlenia prostej listy produktów:

<?xml version="1.0" ?>
<opt:root xmlns:opt="http://xml.invenzzia.org/opt">
<h1>Lista produktów</h1>

<table class="list">
 <thead>
   <tr>
     <td>#</td>
     <td>Nazwa</td>
     <td>Cena</th>
   </tr>
 </thead>
 <tbody>
   <opt:section name="products">
   <tr>
      <td>{$products.id}</td>
      <td>{$products.name}</td>
      <td>{$products.price}</td>
   </tr>
   </opt:section>
 </tbody>
</table>
</opt:root>

Aby utworzyć sekcję, stosujemy znacznik opt:section pokazany w linii 14. Jego zawartość określa wygląd pojedynczego elementu listy. Każda sekcja musi posiadać swoją własną nazwę; w szablonie wykorzystujemy ją do odwoływania się do zmiennych elementu: $products.id, zaś w skrypcie posłuży ona do przypisania danych listy. Zauważmy, że w szablonie nie ma żadnych informacji odnośnie szczegółów implementacyjnych. Jest to jedna z charakterystycznych cech sekcji i nie tylko; w założeniu twórca szablonów powinien skupić się jedynie na końcowym efekcie bez zajmowania się tym, jak do niego dojść. Przyjrzyjmy się zatem, jak wygenerować dane dla naszej listy. Ponownie skorzystamy z pomocy bazy danych z poprzedniego rozdziału, zatem należy pamiętać o wcześniejszym połączeniu się z nią poprzez PDO.

$view = new Opt_View('product_list.tpl');
$products = array();
$stmt = $pdo->query('SELECT id, nazwa, cena FROM produkty ORDER BY id');
while($row = $stmt->fetch(PDO::FETCH_ASSOC))
{
   // Dodajemy nowy element
   $products[] = array(
      'id' => $row['id'],
      'name' => $row['nazwa'],
      'price' => $row['cena']
   );
}
$stmt->closeCursor();
// Dodaj listę do widoku
$view->products = $products;

Jak widzimy, pojedynczy element listy to zwykła tablica asocjacyjna, w której klucze odpowiadają indeksom użytym w szablonie. Aby utworzyć listę, wystarczy wszystkie elementy zgrupować w kolejną tablicę i przekazać do widoku pod nazwą identyczną, jak nazwa sekcji. Po uruchomieniu OPT automatycznie rozwinie je w podaną nam listę.

W szablonie nie mamy żadnych informacji o tym czy dane naszej listy umieszczone są w tablicy czy nie. Skąd zatem OPT wie, jak po niej iterować? Odpowiadają za to tzw. formaty danych. Jest to pewien rodzaj przepisu informujący OPT, jak radzić sobie z określonym rodzajem elementów. Przeróbmy nasz skrypt tak, aby umieszczał elementy w obiekcie klasy SplDoublyLinkedList (lista dwukierunkowa) i nauczmy OPT z niego korzystać:

$view = new Opt_View('product_list.tpl');
$products = new SplDoublyLinkedList;
$stmt = $pdo->query('SELECT id, nazwa, cena FROM produkty ORDER BY id');
while($row = $stmt->fetch(PDO::FETCH_ASSOC))
{
   // Dodajemy nowy element
   $products->push(array(
      'id' => $row['id'],
      'name' => $row['nazwa'],
      'price' => $row['cena']
   ));
}
$stmt->closeCursor();
// Dodaj listę do widoku
$view->products = $products;
$view->setFormat('products', 'SplDatastructure');

Widzimy, że obiekt SplDoublyLinkedList to coś zupełnie innego niż tablica. Okazuje się, że poinformowanie o tym OPT to kwestia dodania jednej linijki (nr 16). Metoda setFormat() ustawia format danych dla określonego elementu. SplDatastructure to format dostosowany do pracy z listami, stosami i kolejkami wchodzącymi w skład znanego nam już pakietu SPL.

Jeśli sekcja obejmuje pojedynczy znacznik HTML, nie musimy tworzyć całego znacznika, ponieważ mamy do dyspozycji skróconą formę atrybutową:

<tr opt:section="products">
   <td>{$products.id}</td>
   <td>{$products.name}</td>
   <td>{$products.price}</td>
</tr>

A co zrobić, gdy chcielibyśmy wyświetlić alternatywny tekst w razie otrzymania pustej listy? Pomoże nam znacznik opt:show:

<?xml version="1.0" ?>
<opt:root xmlns:opt="http://xml.invenzzia.org/opt">
<h1>Lista produktów</h1>
<opt:show name="products">
<table class="list">
 <thead>
   <tr>
     <td>#</td>
     <td>Nazwa</td>
     <td>Cena</th>
   </tr>
 </thead>
 <tbody>
   <opt:section>
   <tr>
      <td>{$products.id}</td>
      <td>{$products.name}</td>
      <td>{$products.price}</td>
   </tr>
   </opt:section>
 </tbody>
</table>
<opt:showelse>
 <p>Przykro nam, ale nie dodano jeszcze żadnych produktów.</p>
</opt:showelse>
</opt:show>
</opt:root>

Zauważmy, że w tym przypadku nazwa sekcji i inne ewentualne atrybuty trafiają do znacznika opt:show, pozostawiając opt:section pusty. Należy pamiętać o tej subtelności, ponieważ w przeciwnym razie efekty mogą być odmienne od zamierzonych.

Jednak prawdziwa elastyczność sekcji pojawia się dopiero przy próbie utworzenia list zagnieżdżonych. Pamiętamy, że w PHP i Savancie musieliśmy zajmować się tym samodzielnie, a wszelkie zmiany w strukturze danych generowanych przez aplikację zmuszały nas do przepisywania szablonów. W OPT wszystkimi detalami zajmują się formaty danych, my natomiast musimy jedynie umieścić jedną sekcję w drugiej i to wszystko. Wykorzystajmy tę właściwość do wypisania listy tagów skojarzonych z każdym produktem.

<?xml version="1.0" ?>
<opt:root xmlns:opt="http://xml.invenzzia.org/opt">
<h1>Lista produktów</h1>
<opt:show name="products">
<table class="list">
 <thead>
   <tr>
     <td>#</td>
     <td>Nazwa</td>
     <td>Cena</th>
   </tr>
 </thead>
 <tbody>
   <opt:section>
   <tr>
      <td>{$products.id}</td>
      <td><span class="name">{$products.name}</span>
        <span class="tags"><opt:section name="tags" str:separator=", ">
         <a parse:href="$tags.url">{$tags.name}</a>
        <opt:section></span>
      </td>
      <td>{$products.price}</td>
   </tr>
   </opt:section>
 </tbody>
</table>
</opt:show>
</opt:root>

Utworzenie sekcji zagnieżdżonej nie wymaga od nas absolutnie żadnej dodatkowej czynności oprócz osadzenie jednej sekcji w drugiej. Przy okazji przykład pokazuje inną ciekawą właściwość. Chcielibyśmy, aby nasze tagi były odseparowane przecinkiem, przy czym po ostatnim ma go już nie być. W tym celu dodajemy do sekcji atrybut str:separator. Przestrzeń nazw str informuje OPT, że wartość atrybutu to zwykły tekst, ponieważ domyślnie kompilator oczekuje wczytywania jego kształtu ze zmiennej lub innego wyrażenia.

Cały proces składania odbywać się będzie po stronie skryptu. Jednak ponieważ nasza baza danych nie zawiera tagów, zapiszemy je na sztywno w kodzie, a stworzenie dynamicznej listy tagów dla każdego produktu pozostawiamy jako ćwiczenie.

$view = new Opt_View('product_list.tpl');
$products = array();
$stmt = $pdo->query('SELECT id, nazwa, cena FROM produkty ORDER BY id');
while($row = $stmt->fetch(PDO::FETCH_ASSOC))
{
   // Dodajemy nowy element
   $products[] = array(
      'id' => $row['id'],
      'name' => $row['nazwa'],
      'price' => $row['cena'],
      // tagi dla tego produktu.
      'tags' => array(0 =>
         array('url' => '/tag1', 'name' => 'tag1'),
         array('url' => '/tag2', 'name' => 'tag2'),
         array('url' => '/tag3', 'name' => 'tag3'),
      );
   );
}
$stmt->closeCursor();
// Dodaj listę do widoku
$view->products = $products;
$view->setFormat('tags', 'SingleArray');

Ponieważ lista tagów jest zapisana dla każdego produktu z osobna, musimy poinformować o tym OPT, wybierając dla sekcji tags format danych SingleArray. Zachowanie domyślnego formatu jest nieco inne i wymaga od nas, aby sekcja tags posiadała swoją własną tablicę, lecz z podwójnym indeksowaniem (pierwsze - produkty; drugie - tagi konkretnego produktu).

Sekcje to bardzo wygodne narzędzie do wyświetlania wszekiego rodzaju list. Oprócz opt:section istnieją też trzy inne rodzaje sekcji:

  1. opt:selector - pozwala zdefiniować kilka różnych możliwych wyglądów dla elementów, które są wybierane na podstawie ich typu.
  2. opt:grid - wyświetlanie elementów w kolumnach z obsługą dopełniania ostatniego wiersza pustymi elementami.
  3. opt:tree - wyświetlanie drzew o dowolnej głębokości.

Wrócimy do nich w kolejnych rozdziałach.

Bloki i sortowanie listy produktów[edytuj]

W panelach administracyjnych często spotyka się możliwość sortowania listy poprzez klikanie na nagłówkach kolumn. Choć Open Power Template nie dostarcza nam gotowego rozwiązania, udostępnia narzędzia, które umożliwią nam łatwą jego implementację. Są to tzw. bloki. Każdy blok składa się zawsze z dwóch elementów:

  1. Obiektu pewnej klasy PHP implementującej interfejs Opt_Block_Interface. Nazywa się on obiektem bloku.
  2. Grupy znaczników w szablonie, w których dany obiekt będzie uruchamiany. Nazywa się ona portem bloku.

Zaczniemy od umieszczenia w naszym szablonie stosownego portu:

<?xml version="1.0" ?>
<opt:root xmlns:opt="http://xml.invenzzia.org/opt">
<h1>Lista produktów</h1>
<opt:show name="products">
<table class="list">
 <thead>
   <tr>
     <td><opt:sort-list str:name="products:id" str:selected="sel">#</opt:sort-list></td>
     <td><opt:sort-list str:name="products:name" str:selected="sel">Nazwa</opt:sort-list></td>
     <td><opt:sort-list str:name="products:price" str:selected="sel">Cena</opt:sort-list></th>
   </tr>
 </thead>
 <!-- treść tabeli -->
</table>
</opt:show>
</opt:root>

Jak widać, port jest zwykłym znacznikiem. Jego nazwę wybraliśmy sami i niebawem poinformujemy OPT, że ma on go skojarzyć z odpowiednim typem bloków. Do portu możemy przekazać dowolną liczbę argumentów. U nas definiują one etykiety, po których rozpoznamy, jaką kolumnę sortujemy, a także klasy CSS, jakie należy użyć np. gdy dana kolumna jest wybrana.

Powyższy rodzaj portów to tzw. porty statyczne. OPT podczas wykonywania automatycznie utworzy dla nich odpowiedni obiekt danego typu. Oprócz tego, istnieją też porty dynamiczne, w których obiekt wczytywany jest ze zmiennej. Oznacza to, że możemy utworzyć taki obiekt po stronie skryptu, skonfigurować go tam, a następnie pchnąć do szablonu, gdzie zostanie odpalony:

<opt:block from="$obiektBloku" argument="wartość">
  ... treść ...
</opt:block>

Zanim zaczniemy implementować Opt_Block_Interface, napiszemy sobie interfejs, który pozwoli skryptowi na skonfigurowanie kolumn, po których będziemy sortować:

<?php
class Sorter
{
   const ASC = 0;
   const DESC = 1;

   private $_columns = array();
   private $_default = null;
   private $_defaultOrder = 0;

   private $_selected;
   private $_order;
   private $_url;

   static private $_sorters = array();

   public function __construct($name, $url)
   {
      $this->_sorters[$name] = $this;
      $this->_url = $url;
   } // end __construct();

   static public function get($name)
   {
      if(!isset(self::$_sorters[$name]))
      {
         throw new RuntimeException('Podany zestaw reguł sortowania: '.$name.' nie istnieje.');
      }
      return self::$_sorters[$name];
   } // end get();

   public function addColumn($id, $dbField)
   {
      $this->_columns[$id] = $dbField;
      if($this->_default === null)
      {
         $this->_default = $id;
      }
   } // end addColumn();

   public function setDefault($id, $order)
   {
      $this->_default = (string)$id;
      $this->_defaultOrder = (int)$order;
   } // end setDefault();

   public function process()
   {
      $this->_selected = $this->_default;
      // Pobierz z adresu URL informację o kolumnie, po której sortujemy.
      if(isset($_GET['col']))
      {
         if(isset($this->_columns[$_GET['col']))
         {
            $this->_selected = $_GET['col'];
         }
      }
      // Pobierz informację o kierunku sortowania
      if(isset($_GET['ord']))
      {
         if($_GET['ord'] == 0 || $_GET['ord'] == 1)
         {
            $this->_order = $_GET['ord'];
         }
      }
      // Zwróć kawałek zapytania SQL
      return $this->_columns[$this->_selected].' '.($this->_order == 0 ? 'ASC' : 'DESC');
   } // end process();

   public function isSelected($id)
   {
      if($this->_selected != $id)
      {
         // ten element nie został wybrany.
         return null;
      }
      return $this->_order;
   } // end isSelected();

   public function getUrl()
   {
      return $this->_url;
   } // end getUrl();
} // end Sorting;

Opis metod jest następujący:

  1. __construct() - tworzy nowy zestaw reguł sortowania.
  2. addColumn() - dodaje informację o nowej kolumnie. Pierwszy argument to nasz identyfikator, drugi - nazwa kolumny w zapytaniu SQL
  3. setDefault() - ustawia domyślne sortowanie.
  4. process() - wczytuje z adresu URL informacje o aktualnym sortowaniu i generuje kawałek zapytania SQL.
  5. isSelected() - metoda ta będzie używana przez nasz obiekt bloku do rozpoznania czy aktualna kolumna jest wybrana.
  6. get() - metoda statyczna zwracająca określony zestaw reguł na potrzeby obiektu bloku.

Teraz pora na klasę implementującą Opt_Block_Interface. Musimy w niej zaimplementować trzy metody:

  1. onOpen($attributes) - wywoływana w momencie otwarcia znacznika portu. Powinna zwrócić true, jeśli chcemy wyświetlić zawartość bloku.
  2. onClose() - wywoływana w momencie zamykania znacznika bloku.
  3. onSingle($attributes) - wywoływana, gdy mamy do czynienia z portem w znaczniku pojedynczym: <znacznik />.

Kod źródłowy:

<?php
class Sorter_Block implements Opt_Block_Interface
{
   private $_order;

   public function onOpen(array $attributes)
   {
      $data = explode(':', $attributes['name']);
      if(sizeof($data) != 2)
      {
         throw new DomainException('Nieprawidłowa nazwa bloku.');
      }
      $sorter = Sorter::get($data[0]);

      // Sprawdź czy sortujemy według tej kolumny
      $this->_order = $sorter->isSelected($data[1]);
      $url = $sorter->getUrl();
      $url .= (strpos($url, '?') !== null ? '?' : '&');

      // Dodaj trochę CSS-a i wygeneruj kod HTML
      $class = (isset($attributes['class']) ? $attributes['class'] : null);
      $selected = (isset($attributes['selected']) ? $attributes['selected'] : null);

      if($this->_order != null)
      {
         echo '<a href="'.$url.'col='.$data[1].'&ord='.(int)(!$this->_order).'" '.($selected !== null ? 'class="'.$selected.'" : '').'>';
      }
      else
      {
         echo '<a href="'.$url.'col='.$data[1].'&ord=0" '.($class !== null ? 'class="'.$class.'" : '').'>';
      }
      return true;
   } // end onOpen();

   public function onClose()
   {
      if($this->_order !== null)
      {
         if($this->_order == 0)
         {
            echo '↓</a>';
         }
         else
         {
            echo '↑</a>';
         }
      }
      else
      {
         echo '</a>';
      }
   } // end onClose();

   public function onSingle(array $arguments)
   {
      /* pusto */
   } // end onSingle();
} // end Sorter_Block;

Nasz mechanizm sortowania będzie przekazywać informacje o aktualnym sortowaniu za pośrednictwem dodatkowych argumentów w adresie URL. Dlatego nasz blok będzie generować wokół nazwy kolumny znacznik <a>, który po kliknięciu spowoduje posortowanie elementów według danej kolumny, a w przypadku już wybranej - odwróci kolejność sortowania. Oczywiście wybraną kolumnę należy wyróżnić odpowiednią klasą CSS oraz dodatkową strzałką pokazującą kierunek sortowania.

Zauważmy, że OPT nie zabrania nam generowania HTML-a przez kod PHP. Oczywiście generowanie w ten sposób większego kawałka kodu mijałoby się z celem i byłoby wyjątkowo nieczytelne, ale dla pojedynczych znaczników nie ma żadnych przeciwwskazań przeciwko programowaniu w ten sposób.

Pora na podłączenie naszego systemu sortowania pod listę. Dla zachowania czytelności pozbyliśmy się obsługi tagów:

$view = new Opt_View('product_list.tpl');

$sorter = new Sorter('products', 'product_list.php');
$sorter->addColumn('id', '`id`');
$sorter->addColumn('name', '`nazwa`');
$sorter->addColumn('price', '`cena`');

$products = array();
$stmt = $pdo->query('SELECT id, nazwa, cena FROM produkty ORDER BY '.$sorter->process());
while($row = $stmt->fetch(PDO::FETCH_ASSOC))
{
   // Dodajemy nowy element
   $products[] = array(
      'id' => $row['id'],
      'name' => $row['nazwa'],
      'price' => $row['cena'],
   );
}
$stmt->closeCursor();
// Dodaj listę do widoku
$view->products = $products;

Wygląda na to, że to już wszystko. Reguły sortowania są ustawione, odpowiedni kawałek zapytania SQL generowany, w szablonie oznaczone kolumny... jednak po uruchomieniu nagłówki kolumn znikają. Oczywiście - zapomnieliśmy poinformować OPT, że opt:sort-list jest blokiem. W przypadku takich nieznanych znaczników kompilator po prostu je ignoruje wraz z zawartością, stąd zniknięcie tekstu w nagłówkach. Musimy zarejestrować naszą klasę Sorter_Block w OPT, dlatego w momencie inicjowania biblioteki dodajemy:

$tpl = new Opt_Class;
$tpl->sourceDir = './templates/';
$tpl->compileDir = './templates_c/';
$tpl->stripWhitespaces = false;
$tpl->register(Opt_Class::OPT_BLOCK, 'opt:sort-list', 'Sorter_Block');
$tpl->setup();

Po ponownym skompilowaniu szablonów nasza lista będzie już mogła być sortowana po nagłówkach kolumn.

Przekonaliśmy się właśnie, że kluczem do efektywnego pisania szablonów jest przygotowanie sobie odpowiedniego zaplecza, bowiem większość napisanego przed chwilą kodu to rozmaite klasy PHP. Moglibyśmy osiągnąć identyczny efekt przy pomocy pętli oraz instrukcji warunkowych bezpośrednio w szablonie, ale przecież w prawdziwej aplikacji nie mamy jednej listy, tylko co najmniej kilkanaście. Dlatego tak ważne jest, aby poświęcić chwilkę czasu na zaprogramowanie obsługi takich fragmentów, jak sortowanie czy stronicowanie oraz aby system szablonów udostępniał odpowiednie narzędzia do ich osadzania w szablonach. W przypadku OPT są to bloki oraz działające na podobnej zasadzie komponenty, które mają jednak dużo bardziej rozbudowany interfejs dostosowany do wyświetlania formularzy. Zauważmy, że gdy przyjdzie nam tworzyć np. listę użytkowników, w szablonie będzie to już kwestia dodania dodatkowego znacznika w nagłówku każdej kolumny i nic więcej!

Zaawansowane sekcje i stronicowanie[edytuj]

Upiększanie listy produktów[edytuj]

Zakończenie[edytuj]

W rozdziale tym pokazaliśmy, jak zbudować i obsłużyć szablon dla dynamicznej listy produktów przy pomocy biblioteki Open Power Template. Jest to jednak tylko wycinek jej możliwości, ponieważ jest ona projektowana, by poradzić sobie nawet z najtrudniejszymi wymaganiami. Filozofia tworzenia szablonów jest tutaj zupełnie inna, niż w poznanym wcześniej Savancie, dlatego nie powinniśmy tutaj polegać na swojej intuicji i przenosić tego, co funkcjonowało w PHP z nadzieją, że zadziała i tutaj. Zauważmy, że w podanych przykładach nie użyliśmy po stronie szablonów ani klasycznej pętli, ani zwykłej instrukcji warunkowej, chociaż oczywiście OPT udostępnia takie instrukcje, jak opt:if czy opt:foreach.

Następny i zarazem ostatni system szablonów, jaki poznamy, to PHPTAL. Jest on ogniwem pośrednim między OPT, a Savantem, z nowoczesnym językiem szablonów, który jednak jest nieco bardziej zbliżony do klasycznego programowania.