Dziedziczenie szablonów pojawiło się, z tego co wiem, w systemie szablonów pythonowego frameworka Django. Idea jest wzięta z programowania obiektowego, gdzie można dziedziczyć klasy, dodając do nich nowe metody, pola oraz nadpisując istniejące. Tutaj jest podobnie. Mamy szablon główny, w którym zaznaczamy pewne charakterystyczne miejsca i wypełniamy je standardową zawartością. Drugi szablon dziedziczy po głównym i może nadpisać wybrane miejsca własną treścią. Po nim może iść kolejny szablon i tak dalej.

Okazało się, że spora część niezbędnej funkcjonalności była już w OPT 1.x - instrukcje bind oraz insert pozwalały przenosić całe fragmenty kodu w inne miejsce, także pomiędzy szablonami. Niestety, nie było tam odpowiedniego mechanizmu detekcji, czy zaszły jakieś modyfikacje w plikach i to trochę psuło całą koncepcję. Tak samo jego opracowanie dla nowej wersji było najtrudniejszym zadaniem, ponieważ nie wystarczy teraz jedynie sprawdzić, czy zaszły zmiany w szablonie, który wywołujemy, ale także czy coś nowego nie pojawiło się w szablonach bazowych! Z zamiarem definitywnego rozwiązania tego problemu siadłem do kodu wczoraj i parę godzin temu wszystko zadziałało tak, jak powinno:

Szablon bazowy (test_inherited_a.tpl):

<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
	   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" lang="en_US" xml:lang="en_US">
 <head>
  <title>Instruction test: snippet and insert</title>
 </head>
 <body>
  <opt:insert snippet="header">
   <h1>I'm a standard header</h1>
   <p>Foo bar joe</p>  
  </opt:insert>
  
  <hr/>
  
  <opt:insert snippet="content">
  	<p>Well, i'm also a standard content.</p>
  
  </opt:insert>
  
  <hr/>
  
  <opt:insert snippet="footer">
  	<p>And I'm a footer.</p>  
  </opt:insert>
  
  <p>&copy; Pasteright 2008 by LMAO, It seems to work!</p>
 </body>
</html>

Szablon dziedziczący po nim (test_inherit_1.tpl):

<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<opt:extend file="test_inherited_a.tpl">
	<opt:snippet name="header">
		<h1>Webmaster Of Puppets</h1>
	</opt:snippet>

	<opt:snippet name="content">
		<p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Phasellus ut tellus id nulla adipiscing eleifend. Sed dictum accumsan ante. Nullam at nisl vitae elit aliquet fringilla. Praesent egestas eros eget tellus. Praesent id odio a sapien rhoncus vehicula. Nunc fringilla, diam eget euismod tempor, tortor metus tincidunt sapien, eu cursus magna tellus at risus. Praesent non tellus eget magna facilisis pulvinar. Praesent libero mi, adipiscing a, pharetra eget, condimentum sodales, mi. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos hymenaeos. Donec ac elit. Duis iaculis tortor a metus. Aliquam id purus et eros faucibus fringilla. Praesent quis quam. In lectus urna, fringilla sit amet, iaculis eget, aliquet ac, quam. Donec vulputate dui sit amet lectus. Aenean tempor, orci at pretium ornare, tortor tortor venenatis ligula, eget blandit nisi risus eget dolor. Duis nunc neque, sodales porta, viverra non, tristique eu, sem. Curabitur magna neque, blandit ullamcorper, congue quis, tristique ut, felis.</p>
	</opt:snippet>

	<opt:snippet name="footer">
		<p>Bye!!!</p>
	</opt:snippet>
</opt:extend>

Do tego kod PHP:

<?php

	define('OPT_DIR', '../lib/');
	require(OPT_DIR.'opt.class.php');

	try
	{
		$tpl = new optClass;
		$tpl -> sourceDir = './templates/';
		$tpl -> compileDir = './templates_c/';
		$tpl -> stripWhitespaces = false;
		$tpl -> printComments = false;
		$tpl -> setup();
		
		$tpl -> parse('test_inherit_1.tpl');
	}
	catch(optException $e)
	{
		optErrorHandler($e);
	}
?>

A rezultat widać poniżej:

Template inheritance

Parę faktów:

  1. Jeżeli zmodyfikujemy dowolny z szablonów, po którym nasz szablon dziedziczy, OPT wykona rekompilację i zmiana zostanie natychmiast uwzględniona.
  2. Jeżeli usuniemy dowolną z instrukcji "opt:snippet", wyświetli nam się domyślna zawartość zdefiniowana wewnątrz "opt:insert".
  3. Istnieje możliwość nadpisania już zdefiniowanego snippeta przez leżący wyżej w hierarchii szablon. Będę implementował mechanizm odwołania się do zawartości rodzica (jakaś instrukcja "opt:parent").
  4. Można to mieszać z "opt:include" oraz "opt:sequence" (mechanizm sekwencji).

Ponadto bardzo możliwe, że dziedziczenie szablonów w ostatecznym rozrachunku może działać szybciej, niż tradycyjna metoda, aczkolwiek kosztem miejsca na dysku. Po prostu jak mamy dziedziczenie "A po B po C", wywołując szablon A, caluteńki kod wynikowy ze wszystkich szablonów jest zrzucany właśnie do A. Zmniejsza to ilość odwołań do dysku, gdyż uruchomienie jednego wielkiego szablonu jest mniej kosztowne, niż tej samej treści rozbitej na 3 mniejsze pliki. Z drugiej strony, w podstawowym trybie OPT wciąż sprawdza czasy modyfikacji (tyle że robi mniej testów). Póki co są to jedynie przypuszczenia. Dopiero po skończeniu i zrobieniu porządnego benchmarku będę w stanie podać konkretne liczby. Chociaż dodam, że z ciekawości puściłem sobie prosty test (lista 15 elementów):

  1. OPT 1.x: 1850 req/s
  2. OPT 2.0: 2100 req/s

Nie są to wyniki ostateczne - trzeba wziąć pod uwagę, że nowy plik opt.class.php wciąż nie ma sporej części opcji, które trochę zwiększą jego objętość, a ponadto nie zaimplementowałem jeszcze niektórych optymalizacji. Tak więc można się spodziewać, że nowe możliwości nie odbiją się niekorzystnie na wydajności, a nawet jest szansa na coś odwrotnego - nowa wersja będzie szybsza.