PHP/Prosty edukacyjny system szablonów

Z Wikibooks, biblioteki wolnych podręczników.
< PHP

Prosty edukacyjny system szablonów[edytuj]

Celem tego rozdziału jest zbudowanie prostego systemu szablonów, aby zapoznać się w praktyce z zasadami działania tego typu bibliotek. Nasz system opierać się będzie na języku PHP, gdyż budowa odpowiedniego kompilatora to temat, którym można by zapełnić całą książkę i który dość mocno odbiega od właściwego zagadnienia.

Projekt[edytuj]

Zanim zaczniemy cokolwiek pisać, zastanówmy się, co system szablonów musi robić. W poprzednim rozdziale powiedzieliśmy, że jest to warstwa pośrednia między resztą aplikacji, a przeglądarką: musi odebrać dane z warstwy logiki biznesowej oraz na podstawie odpowiedniego szablonu wygenerować kod, który zostanie posłany do przeglądarki. Stąd znamy już pierwszą funkcjonalność: możliwość przekazania danych ze skryptu do szablonu.

Szablon jest plikiem, który musi być zlokalizowany gdzieś na dysku. Aplikacja WWW, przekazując dane, musi także wybrać określony szablon. Potrzebna jest nam zatem jakaś prosta konfiguracja, w której programista będzie mógł poinformować, gdzie na dysku zlokalizowane są szablony oraz ustawić ewentualne inne opcje. Na końcu warto pomyśleć o udostępnieniu jakichś helperów, które pozwolą twórcy szablonu skupić się na merytorycznej treści szablonu, zamiast na technicznych szczegółach każdego możliwego zadania.

Nasz projekt napiszemy w oparciu o programowanie obiektowe. Poniżej przedstawiona jest lista klas:

  1. Wikibooks\Tpl\Engine - główna klasa zarządzająca konfiguracją.
  2. Wikibooks\Tpl\View - klasa reprezentująca szablon z przypisanymi do niego danymi i umożliwiająca jego wykonanie, który na nasze potrzeby nazwiemy sobie widokiem.
  3. Wikibooks\Tpl\Helper - klasa z helperami.

Stosujemy się tutaj do standardu nazewnictwa i lokowania klas w plikach omówionego w rozdziale Automatyczne ładowanie.

Klasa Wikibooks\Tpl\Engine[edytuj]

Pierwsza z klas nie będzie zbyt rozbudowana - jedynie, co będzie robić, to przechowywać konfigurację oraz służyć za fabrykę widoków.

<?php
namespace Wikibooks\Tpl;

class Engine
{
private $_templateDir;
private $_extension;
	
public function __construct($templateDir, $extension = 'php')
{
	$this->setTemplateDir($templateDir);
	$this->setExtension($extension);
} // end __construct();
	
public function setTemplateDir($dir)
{
	if(!is_dir($dir))
	{
	   throw new RuntimeException('Podany katalog szablonów '.$dir.' jest niedostępny.');
	}
	   if(strlen($dir) > 0 && $dir[strlen($dir) - 1] != '/')
	{
	$dir .= '/';
	}
	$this->_templateDir = $dir;
} // end setTemplateDir();
	
public function getTemplateDir()
{
 return $this->_templateDir;
} // end getTemplateDir();
	
public function setExtension($extension)
{
	if(!ctype_alnum($extension))
	{
	   throw new DomainException('Nazwa rozszerzenia może zawierać wyłącznie litery i cyfry.');
	}
	$this->_extension = $extension;
} // end setExtension();

public function getExtension()
{
	return $this->_extension;
} // end getExtension();

public function createView($viewName)
{
	return new View($viewName, $this->_templateDir.$viewName.'.'.$this->_extension);
} // end createView();
} // end Engine;

W tej klasie mamy cztery metody do zarządzania konfiguracją. setTemplateDir() pozwala ustawić programiście katalog, w którym przechowywane są szablony, zaś setExtension() pozwala wybrać domyślne rozszerzenie plików. Towarzyszą im odpowiednie gettery. Zgodnie z regułami programowania obiektowego nie upychamy całej funkcjonalności w jednej klasie, lecz rozdzielamy zadania między kilka mniejszych. Dlatego w tym miejscu jedyna rzecz, która ma coś więcej wspólnego z szablonami, to metoda createView() służąca za fabrykę widoków. W imieniu konkretnego widoku buduje mu odpowiednią ścieżkę do pliku, bazując na swojej konfiguracji.

Klasa Wikibooks\Tpl\View[edytuj]

Klasa View reprezentować będzie pojedynczy szablon oraz przypisane do niego dane z warstwy logiki. Jeśli chcemy skomponować wyjście HTML z kilku mniejszych szablonów, np. aby mieć wspólny szablon z nagłówkiem i stopką strony, powinniśmy utworzyć dla każdego z nich odpowiednie widoki.

<?php
namespace Wikibooks\Tpl;

class View
{
	private $_template;
	private $_path;
	private $_data = array();

	public function __construct($template, $path)
	{
		$this->_template = $template;
		
		if(!file_exists($path))
		{
			throw new RuntimeException('Określony szablon '.$template.' nie istnieje.');
		}
		$this->_path = $path;
	} // end __construct();
	
	public function getTemplate()
	{
		return $this->_template;
	} // end getTemplate();
	
	public function __set($name, $value)
	{
		$this->_data[$name] = $value;
	} // end __set();
	
	public function __get($name)
	{
		return $this->_data;
	} // end __get();

	public function render()
	{
		extract($this->_data);
		require($this->_path);
	} // end render();
} // end View;

Każdy widok przechowuje informacje o szablonie, jaki reprezentuje, a także o danych, jakie już zdążyliśmy do niego przypisać. Służy do tego tablica $_data, a obsługiwana jest poprzez metody magiczne __set() i __get(). Gdy chcemy wyświetlić szablon, korzystamy z metody render(). Pojawia się w niej wywołanie funkcji extract(). Rozpakowuje ona zawartość tablicy jako zmienne, czyli np. z elementu foo powstanie nam zmienna $foo. Ułatwi nam to odwoływanie się do danych skryptu w szablonie. Ostatnia rzecz to operacja require i wykonanie szablonu. Jak widać, nie ma tu nic skomplikowanego.

Klasa Wikibooks\Tpl\Helpers[edytuj]

Na koniec pozostało nam napisać kilka helperów. Będą one zapakowane jako metody statyczne w klasie Helpers:

<?php
namespace Wikibooks\Tpl;

class Helpers
{
	static public function linkTo($url, $title)
	{
		return '<a href="'.htmlspecialchars($url).'">'.$title.'</a>';
	} // end linkTo();
	
	static public function pluralPl($number, $singular, $plural1, $plural2)
	{
		if($number == 1)
		{
			return $singular;
		}
		elseif($number > 1 && $number < 5)
		{
			return $plural1;
		}
		return $plural2;
	} // end pluralPl();
} // end Helpers;

Omówienie:

  1. linkTo() - generuje odnośnik.
  2. pluralPl() - pozwala w prosty sposób utworzyć liczbę mnogą (aby móc wyświetlać np. "1 produkt"/"2 produkty").

Oczywiście jest to tylko zalążek - duże projekty korzystają z dziesiątek różnych helperów, np. do generowania formularzy, nawigacji i wielu innych elementów. Zauważmy, że nawet te, które podaliśmy, można by rozbudować. Przykładowo, do linkTo() moglibyśmy dorobić metodę, która pozwoli na automatyczne dodanie odpowiedniej klasy CSS wszystkim odnośnikom tworzonym za jej pomocą. Pozostawiamy to jako ćwiczenie.

Użycie[edytuj]

Jak widać, nasz prosty system szablonów nie był zbyt trudny do napisania. Rzeczywiście, podstawowe mechanizmy stojące za tego typu skryptami nie są zbyt wyszukane i bazują na jedynie kilku elementach języka. Oczywiście jeśli chcielibyśmy dodać więcej funkcjonalności, stopień zaawansowania kodu znacznie by się skomplikował, ale nie jest to celem tego rozdziału.

Przyjrzyjmy się teraz, jak korzystać z naszego systemu. Na początku napiszemy szablon, który umieścimy w katalogu /templates:

<?php use \Wikibooks\Tpl\Helpers as Helpers; ?>
<html>
 <head>
  <title>Mój pierwszy szablon</title>
 </head>
 <body>
  <h1>Witaj,</h1>
  <p>Witaj, <?php echo $name; ?></p>
  <p>Aktualnie na stronie <?php echo Helpers::pluralPl($userNum, 'jest 1 użytkownik', <br />
'są '.$userNum.' użytkownicy', 'jest '.$userNum.' użytkowników'); ?>.</p>

  <?php if(is_array($products) && sizeof($products) > 0){ ?>
  <p>Co chciałbyś dziś kupić?</p>
  <ul>
  <?php foreach($products as $product){ ?>
    <li><?php echo $product['nazwa']; ?></li>
  <?php } ?>
  </ul>
  <p><?php echo Helpers::linkTo('zamowienie.php', 'Złóż zamówienie!'); ?></p>
  <?php }else{ ?>
  <p>Niestety nie mamy żadnych produktów do zaoferowania.</p>
  <?php } ?>
 </body>
</html>

W pierwszej linijce musieliśmy umieścić import klasy helperów z przestrzeni nazw, aby skrócić jej nazwę. Poza tym w pliku mamy niemal wyłącznie kod HTML z paroma wstawkami PHP. Widzimy, że wszystkie dane ze skryptu: $name, $userNum oraz $products zostały rozpakowane do postaci zmiennych. Helpery umożliwiły nam skrócenie niektórych typowych operacji. Do wyświetlenia listy produktów użyliśmy pętli foreach obudowanej warunkiem sprawdzającym czy nie jest ona pusta.

A oto i skrypt korzystający z tego szablonu. Dane pobieramy z tabeli produkty, którą poznaliśmy w rozdziale o bazach danych. Jeśli jej nie masz, skocz tam i zainstaluj ją. Zakładamy także, że w pliku SplClassLoader.php znajduje się automatyczna ładowarka klas SPL omówiona w rozdziale Automatyczne ładowanie.

<?php
require('./SplClassLoader.php');
$loader = new SplClassLoader('Wikibooks', './');
$loader->register();
header('Content-type: text/html;charset=utf-8');
try
{
	// Inicjujemy, co trzeba
	$pdo = new PDO('mysql:host=localhost;dbname=test', 'root', 'root');
	$pdo -> setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

	$tpl = new \Wikibooks\Tpl\Engine('./templates/');
	

	// Warstwa logiki aplikacji - pobieranie danych	
	$productList = array();
	$stmt = $pdo->query('SELECT * FROM produkty');
	while($row = $stmt->fetch(PDO::FETCH_ASSOC))
	{
		$productList[] = $row;
	}
	$stmt->closeCursor();
	
	// Przekazujemy dane do warstwy prezentacji
	$view = $tpl->createView('szablon');
	$view->products = $productList;	
	$view->name = 'Adam Kowalski';
	$view->userNum = rand(0, 10);
	
	// Wyświetlamy wszystko
	$view->render();
}
catch(Exception $exception)
{
	die('Wystąpił błąd: '.$exception->getMessage());
}

W naszym skrypcie znalazły się tylko instrukcje dotyczące przetwarzania danych. Jest on o wiele czytelniejszy niż gdyby ten sam kod HTML próbować umieścić tutaj. Możemy skupić się na samym procesie przetwarzania, bez zajmowania się wyświetlaniem. Jedyne co robimy, to kierujemy wszystko, co wygenerowaliśmy, do systemu szablonów.

Czy warto tworzyć własny system?[edytuj]

Przedstawiony powyżej system szablonów jest bardzo prymitywny i z pewnością nie nadaje się do żadnych poważniejszych zastosowań. Programiści często zadają pytania czy lepiej użyć gotowej biblioteki czy też napisać własną. Za argument często podawana jest "zbyt duża funkcjonalność" gotowych rozwiązań, która budzi pewne przerażenie. Zwłaszcza początkujący mają tendencję do twierdzenia, że jest im ona niepotrzebna. Tymczasem rzeczywistość ma się zupełnie inaczej. Patrząc na artykuły w sieci oraz nawet na ten rozdział łatwo dojść do wniosku, że jedyne, co nam potrzeba do szczęścia, to if i foreach, jednak nic bardziej mylnego! Już na typowym internetowym blogu pojawia się wiele elementów, których wyrażanie za pomocą tych dwóch konstrukcji jest co najmniej męczące. Mamy stronicowanie, wszelkiego rodzaju elementy nawigacyjne, a ilość zależności "jeśli X, wyświetl Y" jest bardzo duża. Bazując jedynie na najprymitywniejszych elementach tracimy mnóstwo czasu na wymyślanie koła od zera, podczas gdy tzw. "gotowe" systemy szablonów oferują od razu gotowe, sprawdzone rozwiązania. Twierdzenie, że wystarczy mi podstawowa funkcjonalność to mit, na którym przejechało się już wiele osób. Nawet jeśli nie wykorzystamy wszystkiego, o wiele lepiej mieć w zanadrzu odpowiednie narzędzia, niż zorientować się, że klient oczekuje od nas czegoś, a my nie możemy mu tego dać, ponieważ nasze biblioteki są zbyt prymitywne. Jeśli chodzi o wydajność, w przypadku systemów szablonów nie do końca sprawdza się zasada większy znaczy wolniejszy. Jak wspomnieliśmy, wiele systemów szablonów to w rzeczywistości typowe API do przekazywania danych ze skryptu oraz kompilator, który jest ładowany tylko wtedy, gdy jest potrzebny. Duża funkcjonalność jest uzyskiwana praktycznie zerowym kosztem, a główna różnica wydajnościowa między takim prostym systemem, a gotowym pakietem będzie wynikać z różnych czasów ładowania samej biblioteki przez interpreter.

System szablonów warto napisać samodzielnie jedynie w dwóch sytuacjach:

  1. W celach edukacyjnych tak, jak to zrobiliśmy powyżej.
  2. Gdy faktycznie mamy ciekawy pomysł na system szablonów i chcemy go realizować.

Przy czym zadanie drugie jest znacznie trudniejsze zwłaszcza, gdy będziemy chcieli napisać własny kompilator. W większości przypadków skończy się to jedynie na udostępnieniu małego podzbioru PHP w postaci pętli i prostych warunków, który przy pierwszym poważniejszym zadaniu będzie bardziej ograniczał, niż pomagał. Najlepsze systemy szablonów powstawały latami rozwijane przez doświadczonych programistów i szansa, że zrobimy coś choć w połowie tak funkcjonalnego "z marszu", jest minimalna.

Zakończenie[edytuj]

Poznaliśmy już, jak systemy szablonów działają w praktyce i jak są zbudowane, a także dowiedzieliśmy się, dlaczego w rzeczywistych projektach nie warto jest tworzyć ich samodzielnie. Pora zatem poznać kilka gotowych systemów.