PHP.de Wissenssammlung

Template-Engine

Was sind Template Engines?

Template Engines sind Bibliotheken, die den notwendigen Aufwand zur Trennung von HTML und PHP so gering wie möglich halten. Template Engines haben dabei die Aufgabe Methoden und Variablen in einen für sich geschlossenen Bereich ( Scope ) bereit zu stellen. Viele Template Engines heben sich von PHP ab, indem sie eine eigene Template-Sprache mitbringen um somit das direkte Ausführen von PHP-Quellcode im Template verhindern.

Wann sind Template Engines sinnvoll?

Will man unbedingt eine Richtlinie festlegen die definiert wann eine Template Engine sinnvoll ist und wann nicht, würde man sich nach reichlicher Überlegung auf “immer” festlegen. Es ist natürlich nicht immer jedes Feature einer Template Engine notwendig. Die Grundaufgabe ( die Trennung von PHP und HTML ) sollte jedoch immer erfüllt werden.

Welche Template Engine sollte ich benutzen?

Kurz gesagt: die mit der du umgehen kannst. Viele würden jetzt blind auf Twig, Smarty oder Mustache verweisen, was dir aber im Endeffekt erstmal wieder eine Lernphase in die Entwicklung baut, die impliziert das du dich mit einer möglicherweise Komplexeren Teil-Anwendung vertraut machen musst, als deine eigentliche Anwendung je sein wird.

Was ist nötig für eine leicht erweiterbare solide Template Engine?

Aus technischer Sicht benötigt eine Template Engine zwei Klassen. Eine Klasse, die es ermöglicht globale Variablen und Template-eigene Funktionen zu registrieren und eine Methode liefert, die das Rendern des Templates anzustoßen. Eine weitere Klasse, die als Template-Instanz dient, und die es der Render-Methode ermöglicht dem Template zur Verfügung stehende Methoden von den eigentlichen Engine-Funktionen zu trennen. Mehr ist aus technischer Sicht für eine traditionelle Template Engine nicht notwendig.

Implementierung

Die Engine-Klasse

Um eine generelle leichte Erweiterbarkeit bereitzustellen erzeugen wir zuerst ein Interface das die Methoden der Engine Klasse definiert. Ein Interface liefert hier die API die der Anwendung die diese Klasse nutzen soll bereitgestellt wird. Unsere eigentliche Anwendung kann sich auf die in diesem Interface definierten Methoden verlassen und wird nur diese Methoden zum bedienen der Template Engine benutzen.

EngineContract.php

<?php

namespace TemplateEngine;

interface EngineContract {
    public function directory($aliasName, $directory);
    public function define($functionName, \Closure $callback);
    public function stringify($interface, \Closure $callback);
    public function assign(array $globalAssignments);
    public function build($templateName, array $localAssignments = array());
    public function compile(Template $template);
    public function render($templateName, array $localAssignments = array());
}

Die konkrete Implementierung: Engine.php

<?php

namespace TemplateEngine;

class Engine implements EngineContract {

    protected $fileExtension = 'phtml';
    protected $functions = array();
    protected $stringifier = array();
    protected $globalAssignments = array();
    protected $directories = array();

    public function __construct(array $directories = array(), $fileExtension = 'phtml')
    {
        if ( ! empty($directories) ) {
            foreach ( $directories as $alias => $directory ) {
                $this->directory($alias, $directory);
            }
        }

        $this->fileExtension = ltrim($fileExtension, '.');

        $this->define('render', function($templateName, array $assignments) {
            return $this->render($templateName, $assignments);
        });
    }

    public function directory($aliasName, $directory)
    {
        if ( ! is_dir($directory) ) {
            throw new \Exception(
                sprintf('`%s` is not a directory', $directory)
            );
        }

        $this->directories['@'.ltrim($aliasName, '@')] = realpath($directory);

        return $this;
    }

    public function define($functionName, \Closure $callback)
    {
        $this->functions[strtolower($functionName)] = $callback;
    }

    public function stringify($interface, \Closure $callback)
    {
        $this->stringifier[$interface] = $callback;
    }

    public function assign(array $globalAssignments)
    {
        $this->globalAssignments = $globalAssignments;
    }

    public function build($templateName, array $localAssignments = array())
    {
        $templateFilename = str_replace(
            array_keys($this->directories),
            array_values($this->directories),
            $templateName
        );

        if ( empty($this->directories) ) {
            throw new Exception(
                'can not resolve template, no directories registered to the engine'
            );
        }

        if ( ! is_file($templateFilename.'.'.$this->fileExtension) ) {
            throw new Exception(
                sprintf(
                    'can not resolve template `%s`, template not found',
                    $templateName
                )
            );
        }

        $file = realpath($templateFilename.'.'.$this->fileExtension);

        $assignments = $this->globalAssignments;

        foreach ( $localAssignments as $key => $value ) {
            $assignments[$key] = $value;
        }

        return new Template($file, $this->functions, $this->stringifier, $assignments);
    }

    public function compile(Template $template)
    {
        $compiler = function() {
            ob_start();
            try {
                include $this->templateFileName();
            }
            catch ( Exception $exception ) {
                ob_end_clean();
                throw $exception;
            }

            $content = ob_get_contents();
            ob_end_clean();

            return $content;
        };

        return call_user_func($compiler->bindTo($template));
    }

    public function render($templateName, array $localAssignments = array())
    {
        return $this->compile($this->build($templateName, $localAssignments));
    }

}

directory()

Die directory()-Methode assoziiert einen beliebigen Dateipfad mit einem Alias, sodass du nicht innerhalb von Template oder beim Render-Aufruf immer den konkreten Pfad zur Template-Datei angeben musst.

define()

Die define()-Methode assoziiert einen beliebigen in der Klasse Template nicht existenten Methodennamen mit einem Closure-Callback. Die mit dieser Methode definierten Funktionen sind als Objekt-Methoden mit Hilfe von $this aufrufbar.

stringify()

Die stringify()-Methode assoziiert ein beliebiges Interface ( Klassen- oder Interface-Name ) mit einem Closure-Callback. Beim Zugriff auf die Assignments im Template ruft das Template automatisch das Callback das auf das jeweilige Interface registriert wurde auf wenn ein Objekt dem Template übergeben wurde.

assign()

Die assign()-Methode setzt die globalen Assignments. Globale Assignments sind in allen Templates verfügbar und müssen nicht innerhalb der Templates an weitere Template-Aufrufe weitergereicht werden.

build()

Die build()-Methode erzeugt eine Template Instanz. Diese Methode wird von render() benutzt und kann dazu verwendet werden ein traditionelles View-Objekt zu erzeugen.

compile()

Die compile()-Methode führt das übergebene Template aus. Diese Methode wird von render() benutzt und sollte dazu verwendet werden zuvor mit build() erzeugte Template-Instanzen auszuführen. Die Compile-Methode erzeugt ein Closure und bindet dieses Closure an die Template-Instanz. Dieses Closure ist notwendig um das Template selbst in einen eigenen Scope zu bringen, um zu vermeiden das Templates selbst die Engine oder andere anwendungsspezifischen Klassen direkt manipulieren können.

render()

Die render()-Methode führt ein angegebenes Template aus. Diese Methode ruft build() auf und benötigt keine Datei-Erweiterung. Es werden zuvor mit directory() assoziierte Verzeichnis-Aliase in dem übergebenen Template-Namen ersetzt. Templates dürfen in dieser Implementierung Exceptions werfen, die build()-Methode reicht diese an den aufrufenden Scope weiter.

Zusätzlich zu diesen Methoden ist noch ein Constructor definiert. Dieser speichert die File-Extension der Templates und übernimmt generelle Directory-Alias-Assoziationen als Array. Außerdem registriert der Constructor eine render-Template-Funktion sodass innerhalb der Templates weitere Templates aufgerufen werden können.

Die Template-Klasse

Die Template-Klasse dient als Handler und wird in dieser Implementierung als final definiert, um zu verhindern das die Template-Klasse selbst erweitert wird. Dies soll Methoden und Eigenschaften-Konflikte innerhalb der Templates verhindern.

Template.php

<?php

namespace TemplateEngine;

final class Template {

    private $filename;
    private $functions = array();
    private $stringifier = array();
    private $assignments = array();

    public function __construct($filename, array $functions, array $stringifier, array $assignments)
    {
        $this->filename = $filename;
        $this->functions = $functions;
        $this->assignments = $assignments;
        $this->stringifier = $stringifier;
    }

    public function templateFileName()
    {
        return $this->filename;
    }

    public function __get($assignment)
    {
        if ( ! array_key_exists($assignment, $this->assignments) ) {
            throw new \Exception(
                sprintf('Unknown assignment `%s`', $assignment)
            );
        }

        if ( is_object($this->assignments[$assignment]) ) {
            foreach ( $this->stringifier as $currentInterface => $callback) {
                if ( is_a($this->assignments[$assignment], $currentInterface, true) ) {
                    return call_user_func($callback, $this->assignments[$assignment]);
                }
            }
        }

        return $this->assignments[$assignment];
    }

    public function __set($assignment, $value)
    {
        $this->assignments[$assignment] = $value;
    }

    public function __unset($assignment)
    {
        unset($this->assignments[$assignment]);
    }

    public function __isset($assignment)
    {
        return array_key_exists($assignment, $this->assignments);
    }

    public function __call($templateFunction, array $args)
    {
        if ( ! array_key_exists(strtolower($templateFunction), $this->functions) ) {
            throw new \Exception(
                sprintf('Unknown template function `%s`', $templateFunction)
            );
        }

        return call_user_func_array($this->functions[strtolower($templateFunction)], $args);
    }

}
templateFileName()

Die templateFileName()-Methode dient der Engine dazu den aktuellen Dateinamen des Templates abzurufen. Diese Methode kann außerdem innerhalb von Templates für Debugging-Zwecke genutzt werden.

Magische Methoden

Die in dieser Klasse definierten Magischen Methoden erlauben das dynamische Implementieren von öffentlichen Eigenschaften und Methoden. Die Methode __get() implementiert außerdem die Stringify-Auflösung der Engine und untersucht das Assignment nach Objekten. Das automatische Stringify ist in dieser Implementierung aber nur auf der obersten Ebene ( also dem direkten Zugriff auf das Assignment ) verfügbar. Du kannst dieses Verhalten aber später mit einer Template-Funktion für Arrays und andere Strukturen implementieren.

Der Test

In diesem Test-Beispiel wird Composer für das Autoloading Verwendet.

test.phtml

<h1>Hallo Welt!</h1>
<?= $this->render('@here/another-template', array('today' => $this->today)) ?>

another-template.phtml

<p>Heute ist der: <?= $this->today ?></p>

test.php

<?php

require __DIR__.'/vendor/autoload.php';

$engine = new TemplateEngine\Engine(array('here' => __DIR__));

$engine->stringify(DateTimeInterface::class, function(DateTimeInterface $dt) {
    return $dt->format('d.m.Y');
});

echo $engine->render('@here/test', array('today' => new DateTime));

Resultat:

<h1>Hallo Welt!</h1>
<p>Heute ist der: 30.11.2014</p>

Aus technischer Sicht sind den Möglichkeiten dieser Template-Engine keine Grenzen gesetzt. Du solltest dir aber darüber im klaren sein, was eine Template-Engine für Aufgaben übernehmen sollte und was eine Template-Engine nicht tun sollte. Sinn und Zweck von Template-Engines sollte in jedem Fall die strikte Trennung von Business- und Template-Logik sein.

Dieser Beitrag ist fertiggestellt und wurde zuletzt von hausl bearbeitet.

An diesem Beitrag waren bisher beteiligt: tr0y, hausl