Die Trennung von Logik und Ausgabe in einer (Web-)Anwendung ist eines der fundamentalen Prinzipien, um als Programmierer seine geistige Gesundheit zu behalten. Für die Ausgabe wird dabei eine zumeist einfache (Template-)Sprache verwendet, die verhindern soll, dass zu viel Logik in die Ausgabe wandert.

In PHP wird dies in der Regel von Template-Engines wie Twig oder Smarty gelöst. Aber warum benötigen wir eigentlich eine Template-Engine? Könnte es sein, dass PHP ohne weitere Zusätze auch eine ganz passable Lösung für Templating parat hält?

Die Hauptidee hinter den meisten Template-Engines ist:

  • Template-Engines bieten mit Vorsatz wenig Logik, um die Komplexität einer Programmierung an einer anderen Stelle zu bündeln, und Fehlerquellen zu vermeiden.
  • Dafür bieten Template-Engines einfach zu bedienende Kontrollstrukturen wie if, while und for.
  • Template-Engines kümmern sich um die korrekte Ausgabe von Variablen (Quoting / Escaping).
  • Template-Engines erlauben die mehrfache Verwendung von Template-Teilen (Snippets / Partials) in verschiedenen Kontexten.

Ein ausgezeichnetes Beispiel für die Philosophie von Template-Engines ist Mustache, das ein absolutes Minimum von Methoden anbietet, um Komplexität zu verhindern.

Aber brauchen wir in PHP den Overhead einer Template-Engine wirklich? Oder können wir nicht mit ein bisschen Disziplin direkt mit PHP alle unsere Template-Bedürfnisse stillen – ohne uns in Abhängigkeit zu einer weiteren Software-Bibliothek zu begeben?

Tatsächlich bietet PHP (auch aufgrund seiner Genese) sehr gute Möglichkeiten, Templating damit zu betreiben.

Dateiendungen

Template-Engines legen ihre Templates in der Regel in einen eigenen Ordner mit einer eigenen Dateiendung ab. In der Kombination Symfony mit Twig liegen Templates im Projekt in einem Ordner /templates und haben die Dateiendung .twig.

Tatsächlich hindert uns nichts daran, unsere PHP-Templates ebenfalls in einem /templates-Verzeichnis abzulegen. Als Datei-Endung verwenden wir dabei (analog zu Twig und zur Erhaltung der geistigen Gesundheit):

Template-Ausgabe Datei-Endung (MIME-Type)
HTML-Dateien *.html.php text/html
XML-Dateien *.xml.php text/xml oder application/xml
JSON-Dateien *.json.php application/json
Text-Dateien *.txt.php text/plain
CSV-Dateien *.csv.php text/csv

Diese Liste lässt sich natürlich beliebig um neue Dateitypen erweitern. Wichtig ist nur, dass die Dateien auf PHP enden, damit sie einerseits von einem Code-Editor als PHP-Dateien erkannt werden, andererseits beim irrtümlichen Aufruf über einen Webserver auch als PHP ausgeführt werden, und nicht irgendwelche PHP-Geheimnisse verraten.

Nach alter Sitte benennen wir das Template nach dem Controller. So wird das HTML-Template für den Index-Controller index.html.php, für den User-Controller user.html.php benannt. Das XML-Template für den Index-Controller ist dann entsprechend index.xml.php.

Templates, die keinen Controller zugeordnet werden können (sogenannte Snippets oder Partials) beginnen mit einem _, also zum Beispiel _meta.html.php. Was es mit diesen Dateien auf sich hat klären wir weiter unten.

Variablen-Ausgabe

Wir nutzen die Fähigkeit von PHP, dass jede Anweisung, die nicht in PHP-Tags (<?php ?>) steht, nicht als PHP verstanden wird, sondern direkt an den Browser zurückgegeben wird. Damit können wir innerhalb unserer Template-Dateien uns tatsächlich auf die Ausgabe konzentrieren:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
</head>
<body>
  <!-- I am valid PHP -->
</body>

Die wichtigste Funktion eines Templates ist die Ausgabe von Variablen, die im Controller vorbereitet wurden. Dazu öffnen wir (wie in jeder Template-Sprache) einen Kommandoblock, und führen darin das Kommando zum Ausgeben von Variablen aus.

<!-- Bad idea -->
<div><?php echo($title) ?></div>

Netterweise gibt es eine PHP-Tag, das diese Ausgabe nochmals verkürzt:

<!-- Still a bad idea -->
<div><?= $title ?></div>

Tatsächlich sind aber beide Wege eine ganz schlechte Idee, weil wir hier sträflich das Quoting / Escaping vernachlässigt haben – eine Kernfunktionalität einer jeden Template-Engine und ein Garant für die Sicherheit unserer Web-Applikation. Im obigen Beispiel wäre es möglich, dass in $title HTML steckt, was direkt wieder auf der Seite ausgegeben werden kann. In den wenigsten Fällen ist dies erwünscht und setzt voraus, dass man dem Variablen-Inhalt jederzeit vertrauen kann. Beispiel gefällig?

<!-- See, a bad idea -->
<div><?= $_GET['search'] ?></div>

Praktischerweise liefert PHP die Funktion htmlspecialchars mit, die einen String für die Ausgabe in sicherem HTML umwandelt. So werden Zeichen wie <, > und " sicher umgewandelt:

<!-- Better idea -->
<div><?= htmlspecialchars($title) ?></div>

Da diese Art der Ausgabe unsere Hauptmethode sein soll, die Schreibweise aber etwas sperrig ist, bauen wir uns einen Alias für htmlspecialchars. Der Beginn der kleinsten PHP-Template-Engine der Welt:

// Template.php

/**
 * Alias for `htmlspecialchars`
 */
function html($s): string
{
  return htmlspecialchars($s);
}

Diese Funktion kürzt uns die Schreibarbeit ab, um eine Variable sicher auszugeben, und ist ab sofort unsere Hauptfunktion zur Ausgabe von Variablen:

<!-- Best idea -->
<div><?= html($title) ?></div>

Weiteres Quoting

PHP bietet aber noch mehr Funktion für Quoting / Escaping. Unter anderem müssen wir bei der Ausgabe von Query-Parametern in URLs Zeichen wie # und & umwandeln. Dafür hat PHP die Funktion urlencode parat – in Kombination mit unserer html-Funktion unschlagbar:

<a href="https://www.example.com?id=<?= html(urlencode($id)) ?>">Test</a>

Dann und wann müssen wir auch PHP-Variablen in JavaScript-Variablen umwandeln. Hier bietet sich die (etwas artfremde) Funktion json_encode an, die PHP-Variablen als JSON ausgibt. Damit wird die Variable nicht nur in korrekte Anführungszeichen gesetzt (die wir wiederum nicht HTML-enkodieren dürfen), sondern erlaubt sogar die Ausgabe von komplexen Variablen wie Arrays:

<script>
var data = <?= json_encode($data) ?>;
</script>

Bedingungen

Die wichtigsten Kontrollstrukturen für das Templating sind Bedingungen. Hier ist das if der wichtigste Ansprechpartner. „Echtes“ PHP mit seinen geschweiften Klammern ist dabei aber eher unübersichtlich, da zum Beispiel bei Schachtelungen unklar sein kann, welche schließende Klammer für welchen Block zuständig ist:

<?php if (!empty($title)) { ?>
  <div><?= html($title) ?></div>
<?php } ?>

Praktischerweise kennt PHP auch eine alternative Syntax für Kontrollstrukturen, bei dem jeder Block hinter einem Kommando nicht mit einem {, sondern mit einem : beginnt – und statt mit einem } mit einem end...; beendet wird.

<?php if (!empty($title)): ?>
  <div><?= html($title) ?></div>
<?php endif ?>

In den if-Bedingungen kann man dabei alle Operatoren und Funktionen verwenden, die PHP so kennt. Wichtig ist (wie immer), dass vor dem Verwenden einer Variable getestet wird, ob die Variable überhaupt definiert wird, was mit !empty oder isset geschehen kann.

Darüber hinaus stehen natürlich auch else und elseif zur Verfügung:

<?php if (!empty($results)): ?>
  <h4>Wir haben <?= html(count($results)) ?> Ergebnisse gefunden.</h4>
<?php else: ?>
  <h4>Wir haben leider keine Ergebnisse gefunden.</h4>
<?php endif ?>

Schleifen und Arrays

Die Ausgabe von Listen und Tabellen ist ebenfalls eine wichtige Funktion von Templates. Mittels Schleifen kann man zum Beispiel über vorher definierte Arrays iterieren – ebenfalls mit der alternative Syntax für Kontrollstrukturen:

<?php if (!empty($list)): ?>
  <ul>
    <?php foreach($list as $index => $item): ?>
      <li><?= html($item) ?></li>
    <?php endforeach ?>
  </ul>
<?php endif ?>

Dabei funktionieren for, foreach und while ganz wunderbar:

<h4>Lottozahlen</h4>
<ol>
  <?php for ($i = 1; $i <= 49; $i++): ?>
    <li>
      <input type="checkbox" name="lotto_<?= html($i) ?>" id="lotto_<?= html($i) ?>" value="1" />
      <label for="lotto_<?= html($i) ?>"><?= html($i) ?></label>
    </li>
  <?php endfor ?>
</ol>

Snippets / Partials

Um Template-Teile wie zum Beispiel einen Header, Footer oder Navigation in ein Template einzufügen, fügen wir dem Dateinamen des Template-Teils einfach einen führenden _ hinzu, wie zum Beispiel _header.xml.php.

Ein Template-Teil (auch als Snippet oder Partial bekannt) kann mit dem PHP eigenen include ins ein anderes Template eingebunden werden:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
  <?php include('_meta.html.php') ?>
</head>
<body>
  <?php include('_header.html.php') ?>
  ...
  <?php include('_footer_.html.php') ?>
</body>

Übersetzung

Sogar Übersetzungen von Templates beherrscht PHP! Die in PHP vorhandene Gettext-Bibliothek erlaubt es, einen String zu übergeben, für den Gettext in einem beigefügten Wörterbuch nach der korrekten Übersetzung sucht, und diesen zurückgibt.

Wichtig dafür ist das Setzen der Locale in PHP mittels setlocale:

setlocale(LC_MESSAGES, 'de_DE');

Die korrekte Bedienung dieser Funktion sorgt übrigens auch für die Übersetzung von Zahlen- und Datumsformaten, wie auch Datumsbezeichnern.

Die eigentlich zu übersetzenden Bezeichner können an die Funktion gettext bzw. _ übergeben werden.

<!-- translate to 'Guten Morgen!' -->
<h4><?= html(_('Good morning!')) ?></h4>

Die dafür notwendigen Wörterbücher kann man spielend leicht selber erstellen. Die dafür notwendigen PO- und MO-Dateien kann man zum Beispiel mit Poedit erstellen. Poedit ist sogar in der Lage, die gesamte PHP-Programmierung nach noch nicht katalogisierten, übersetzbaren Strings zu durchsuchen.

Wenn wir mit Variablen-Ersetzung arbeiten, wird die Konstruktion etwas schwieriger:

<div><?= html(vsprintf(_('There are %d results'), [$count])) ?></div>

Da wir so ziemlich jeden hardcodierten Text übersetzbar machen wollen, können wir unserer simplen Template-Engine eine weitere Funktion als Abkürzung hinzufügen:

// Template.php

/**
 * Alias for `vsprintf`, but with HTML escaping and translation
 */
function _html(string $format, array $args = []): string
{
  return htmlspecialchars($args
    ? vsprintf(_($format), $args)
    : _($format)
  );
}

…was den Aufruf deutlich einfacher macht:

<h4><?= _html('Good morning!') ?></h4>
<div><?= _html('There are %d results', [$count]) ?></div>

Variablen-Dumping

Für Entwickler wie auch Template-Designer ist es hilfreich, sich komplexe Variablen im Frontend zu Debugging-Zwecken ausgeben zu lassen. Dafür bietet PHP die Funktionen print_r und var_dump. Dummerweise ist in HTML ein solcher Dump schwer lesbar, da die Zeilenumbrüche in HTML ignoriert werden. Eine kleine Funktion für unsere Template-Engine kann auch diesen Missstand beheben:

// Template.php

/**
 * HTML dumper für PHP variables
 */
function debug($mixed, bool $extended = false): void
{
  echo('<pre class="debug" style="margin: 1em 0; border: 1px solid red; background: #fee; padding: 1em; ">');
  if (!$extended) {
    echo(htmlspecialchars(print_r($mixed, 1)));
  } else {
    var_dump($mixed); // HTML errors may occur
  }
  echo('</pre>');
}

Damit können auch komplexe Variablen bis hin zu Objekten lesbar ausgegeben werden:

<?php debug($data) ?>

Ausgabe von anderen Content-Typen

PHP erzeugt von Haus aus eine HTML-Ausgabe. Im Browser funktioniert dies so, dass jedes PHP-Skript den Webserver anweist, als MIME-Type text/html zurückzugeben. Tatsächlich kann PHP aber auch ganz andere MIME-Types ausgeben. Dazu gibt es die Funktion header, mit der PHP den Webserver anweisen kann, bestehende Header zu ändern bzw. neue zu setzen. Der für uns interessante Header ist Content-Type.

So können wir PHP anweisen, jeden erdenklichen MIME-Type zurückzugeben.

XML

Für XML verwenden wir die Template-Endung .xml.php, also zum Beispiel sitemap.xml.php. Diese Templates unterscheiden sich tatsächlich nicht großartig von HTML-Templates, und verwenden sogar die selben Quoting/Escaping-Funktionen. Nur der zusätzliche Content-Type-Header muss mitgeschickt werden:

<?php header('Content-Type: text/xml'); ?>
<urlset>
  <?php foreach($urls as $url): ?>
    <url>
      <loc><?= html($url) ?></loc>
    </url>
  <?php endforeach ?>
</urlset>

JSON

Auch JSON kann PHP für uns ausgeben. Hierfür verwenden wir die Template-Endung .json.php, wie zum Beispiel feed.json.php, und senden wieder den korrekten Content-Type. Um das Quoting/Escaping in den Griff zu bekommen, bauen wir unser JSON nicht von Hand, sondern verwenden die Funktion json_encode, die auch strukturierte PHP-Variablen strukturiert und proper formatiert ausspuckt:

<?php
  header('Content-Type: application/json');
  echo(json_encode($data));

Für besonders schönes JSON kann man übrigens den Parameter JSON_PRETTY_PRINT mitgeben:

<?php
  header('Content-Type: application/json');
  echo(json_encode($data, JSON_PRETTY_PRINT));

Übrigens respektiert die Funktion json_encode tatsächlich PHP-Typen. Damit werden PHP-Integer tatsächlich als JSON-Integer ausgegeben, und PHP-Strings als JSON-Strings. Hier muss also in PHP den Unterschied zwischen 1 und „1“ berücksichtigt werden.

Fazit

Jedes PHP-Projekt braucht eine saubere Trennung zwischen Logik und Ausgabe – aber für diese Trennung braucht es nicht immer eine Template-Engine wie Twig oder Smarty. Auch handelsübliches PHP kann (mit wenigen, kleinen Helferlein) eine performante, wartungsarme Alternative fürs Templating sein.

Hier nochmals eine Übersicht über die wichtigsten PHP-Funktionen für Templating:

Funktion Alias Beschreibung
htmlspecialchars html HTML-Escaping
urlencode - URL-Escaping
json_encode - JSON/JavaScript-Escaping
setlocale - Setzen der Sprache für Übersetzungen
gettext / _ _html Ausgabe von Übersetzungen
strftime - Gibt ein lokales Datumsformat aus
localeconv - Ermittelt die lokale Ausgabeeinstellungen für Zahlen
printf _html Ausgabe von Variablen in Strings
nl2br - Konvertiert Zeilenumbrüche in HTML-<br />
implode - Verbindet ein Array zu einem String
var_dump debug Dump von PHP-Variablen
header - Ausgabe von anderen Content-Type

Einzig beim Thema Vererbung von Templates sind echte Template-Engines deutlich komfortabler, wie Twigs Template-Vererbung und Smartys Template-Vererbung zeigen.