devblog, portfolio

movie producer, zend framework, php, jquery pluginy



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.

Comandeer Mozilla/5.0 (Windows NT 6.0) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.5 Safari/534.30

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?

Kacper Kołodziej www Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.205 Safari/534.16

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.

Kuba www Mozilla/5.0 (X11; Linux i686; rv:2.0) Gecko/20100101 Firefox/4.0

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).

krzotr Opera/9.80 (Windows NT 5.1; U; pl) Presto/2.8.131 Version/11.10

Po to się pisze własny system szablonów, by być niezależnym od innych. Wprowadza się tylko takie funkcjonalności, które potrzebujemy.

Kacper Kołodziej www Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.205 Safari/534.16

@krzotr - trafiłeś w sedno :)

Kuba www Opera/9.80 (X11; Linux i686; U; en) Presto/2.8.131 Version/11.10

@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.

Kacper Kołodziej www Mozilla/5.0 (Windows; U; Windows NT 5.1; pl; rv:1.9.2.16) Gecko/20110319 Firefox/3.6.16

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.

Kuba www Mozilla/5.0 (X11; Linux i686; rv:2.0) Gecko/20100101 Firefox/4.0

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.

krzotr Mozilla/5.0 (Windows NT 5.1; U; pl; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 11.10

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.

Kuba www Mozilla/5.0 (X11; Linux i686; rv:2.0) Gecko/20100101 Firefox/4.0

@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.

Piotr Krych www Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.57 Safari/534.24

Piszą w ZF i nie wiedzą co to obiekt? :D a wiedzą co to klasa?:P

lisek Mozilla/5.0 (Windows NT 6.1; rv:2.0.1) Gecko/20100101 Firefox/4.0.1

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...

Kacper Kołodziej www Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.100 Safari/534.30

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.

lisek Mozilla/5.0 (Ubuntu; X11; Linux x86_64; rv:8.0) Gecko/20100101 Firefox/8.0

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 ;)

Dodaj komentarz »