PHP/Open Power Template: Różnice pomiędzy wersjami

Usunięta treść Dodana treść
Zyx (dyskusja | edycje)
dalsze prace nad rozdziałem
Zyx (dyskusja | edycje)
dalsza rozbudowa
Linia 64:
 
<source lang="xml" line><?xml version="1.0" ?>
<opt:root xmlns:opt="http://xml.invenzzia.org/opt">
<opt:root>
<opt:prolog />
<opt:dtd template="html5" />
Linia 155:
=== Sekcje ===
 
Sekcje to rodzaj inteligentnych pętli i służą do wyświetlania różnego rodzaju list. Zacznijmy od wyświetlenia prostej listy produktów:
=== Szkielet aplikacji ===
 
<source lang="xml" line highlight="14"><?xml version="1.0" ?>
=== Zarządzanie kodem HTML ===
<opt:root xmlns:opt="http://xml.invenzzia.org/opt">
<h1>Lista produktów</h1>
 
<table class="list">
=== Snippety ===
<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>
</source>
 
Aby utworzyć sekcję, stosujemy znacznik <code>opt:section</code> 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: <code>$products.id</code>, 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.
=== Komponenty ===
 
<source lang="php" line>$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;
</source>
 
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ę.
 
{{Uwaga|Domyślnie OPT wymaga, aby indeksy listy były numerowane od zera i nie było w nich luk, zatem nie używaj pola ''id'' z bazy danych do indeksowania!}}
 
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ć:
 
<source lang="php" line highlight="16">$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');
</source>
 
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 <code>setFormat()</code> 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.
 
{{Uwaga|Zmiana formatu danych dowolnego elementu wymaga przekompilowania szablonu. Niestety OPT nie rozpoznaje tego automatycznie, dlatego w tym celu musisz wejść do katalogu <code>/templates_c</code> i usunąć plik odpowiadający naszemu szablonowi <code>product_list.tpl</code>. Nie przeszkadza to jednak w codziennym użytkowaniu, ponieważ formaty danych wystarczy ustawić raz dla całej aplikacji.}}
 
Jeśli sekcja obejmuje pojedynczy znacznik HTML, nie musimy tworzyć całego znacznika, ponieważ mamy do dyspozycji skróconą formę atrybutową:
 
<source lang="xml" line highlight="14"><tr opt:section="products">
<td>{$products.id}</td>
<td>{$products.name}</td>
<td>{$products.price}</td>
</tr>
</source>
 
A co zrobić, gdy chcielibyśmy wyświetlić alternatywny tekst w razie otrzymania pustej listy? Pomoże nam znacznik <code>opt:show</code>:
 
<source lang="xml" line highlight="4"><?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>
</source>
 
Zauważmy, że w tym przypadku nazwa sekcji i inne ewentualne atrybuty trafiają do znacznika <code>opt:show</code>, pozostawiając <code>opt:section</code> 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.
 
<source lang="xml" line highlight="18"><?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>
</source>
 
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 <code>str:separator</code>. 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.
 
<source lang="php" line>$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');
</source>
 
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 <code>opt:section</code> istnieją też trzy inne rodzaje sekcji:
 
# <code>opt:selector</code> - pozwala zdefiniować kilka różnych możliwych wyglądów dla elementów, które są wybierane na podstawie ich typu.
# <code>opt:grid</code> - wyświetlanie elementów w kolumnach z obsługą dopełniania ostatniego wiersza pustymi elementami.
# <code>opt:tree</code> - wyświetlanie drzew o dowolnej głębokości.
 
Wrócimy do nich w kolejnych rozdziałach.
 
=== Bloki i sortowanie listy produktów ===
 
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:
 
# Obiektu pewnej klasy PHP implementującej interfejs '''Opt_Block_Interface'''. Nazywa się on ''obiektem bloku''.
# 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:
 
<source lang="xml" line highlight="8"><?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>
</source>
 
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:
 
<source lang="xml"><opt:block from="$obiektBloku" argument="wartość">
... treść ...
</opt:block>
</source>
 
Zanim zaczniemy implementować '''Opt_Block_Interface''', napiszemy sobie interfejs, który pozwoli skryptowi na skonfigurowanie kolumn, po których będziemy sortować:
 
<source lang="php"><?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;
</source>
 
Opis metod jest następujący:
 
# <code>__construct()</code> - tworzy nowy zestaw reguł sortowania.
# <code>addColumn()</code> - dodaje informację o nowej kolumnie. Pierwszy argument to nasz identyfikator, drugi - nazwa kolumny w zapytaniu SQL
# <code>setDefault()</code> - ustawia domyślne sortowanie.
# <code>process()</code> - wczytuje z adresu URL informacje o aktualnym sortowaniu i generuje kawałek zapytania SQL.
# <code>isSelected()</code> - metoda ta będzie używana przez nasz obiekt bloku do rozpoznania czy aktualna kolumna jest wybrana.
# <code>get()</code> - 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:
 
# <code>onOpen($attributes)</code> - wywoływana w momencie otwarcia znacznika portu. Powinna zwrócić '''true''', jeśli chcemy wyświetlić zawartość bloku.
# <code>onClose()</code> - wywoływana w momencie zamykania znacznika bloku.
# <code>onSingle($attributes)</code> - wywoływana, gdy mamy do czynienia z portem w znaczniku pojedynczym: <code>&lt;znacznik /&gt;</code>.
 
Kod źródłowy:
 
<source lang="php" line><?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;
</source>
 
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 <code>&lt;a&gt;</code>, 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:
 
<source lang="php" line>$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;
</source>
 
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 <code>opt:sort-list</code> 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:
 
<source lang="php" line highlight="">$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();
</source>
 
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 ===
 
=== Upiększanie listy produktów ===
 
=== Zakończenie ===
 
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 <code>opt:if</code> czy <code>opt:foreach</code>.
 
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.