Templating mit WordPress

Mit diesen Artikel möchte ich zum Teil ein über 6 Monate altes Versprechen einlösen. Zum anderen ist es der dritte Teil einer Reihe von Beiträgen die Schritt für Schritt zu einer einfachen Template-Engine führen.

Im ersten Teil hatte ich einen Formatter vorgestellt mit dem ich auf vergleichsweise einfache und flexible Art aus Daten eine Ausgabe machen kann. Im zweiten Teil habe ich mittels der Formatter-Klasse und einer Templates-Klasse eine Mini-Template-Engine gebastelt. Die hat aber noch etliche Nachteile und diente lediglich der Darstellung des Prinzips das dahinter steckt.
Diesmal möchte ich auf eine konkrete Implementierung in WordPress eingehen die in dieser Form immer wieder verwendet werden kann.

Das Pferd von hinten aufgezäumt

Es ist vielleicht etwas ungewöhnlich, ich will jedoch von hinten anfangen und beginne mit den eigentlichen Templates. Das ist in sofern sinnvoll, als das wir konkrete Templates haben und uns dann einen Weg überlegen können wie wir an diese heran kommen. Hier also erst einmal die Template-Klassen:

<?php
/**
*
* Abstract class Wp Simple Templates define one method to retrive the defined templates
* @author Ralf Albert
*
*/
abstract class WP_Simple_Templates
{

/**
*
* Returns the template if it was defined in the template class. If the requested template was not
* defined in the template-class, return a wp-error.
* @param string $type
* @return array|string|object Array with template-strings or a single template-string or wp-error on failure
*/
public function get_templates( $type = '' ){

if( '' == $type )
return NULL;

// if the template-class have a method $type, return the template(s). else return a wp-error
if( method_exists( $this, $type . '_template' ) ){

$method = $type . '_template';
return $this->$method();

} else {

return new WP_error(
'template_error',
printf(
'<h4>Template Error</h4>Template <strong>%s</strong> does not exists in <strong>%s</strong>',
$type,
get_class( $this )
)
);

}

}

/**
*
* Return an array with all available templates from the template-class
*/
public function get_available_templates(){

// get all methods from the abstract class (__CLASS__) and the extended class ($this)
$self_methods = get_class_methods( __CLASS__ );
$extended_methods = get_class_methods( $this );

// remove the '_template' extension and return the array with template-names
$templates = array();

foreach( array_diff( $extended_methods, $self_methods ) as $template )
array_push( $templates, str_replace( '_template', '', $template ) );

return $templates;

}

}

/**
*
* Concrete class WP Simple HTML Templates defines the templates
* @author Ralf Albert
*
*/

class WP_Simple_HTML_Templates extends WP_Simple_Templates
{
/**
* Returns an array with list templates
*/
public function list_template(){

return array(
'ol' => array(
'outer' => "<ol>n%inner%</ol>n",
'inner' => "<li>%item%</li>n"
),

'ul' => array(
'outer' => "<ul>n%inner%</ul>n",
'inner' => "<li>%item%</li>n"
),

'div' => array(
'outer' => "<div>n%inner%</div>n",
'inner' => "<p>%item%</p>n"
),
);

}

/**
* Returns a headline template with h1-tags
*/
public function hone_template(){

return '<h1>%headline%</h1>';

}

/**
* Returns a paragraph-template with p-tags
*/
public function paragraph_template(){

return '<p>%text%</p>';

}

}
view raw file1.php This Gist is brought to you using Simple Gist Embed.

Am Anfang sehen wir eine abstrakte Klasse WP_Simple_Templates. Diese Klasse hat lediglich zwei Methoden (get_templates() und get_available_templates()) und da sie als abstrakt definiert wurde, kann man von ihr keine Instanz (kein Objekt) erzeugen. Die zweite Klasse enthält lediglich Methoden die nichts anderes machen als Strings und Arrays zurück zu geben.
Wir könnten uns jetzt die abstrakte Klasse sparen und direkt auf die Methoden der Template-Klasse zugreifen. Dazu muss man aber immer genau wissen welche Methoden in der Template-Klasse definiert wurden. Das ist irgendwie doof, besser wäre es doch wenn man die Klasse fragen kann und sie antwortet mit einer Liste der verfügbaren Templates. Genau diese Aufgabe übernimmt die Methode get_available_templates() in der abstrakten Klasse.
Um an die einzelnen Templates heran zu kommen gibt es die Methode get_templates(). Sie liefert immer ein Array mit den angeforderten Template(s) zurück. Es muss uns also gar nicht mehr interessieren wie die Templates in der Template-Klasse benannt wurden oder wie die Template-Klasse diese erzeugt, mit get_template( 'list' ) bekommen wir immer das Template für HTML-Listen geliefert.
Da wir später noch verschiedene Template-Klassen verwenden wollen, habe ich die Logik der Template-Klasse (get_template() und get_available_templates()) in eine abstrakte Klasse ohne Template-Strings ausgelagert und erweitere die Template-Klassen um diese abstrakte Klasse. Einfaches Code-Recyling halt.

Die Templates verarbeiten

Nun nützt es uns recht wenig wenn wir an die Templates heran kommen. Im Grunde genommen wollen wir das auch gar nicht. Das einzige was wir machen wollen ist, Daten angeben und eine Ausgabe zurück bekommen. Wir benötigen also noch eine Klasse die unsere Daten in die Templates einfügt:

<?php
/**
*
* Abstract class WP Simple Templater creates a copy of the template-class and
* provide the templates to the template-engine
* @author Ralf Albert
*
*/
abstract class WP_Simple_Templater extends Formatter
{
/**
*
* Instance of template-class
* @var object $templates_object
*/
protected $templates_object = NULL;

/**
*
* Constructor
* Creates an instance of the template-class and setup the delimiters for Formatter
* @param WP_Simple_Templates $templates
*/
public function __construct( WP_Simple_Templates $templates ){

$this->templates_object = &$templates;

$this->set_delimiter( '%', '%' );

}

/**
*
* Return a template by given type. Returns an wp-error on failure
* @param string $type
* @return string|bool Return a template-string if it was defined by the template-class. Or false if no such template was defined
*/
protected function get_templates( $templatetype = '', $type = '' ){

// error message
$err_msg = '';

// if the sub-type is empty (e.g the template is a string not an array), use the template-type as sub-type
if( '' == $type )
$type = $templatetype;

// first check if a template-type was set
if( '' == $templatetype )
$err_msg = 'Empty template-type';

// check if a template-class was loaded
elseif( NULL === $this->templates_object )
$err_msg = 'No templates defined in <strong>' . get_class( $this ) . '</strong>';

// we have a template-type and a template-class. get the template(s)
else {

$template = $this->templates_object->get_templates( $templatetype );

// get error-message thrown by template-class
if( is_wp_error( $template ) )
$err_msg = $template->get_error_message();

// check the template.
// convert a string into an array. else check if the requested type is in the template-array
else {

if( ! is_array( $template ) )
$template[$type] = $template;

elseif( ! in_array( $type, array_keys( $template ) ) )
$err_msg = $type . ' is not defined.';

}

}

// if a error occurs, return an error-object
if( '' != $err_msg ){

return new WP_Error(
'template_error',
sprintf(
'<div class="error"><h4>Template Error</h4>%s</div>',
$err_msg
)
);

}

// finally all checks are ok, return the template-array
return $template;

}

}

/**
*
* The concrete class WP Simple HTML insert the data in the templates
* @author Ralf Albert
*
*/
class WP_Simple_HTML extends WP_Simple_Templater
{

/**
*
* Constructor overrides the delimiters in parent class
* @param WP_Simple_Templates $templates
*/
public function __construct( WP_Simple_Templates $templates ){

parent::__construct( $templates );

$this->set_delimiter( '{', '}' );

}

/**
*
* Create a html list (ul, ol or with div-tag)
* @param string $type The type of the list. ul, ol or div
* @param array $data Data to display inside the list
*/
public function get_list( $type = '', $data = array() ){

// get list templates
$templates = $this->get_templates( 'list', $type );

if( is_wp_error( $templates ) )
return $templates->get_error_message();

// create list
$inner = new stdClass();
$values = new stdClass();

foreach( $data as $key => $value ){

$values->key = $key;
$values->item = $value;

$inner->inner .= self::sprintf( $templates[$type]['inner'], $values );

}

return self::sprintf( $templates[$type]['outer'], $inner );

}

/**
*
* Prints a html-list (ul, ol or with div-tag)
* @param string $type The type of the list. ul, ol or div
* @param array $data Data to display inside the list
*/
public function print_list( $type = '', $data = array() ){

if( ! is_array( $data ) )
$data = (array) $data;

echo $this->get_list( $type, $data );

}

/**
*
* Create a H1-headline
* @param string $text
*/
public function get_headline( $text = '' ){

// get headline template
$template = $this->get_templates( 'hone' );

if( is_wp_error( $template ) )
return $template->get_error_message();

$data = new stdClass();
$data->headline = $text;

return self::sprintf( $template, $data );

}

/**
*
* Print a H1-headline
* @param string $text
*/
public function print_headline( $text = '' ){

echo $this->get_headline( $text );

}

}

view raw file1.php This Gist is brought to you using Simple Gist Embed.

Dies wird die Klasse WP_Simple_HTML übernehmen. Auch hier könnte man es sich einfach machen und sich den Template-String direkt aus der Template-Klasse holen und dann mit den entsprechenden Daten versehen an die Formatter-Klasse übergeben. Allerdings müssten wir dann zuerst die Daten jedes mal für das entsprechende Template aufbereiten. Diese Logik lagern wir lieber in die Klasse WP_Simple_HTML aus um sie nicht ständig im Plugin- oder Theme-Code drin zu haben.
Das Heranholen der Templates aus der Template-Klasse ist immer der gleiche Vorgang, deswegen können wir diesen ebenfalls in eine abstrakte Klasse (WP_Simple_Templater) auslagern die wir ebenfalls immer wieder verwenden können.
Diese Klasse hat eine kleine Besonderheit. Im Konstruktor wird explizit ein Objekt angefordert das von der Klasse WP_Simple_Templates abstammt. Da wir WP_Simple_Templates abstrakt definiert haben, und somit davon nicht direkt eine Instanz erzeugen können, muss es also eine Instanz einer Klasse sein die WP_Simple_Templates erweitert. Damit gehen wir sicher das wir WP_Simple_HTML nicht z.B. eine Instanz von wpdb (der Datenbank-Klasse) oder ein Error-Object (WP_Error()) übergeben. Die abstrakte Klasse erfüllt somit neben dem Code-Recycling zusätzlich noch die Aufgabe sicherzustellen das wir eine Template-Klasse und nichts anderes übergeben.

Damit haben wir jetzt die Logik von der Ausgabe getrennt. Sowohl in unserer Template-Klasse (WP_Simple_Templates und WP_Simple_HTML_Templates), als auch in der Template-Engine (WP_Simple_Templater und WP_Simple_HTML). Wiederkehrende Logik haben wir in abstrakte Klassen ausgelagert die wir problemlos recyclen können.
Die einzige Arbeit die uns zukünftig bleibt, ist eine Template-Klasse zu schreiben die die entsprechenden Templates enthält und für jedes Template die nötige Logik schreiben die die Daten aufbereitet und anschließend Daten und Template verbindet. Da wir die Logik für die Datenaufbereitung von den Templates getrennt haben, können wir munter die Templates umstellen ohne in unseren eigentlichen Plugin- oder Theme-Code eingreifen zu müssen.
Die Logik für die Datenaufbereitung müssen wir auch nur dann anfassen, wenn wir die Templates um weitere Variablen ergänzen oder ändern. Logik und Ausgabe sind in allen Bereichen komplett voneinander getrennt.

Lose koppeln anstatt fest verdrahten

Wie würde man nun diese Template-Engine in seinem Plugin oder Theme verwenden? Wahrscheinlich würde man sie an entsprechender Stelle so im Code einfügen:

$template = new WP_Simple_HTML_Templates();
$html = new WP_Simple_HTML( $template );
//some other code

$html->print_headline( 'Success!' );

$list = $html->get_list( 'ol', array( 'one' => 'eins', 'two' => 'zwei', 'three' => 'drei' ) );

// more code
echo $list;

Das sieht im Grunde genommen schon mal schön aus. Relativ schön, um genau zu sein. Denn sowohl die Template-Klasse ($template) als auch die Template-Engine ($html) sind mehr oder minder fest verdrahtet. Würden wir für unsere Template-Engine eine andere Template-Klasse verwenden wollen, müssten wir dies im Code ändern. Gleiches gilt für den Fall das wir unsere Template-Klasse behalten aber auf eine andere Template-Engine zurückgreifen wollen.

Besser wäre es, wenn wir später noch andere Template-Engines und Template-Klassen angeben könnten. WordPress bietet uns hierzu die Möglichkeit mit apply_filters() und add_filter(). Schauen wir uns dazu folgende Demo an:

<?php
class Template_Engine_Test
{
/**
*
* Container for the different template-engines
* @var object $template_engine
*/
public static $template_engine;

/**
*
* Initial some vars
*/
public function __construct(){

// initialize the template-engine to avoid strict errors
self::$template_engine = new stdClass();

}

/**
*
* Caching for the template-classes
* Create a WP-Error if the template-engine could not be initialize
* @param string $template_engine The requested template-engine
* @return object The requested template-engine if available
*/
protected function get_template_engine( $template_engine ){

$template_engine = strtolower( $template_engine );

// available template-engines and the template-classes
$available_template_engines = array(

'html' => array( 'WP_Simple_HTML', 'WP_Simple_HTML_Templates' )

);

$available_template_engines = apply_filters( 'available_template_engines', $available_template_engines );

// check if the requested template-engine is available. if not, try to create it
if( ! isset( self::$template_engine->$template_engine ) ){

if( ! isset( $available_template_engines[$template_engine] ) ){

return new WP_Error( 'template_engine', 'Unknown template-engine <strong>' . $template_engine . '</strong>' );

} else {

$tmpl_ng = &$available_template_engines[$template_engine];
$class = &$tmpl_ng[0];
$template = &$tmpl_ng[1];

self::$template_engine->$template_engine = new $class( new $template );

}

}

return self::$template_engine->$template_engine;

}

/**
*
* The output
*/
public function output( $stream ){

$output = $this->get_template_engine( $stream );

if( is_wp_error( $output ) )
die( $output->get_error_message() );

$output->print_headline( strtoupper( $stream ) );

$list = $output->get_list( 'ol', array( 'eins', 'zwei', 'drei' ) );
echo $list;

}

/**
*
* Setup html-mode
*/
public function set_html(){

// html is build in

}

/**
*
* Setup xml-mode
*/
public function set_xml(){

add_filter( 'available_template_engines', array( &$this, 'add_xml' ), 1, 0 );

}

/**
*
* Add the XML-Template-Engine
*/
public function add_xml(){

return array(

'xml' => array( 'WP_Simple_HTML', 'WP_Simple_HTML_Templates' ),

);

}

public function set_feed(){

add_filter( 'available_template_engines', array( &$this, 'add_feed' ), 1,1 );

}

public function add_feed( $template_engines ){

$feed_engines = array(

'rss' => array( 'WP_Simple_HTML', 'WP_Simple_HTML_Templates' ),
'atom' => array( 'WP_Simple_HTML', 'WP_Simple_HTML_Templates' ),

);

return array_merge( $template_engines, $feed_engines );

}

}

$html = new Template_Engine_Test();
$html->set_html();
$html->output( 'html' );

$xml = new Template_Engine_Test();
$xml->set_xml();
$xml->output( 'xml' );

$feed = new Template_Engine_Test();
$feed->set_feed();
$feed->output( 'rss' );
$feed->output( 'atom' );

$bar = new Template_Engine_Test();
$bar->output( 'wallawalla' );

view raw file1.php This Gist is brought to you using Simple Gist Embed.

Die Methode get_template_engine() erfüllt zwei Aufgaben. Zum einen gibt sie die angeforderte Template-Engine inkl. der dazugehörigen Template-Klasse zurück (sofern beides vorhanden). Und zum anderen speichert sie die Template-Engine und Template-Klasse in einer statischen Variablen. Welche Kombination aus Template-Engine und Template-Klasse zurück gegeben wird, wird anhand eines Tags entschieden. So ist dem Tag ‘html’ schon einmal die Kombination WP_Simple_HTML/WP_Simple_HTML_Templates zugeordnet.
Weitere Kombinationen aus Template-Engine und Template-Klasse lassen sich über einen Filter hinzufügen. Bevor geprüft wird ob es den angeforderten Tag mit der entsprechenden Kombination gibt, wird ein Filter ausgeführt der es uns erlaubt weitere Kombinationen hinzuzufügen.
Dies nutzen die Methoden set_xml() und set_feed() aus. Sie fügen einfach dem Array der verfügbaren Template-Engines/Klassen ihre eigenen hinzu und machen diese dadurch zugänglich. Danach kann in der Methode output() auf die neu hinzugefügten Template-Engines und Klassen zugegriffen werden. Hiermit sind wir quasi am Ziel unserer Wünsche angelangt. Denn die Methode output() muss sich gar nicht mehr darum kümmern wie die Daten ausgegeben werden, sie stellt diese einfach bereit und erwartet das die entsprechende Template-Engine entsprechende Methoden zur Ausgabe zur Verfügung stellt.