PHP: Obsługa szablonów (#2)
Pisałem już kiedyś o obsłudze szablonów zarówno za pomocą Smarty, jak i własnych skryptów. (Na marginesie dodam, że kontynuacja artykułów o Smarty będzie - nie zapomniałem ;) Dzisiaj chcę pokazać jak stworzyć własny system, który będzie konwertował składnię naszego szablonu na kod PHP, który następnie będzie mógł być uruchomiony.
Pisałem już kiedyś o obsłudze szablonów zarówno za pomocą Smarty, jak i własnych skryptów. (Na marginesie dodam, że kontynuacja artykułów o Smarty będzie - nie zapomniałem ;) Dzisiaj chcę pokazać jak stworzyć własny system, który będzie konwertował składnię naszego szablonu na kod PHP, który następnie będzie mógł być uruchomiony.
Tagi
Aby nasza klasa mogła przetwarzać kod, potrzebuje wzorców. My skorzystamy z rozwiązania najszybszego jakie może być - zapisania wzorców w tablicy. Zagwarantuje nam to najszybsze z możliwych działanie skryptu (przynajmniej w tym aspekcie).
Będziemy potrzebowali (nie jest to konieczne, ale może się przydać także w innych klasach) lepszej obsługi tablic. Użyjemy do tego małej klasy, którą będziecie mogli wzbogacić o własne funkcje. Oto jej kod:
<?php
/* class Arr
* @author Kacper Kołodziej
* @www http://kacperkolodziej.com
*/
class Arr {
/* dodajemy wartość do tablicy
*
* @param array $array (przekazywany przez referencję)
* @param string $value - wartość do zapisania w tablicy
* @param string $key - klucz; null => jako klucz podajemy kolejny indeks liczbowy
* @param bool $overwrite - czy nadpisywać istniejące klucze?
*/
public static function push(array &$array,$value,$key = null,$overwrite = true) {
if (empty($key)) {
$key = count($array);
}
if ($overwrite === true) {
$array[$key] = $value;
} else {
if (!isset($array[$key])) {
$array[$key] = $value;
}
}
}
/* wyświetlamy zawartość tablicy w tagach <pre>
*
* @param array $array - tablica do wyświetlenia
* @return string
*/
public static function show(array $array) {
return "<pre>" . print_r($array,true) . "</pre>";
}
}
?>
Wszystkie jej elementy dość dokładnie opisałem po polsku, więc nie powinno być żadnych problemów.
Teraz zabierzmy się za klasę obsługi szablonów.
<?php
/* class Template
* @author Kacper Kołodziej
* @www http://kacperkolodziej.com
*/
// ścieżka do katalogu z szablonami
define("TPL_PATH","/var/www/template_article/tpl/");
class Template {
// wartości zmiennych
private $_values = array();
// oryginalny kod szablonu
private $_template = null;
// skompilowany kod szablonu
private $_compiled = null;
// wykonany kod - widok
private $_view = null;
// nazwa pliku szablonu
private $_filename = null;
// rozszerzenie pliku szablonów
private $_extension = ".html";
// tablica tagów
private $_tags = array(
// instrukcje warunkowe
"{if (.*)}" => "<?php if ($1) { ?>",
"{else}" => "<?php } else { ?>",
"{elseif (.*)}" => "<?php } elseif ($1) { ?>",
"{/if}" => "<?php } ?>",
// pętla foreach
"{foreach (.*) as (.*) => (.*)}" => "<?php foreach ($1 as $2 => $3) { ?>",
"{foreach (.*) as (.*)}" => "<?php foreach ($1 as $2) { ?>",
"{/foreach}" => "<?php } ?>",
// załączanie plików
"{include (.*)}" => "<?php include('$1'); ?>",
"{include_once (.*)}" => "<?php include_once('$1'); ?>",
/* wyświetlanie:
* {$zmienna}
* {2003+2001}
* {"string"}
*/
"{(.*)}" => "<?php echo $1; ?>"
);
// konstruktor
public function __construct($filename,$ext = ".html") {
$this->_filename = $filename;
$this->_extension = $ext;
// pobieranie kodu szablonu do zmiennej _template
$this->_template = file_get_contents(TPL_PATH . $this->_filename . $this->_extension);
}
/* przekazuje zmienne użyte w szablonie do tablicy _values
* @param string $key - klucz - wynikowo nazwa zmiennej
* @param string $value - wartość
* @return $this (instance)
*/
public function assign($key, $value) {
// tutaj korzystamy z klasy Arr (dołączona do artykułu)
Arr::push($this->_values,$value,$key);
return $this;
}
/* kompiluje kod i zapisuje go do pliku phtml w odpowiednim katalogu
* @return $this (instance)
*/
private function _compile() {
$compile = $this->_template;
foreach ($this->_tags as $regexp => $replace) {
$compile = preg_replace("#" . $regexp . "#",$replace,$compile);
}
$this->_compiled = $compile;
// zapisywanie skompilowanego kodu do katalogu
file_put_contents(TPL_PATH . "c/" . $this->_filename . ".phtml",$this->_compiled);
return $this;
}
/* wykonuje skompilowany kod, zapisuje go do bufora, a następnie do zmiennej _view
* @return $this (instance)
*/
public function fetch() {
$this->_compile();
// uruchamiamy bufor, do którego zapiszemy stronę
ob_start();
foreach ($this->_values as $key => $value) {
$$key = $value;
}
include(TPL_PATH . "c/" . $this->_filename . ".phtml");
// przekazujemy wartość bufora, czyli skompilowany i wykonany kod do zmiennej prywatnej _view
$this->_view = ob_get_contents();
// czyścimy i zamykamy bufor
ob_end_clean();
return $this;
}
/* wyświetla skompilowany i wykonany kod
*/
public function display() {
print $this->fetch()->_view;
}
/* konwertowanie objektu do typu string
*/
public function __toString() {
return $this->fetch()->_view;
}
}
?>
Cały kod jest opatrzony komentarzami, więc nie powinno być jakichkolwiek problemów z jego zrozumieniem. Jak widzimy wyrażenia regularne w definicjach wzorców (w tablicy Template::_tags) użyłem bardzo ogólnego (.*). Jakie nakłada to na nas obowiązki? Właściwie tylko jeden, ale stosunkowo niewielki: w jednej linii może znajdować się tylko jedno wyrażenie korzystające ze składni. Ponieważ pomiędzy znakami { i } możemy zamieścić dużo różnych wartości (nawet ciągi string), to stworzenie odpowiednich wyrażeń regularnych jest bardzo trudne.
Stwórzmy więc przykładowy szablon (index.html):
<html>
<head>
<title>{$title}</title>
<meta http-equiv="content-type" content="text/html;charset=utf-8" />
</head>
<body>
<p>Twórca stroy nazywa się <b>{if isset($name)}
{$name}
{else}
{$kacper}
{/if}
</b></p>
<p>Za rok będzie {date("Y")+1}</p>
<p>Wynik działania: 15 + 16 * 2 = {15+16*2}</p>
<p>Wyświetlam tekst <b>Lorem ipsum</b>: {"Lorem ipusm"}</p>
</body>
</html>
i plik, który przetworzy szablon (index.php):
<?php
require "Arr.php";
require "Template.php";
$view = new Template("index");
$view->assign("kacper","Kołodziej")->assign("name","Kacper Kołodziej")->assign("title","Tytuł strony")->display();
?>
Jak widzimy, korzystając z szablonów, wszystko jest prostsze i bardziej przejrzyste.
Wykonany kod można zobaczyć tutaj.
Hmmm... osobiście jakoś nie bardzo podoba mi się pomysł mieszania PHP i szablonów w tak dowolny sposób. Jakoś bardziej przekonuje mnie rozwiązanie stosowane w PHPTAL.
Jak już pozwalasz na tak szerokie użycie PHP, to po prostu pozwól go używać, a nie rób dodatkowy narzut w postaci stringów do zastępowania.
I jeszcze jedno: nie lepiej pozwolić przekazywać do metody assign tablic?
Jeśli chcemy uprościć tworzenie szablonów, to przetwarzanie składni na PHP jest sposobem najszybszym (dla serwera).
Można i tablice, kilka przeróbek i będzie gotowe. Ale można zezwolić na obydwie opcje. Rozwiązań jest dużo.
Co do stringów, to spójrz na ostatni wzorzec, tam można wpisać cokolwiek co podpasuje pod echo.
Po co piszesz własny system szablonów?
Znasz twiga? Ma zzytelną składnię i jest równie szybki jak "gołe" php (jest kompilowany do php).
Po to się pisze własny system szablonów, by być niezależnym od innych. Wprowadza się tylko takie funkcjonalności, które potrzebujemy.
@krzotr - trafiłeś w sedno :)
@krzotr Nie wiem jak się możesz uzależnić od innych używając oprogramowania open source. W każdej chwili taką blbliotekę możesz zmienić i rozszerzyć. Dobre rozwiązania architektonicznie na to pozwalają.
Ja jednak wolę nie tracić czasu na wynajdywanie koła na nowo. Uważam, że lepiej skupić się na szybkim dostarczeniu działającego rozwiązania klientowi.
Z całym szacunkiem dla Waszych umiejętności. Nikt w pojedynkę nie jest w stanie stworzyć lepszego rozwiązanie niż cała społeczność programistów.
Ale czy zdajesz sobie sprawę z tego, że osobnych skryptów też nie trzeba pisać, bo jest Wordpress, Joomla itp.
Chodzi po prostu, żeby umieć zrobić coś takiego samemu, wiedzieć na jakiej zasadzie to działa. Ja osobiście najczęściej korzystam ze Smarty (jeśli chodzi o wdrożenie systemu szablonów do projektu). Tutaj po prostu pokazuję jak zabrać się za taki skrypt. Może się komuś przyda.
Oczywiście, że sobie zdaję z tego sprawę. Dlatego z powodzeniem używam WordPressa, Drupala i innych. Tam, gdzie się nadają...
Nie zrozum mnie źle. Nie atakuję Ciebie. Wydaje mi się uczciwe zapytać o motywacje pisania własnego systemu szablonów, podczas gdy potrzeba pisania takowego jest bardzo rzadka.
Co do walorów edukacyjnych, polecam zajrzeć w kod Twiga.
Nie można ograniczać się "tylko" do gotowców. Znam osoby które używają ZF i nie znają pojęć "modyfikatory dostępu", "abstract", "interface" w dodatku nie wiedzą co to jest obiekt. Nie dziwie się, że na forum.php.pl są tematy typu "Parse error:"
Przez napisanie tej biblioteki Kacper wie jak tworzyć "fluent interface", do czego służy metoda magiczna __toString(). Chciał sprawdzić swoje możliwości i to mu się udało.
@krzotr to doceniam i nie neguję! Wiem, że pisanie jest to dobrą metodą na naukę i dyskutowanie rozwiązań. Jednak proponowanie takiej klasy komuś, kto ma problemy typu "parse error:" jest dla niego zgubne (a tacy też mogą znaleźć ten post). Wystarczylo zaznaczyć, że to forma nauki/zabawy.
Piszą w ZF i nie wiedzą co to obiekt? :D a wiedzą co to klasa?:P
ok, ja rozumiem że separacja php od html, logiki od prezentacji itp. możecie mnie wyśmiać ale czy na prawdę trzeba używać takich pseudo języków żeby to osiągnąć? i czy jest w ogóle sens korzystania z szablonów gdy całą stronę robi jedna osoba (to tak na marginesie)?
jak dla mnie zastąpienie <?= $tytul; ?> czymś w stylu {$tytul} jakoś mnie nie przekonuje. Cały kod php też potrafię odseparować i wrzucić w osobne pliki i klasy, tak że w kodzie html czasem pojawia się jakaś pętla która wyświetli dane zwrócone przez przygotowane wcześniej funkcje. I jakoś nigdy się w kodzie nie pogubiłem, ze zmianami dot. wyglądu strony też nie było problemów...
Jeśli wszystko robisz sam, nie musisz tworzyć sobie tych pseudo-języków. W projektach, które tworzę sam, albo z jakimś innym programistą szablony tworzymy w PHP, ale dla przykładu przy jednym większym projekcie pracuję w grupie trzyosobowej. Grafik nie zna PHP. Wtedy szablony są bardzo przydatne, ponieważ foreach można zastąpić jakąś inną nazwą, która jest bardziej zrozumiała dla osoby niezwiązanej z programowaniem. if można zastąpić jeżeli itp. Wiesz jak to ułatwia zadanie? Ja np. nie zajmuję się tworzeniem szablonu tylko warstwą kontrolera i modelu.
Dodatkowo, jeśli wrzucasz cały kod do jednego wora, to przy zmianie designu strony, musisz grzebać w śmietniku. A tak tworzysz nowy szablon i stary guzik Cię obchodzi.
Co do separowania kodu, to korzystanie z modelu MVC, jest bardzo dobrą praktyką nawet jak tworzysz sam. Kiedy chcesz przerobić stronę na telefon, zmieniasz tylko widok, chcesz skorzystać z plików zamiast bazy danych, przerabiasz model.
Dzisiaj musiałem tu wrócić i zerknąć na nazwy funkcji bufora ;) przydał się artykuł ;) a sobie dzisiaj zrobiłem dzisiaj własną klasę do obsługi szablonów, ale moja ma tylko 30 linijek... php jest na tyle prosty że nie chce mi się robić pseudo-języka i kompilatora do niego, zmienne ustawiam przez $template->zmienna=$x dzięki __set(), a w szablonie pobieram przez $this->zmienna i __get(). Grunt że się sprawdza ;)