Nello scorso articolo abbiamo introdotto il Template engine XML che ho deciso di affiancare al framework Taste. Con questo articolo completeremo il quadro descrittivo ed implementativo del package taste.mvc.template
soffermandoci in modo dettagliato sull'implementazione delle classi TemplatePlugin e DefaultTemplatePlugin.
Come sempre potete trovare i sorgenti allegati a questo articolo, nella sezione download.
Implementazione di TemplatePlugin
Per prima cosa ci soffermeremo sulla classe astratta TemplatePlugin
, che verrà utilizzata come base per tutti i plug-in che andremo ad implementare da affiancare al nostro template engine. Un plug-in non è altro che una sottoclasse di TemplatePlugin, registrata appositamente all'interno del template engine attraverso il metodo statico registerPlugin
.
Ad ogni plug-in viene associato un namespace XML, che potrà successivamente essere utilizzato nel template per importare ed utilizzare il plug-in all'interno del proprio codice. Durante l'analisi dei sorgenti, il template engine registra tutti i namespace utilizzati nel documento associandovi il rispettivo plug-in; quando l'iteratore dei nodi incontra un elemento o un attributo a cui è stato associato un namespace diverso da quello standard HTML, viene caricato il plug-in corrispettivo e viene applicato al nodo su cui si sta lavorando. La trasformazione del nodo è demandata ad un metodo del plug-in, che verrà selezionato secondo semplici criteri che poi vedremo dal template engine. Una volta trasformato il nodo si procede con l'iterazione.
Il metodo da applicare ad un nodo è ricercato all'interno della lista dei metodi pubblici esposti che iniziano con tag_
o attribute_
seguiti dal nome del nodo o dell'attributo a cui è stato associato il namespace con cui è registrato il plug-in.
Vediamo l'implementazione della classe TemplatePlugin:
abstract class TemplatePlugin { private $tags; private $attributes; private $priority_map; public function __construct($priority_map=array()) { $this->tags = array(); $this->attributes = array(); $this->priority_map = array(); $index = 0; foreach($priority_map as $item) $this->priority_map[$item] = $index++; $ref = new ReflectionClass($this); foreach($ref->getMethods() as $method) { if(!preg_match("/^(attribute|tag)_.*/i", $method->name)) continue; list($type, $name) = explode("_", $method->name, 2); $this->{$type."s"}[] = $name; } } public function supportTag($name) { return in_array($name, $this->tags); } public function supportAttribute($name) { return in_array($name, $this->attributes); } public function sortCommands($a, $b) { $a = $a[0]; $b = $b[0]; $a = isset($this->priority_map[$a]) ? $this->priority_map[$a] : 0; $b = isset($this->priority_map[$b]) ? $this->priority_map[$b] : 0; return ($a > $b ? 1 : ($a == $b ? 0 : -1)); } protected function createTemplateFromNode($node) { $template = $node->cloneNode(true); $node->parentNode->removeChild($node); return $template; } }
La lista dei metodi pubblici esposti viene creata durante la costruzione del plug-in, in modo che non sia necessario effettuare ogni volta una ricerca all'interno di tutti i metodi per cercare quello desiderato. Il costruttore accetta come parametro un array che viene utilizzato per ordinare per priorità i metodi nel caso in cui su un nodo ne siano presenti più di uno.
Le funzioni support*
vengono definite per controllare se un determinato plug-in supporta o meno un determinato comando; potrebbero essere utilizzate in fase di precompilazione del template per controllare che non vi siano delle imprecisioni.
Con questo semplice metodo siamo in grado di ottenere una libertà estrema nella scrittura dei plug-in. Possiamo addirittura generare XML dinamico (contenente a sua volta dei plugin da caricare e dei comandi da eseguire) che verrà automaticamente (e correttamente) gestito dell'engine senza lavoro aggiuntivo da parte nostra.
I metodi per l'esecuzione dei comandi andranno definiti con un prototipo specifico:
- Nel caso di attributo, i parametri da accettare dovranno essere quattro: il primo rappresentante il template su cui si sta operando, il secondo lo scope nel quale si sta lavorando, il terzo il nodo XML contenente l'attributo che ha scatenato la chiamata al metodo ed infine una stringa che rappresenta il valore assegnato all'attributo;
- Nel caso di tag invece gli argomenti saranno tre e non verrà passato l'ultimo;
Nel caso uno di questi metodi restituisca un valore non nullo, questo valore dovrà necessariamente essere un elemento XML che verrà utilizzato al posto di quello che ha scatenato la chiamata al metodo.
Il TemplatePlugin di default
Passiamo ora ad analizzare il plug-in di default. Come già accennato precedentemente ho deciso di implementare alcune delle funzionalità più comuni fornite dai template engine XML più importanti, come PHPTal (che oltretutto abbiamo già avuto modo di analizzare qualche mese fa proprio su php.html.it).
Vediamo inizialmente il codice, poi passeremo ad un'analisi dei singoli metodi per vederne il comportamento:
class DefaultTemplatePlugin extends TemplatePlugin { public function __construct() { parent::__construct(array('condition', 'repeat', 'replace', 'content', 'attributes', 'omit')); } public function attribute_condition(Template $template, Scope $scope, $node, $expression) { $value = $scope->evaluateVar($expression); if($value) $template->evaluateNode($node, $scope); else $node->parentNode->removeChild($node); } public function attribute_repeat(Template $template, Scope $scope, $node, $expression) { list($source, $item) = array_map('trim', explode(",", $expression)); $parent = $node->parentNode; $tpl_node = $this->createTemplateFromNode($node); $source_data = $scope->evaluateVar($source); $scope->pushScope(); $iteration = 0; foreach($source_data as $data) { $scope->set('iteration', $iteration); $scope->set($item, $data); $new_node = $tpl_node->cloneNode(true); $new_node = $template->evaluateNode($new_node, $scope); $parent->appendChild($new_node); ++$iteration; } $scope->popScope(); } public function attribute_replace(Template $template, Scope $scope, $node, $expression) { $fragment = $node->ownerDocument->createDocumentFragment(); $fragment->appendXML($scope->evaluateVar($expression)); return $fragment; } public function attribute_content(Template $template, Scope $scope, $node, $expression) { foreach($node->childNodes as $child) $node->removeChild($child); $fragment = $node->ownerDocument->createDocumentFragment(); $fragment->appendXML($scope->evaluateVar($expression)); $node->appendChild($fragment); } public function attribute_attributes(Template $template, Scope $scope, $node, $expression) { $rules = array_map('trim', explode(",", $expression)); foreach($rules as $rule) { list($attribute, $var) = array_map('trim', explode(":", $rule, 2)); $value = $scope->evaluateVar($var); $node->setAttribute($attribute, $value); $template->evaluateNode($node, $scope); } } public function attribute_omit(Template $template, Scope $scope, $node, $expression) { $node->parentNode->removeChild($node); } } Template::registerPlugin("http://php.html.it/taste", new DefaultTemplatePlugin);
Partendo dall'ultima riga, possiamo notare come avviene la registrazione di un plugin attraverso l'utilizzo del metodo statico registerPlugin
; grazie a questo accorgimento ci basterà utilizzare la direttiva xmlns="http://php.html.it/taste"
all'interno dell'XML (magari specificando un alias così che non venga utilizzato come plug-in di default per tutti i nodi che il template engine andrà ad iterare) per caricare il plug-in e poterlo utilizzare.
Nel costruttore viene passato l'ordine con ci dovranno essere ordinate le chiamate ai metodi, nel caso in cui un nodo abbia più di un comando richiamato su di esso. L'ordine è lo stesso utilizzato da PHPTal, e possiamo considerarlo uno standard logico che permette di avere un comportamento simile a quello che avremmo in un linguaggio di programmazione.
Passiamo in rassegna i singoli tag, identificando per ognuno eventuali punti chiave per comprendere meglio il funzionamento del sistema:
- omit: rimuove semplicemente il nodo corrente dal parent; non restituendo nulla ci assicuriamo che l'iterazione si interrompa ed il template egine prosegua con il sibling successivo;
- attributes: aggiunge dinamicamente degli attributi al nodo. Il valore del comando è rappresentato da una serie di assegnamenti separati da virgola; ogni assegnamento separa con due punti il nome dell'attributo dal nome della variabile che ne contiene il valore;
- content: sostituisce il contenuto del nodo corrente con il valore della variabile passata come espressione;
- replace: sostituisce l'intero nodo con il contenuto del valore della variabile passata come espressione; restituisce il nuovo nodo in modo che sia sostituito a quello che è stato passato in input;
- condition: fa proseguire la valutazione dei nodi figli dell'argomento solamente se il valore della variabile passata come espressione è vero;
- repeat: è il nodo più complesso ma anche il più utile e potente. Per poter effettuare correttamente la ripetizione del nodo per ogni elemento dell'array rappresentato dalla variabile passata come espressione, è necessario dapprima rimuovere il nodo e poi creare una template che verrà riempita e copiata in successione all'interno del nodo padre. La template verrà poi lasciata da valutare al template engine in modo che eventuali comandi vengano gestiti correttamente;
Conclusioni
Eccoci giunti alla fine della descrizione del template engine. Nel prossimo articolo vedremo come utilizzare praticamente il codice scritto all'interno del framework MVC. Nel frattempo invito chi fosse interessato a proporre qualche upgrade per il framework Taste che lo renda utile anche in campo produttivo e non solo didattico, scrivendomi via email le proposte. Per esempio potrebbe essere interessante estendere il template engine in modo che le espressioni passate ai comandi del plug-in di default possano essere più complesse, oppure si potrebbe introdurre un sistema di permessi per i controller.