Templates mit PHP ganz einfach

Autor: Peter Bieling - 18.07.2025 (Code-Beispiele für Smarty, PHP-Template, Twig und Moustache KI generiert)

Am Anfang diente PHP hauptsächlich dazu, dynamische Elemente innerhalb von statischem HTML auszugeben. So konnte man z.B. die letzte Aktualisierung oder einen Besucherzähler leicht realisieren. Später ging man dazu über, das HTML von PHP aus zu generieren und dann mit echo die Seite auszugeben. Doch wie man es auch drehte und wendete: Der Mischmasch aus PHP und HTML bestand weiter.

Sogenannte Template-Engines mit eigenen Sprachen sollten das Problem lösen und das PHP aus dem HTML heraushalten oder zumindest reduzieren. Das brachte den Vorteil, dass PHP- und HTML-Entwickler weitgehend getrennt arbeiten konnten.

Smarty, der Klassiker

Hier ein einfaches Beispiel mit der altbewährten Template-Engine Smarty:

<!DOCTYPE html>
<html lang="de">
<head>
    <meta charset="UTF-8">
    <title>Einfaches Smarty-Template</title>
</head>
<body>
    <h1>Benutzerprofil</h1>
    <p><strong>Name:</strong> {$name}</p>
    <p><strong>Alter:</strong> {$age}</p>
    <p><strong>Stadt:</strong> {$city}</p>
    <p><strong>Beruf:</strong> {$job}</p>
    <p><strong>Status:</strong> {$status}</p>
</body>
</html>

Auf PHP-Seite wird dafür dieser Code gebraucht:

<?php
require_once 'vendor/autoload.php';

$smarty = new Smarty();

// Template-Verzeichnisse setzen
$smarty->setTemplateDir(__DIR__ . '/templates');
$smarty->setCompileDir(__DIR__ . '/templates_c'); // Sicherstellen, dass das Verzeichnis existiert

// Platzhalter-Daten setzen
$smarty->assign('name', 'Anna Müller');
$smarty->assign('age', 30);
$smarty->assign('city', 'Berlin');
$smarty->assign('job', 'Webentwicklerin');
$smarty->assign('status', 'Aktiv');

// Template rendern
$smarty->display('simple.tpl');

Hier ist die Welt noch eingermaßen in Ordnung. Auch der Nichtprogrammierer kann sehen, was er ändern darf und was nicht.

Sobald es aber komplizierter wird, schwindet auch die Übersicht. Bleiben wir noch kurz bei Smarty und beginnen mal mit dem übersichtlichen PHP-Code:

<?php
require_once 'vendor/autoload.php';

$smarty = new Smarty();

// Beispiel-Daten
$data = [
    ['name' => 'Anna', 'alter' => 30, 'stadt' => 'Berlin'],
    ['name' => 'Ben', 'alter' => 25, 'stadt' => 'München'],
    ['name' => 'Carla', 'alter' => 28, 'stadt' => 'Berlin'],
];

// Template-Verzeichnisse (optional anpassen)
$smarty->setTemplateDir(__DIR__ . '/templates');
$smarty->setCompileDir(__DIR__ . '/templates_c'); // sicherstellen, dass dieses Verzeichnis beschreibbar ist

// Variablen an Smarty übergeben
$smarty->assign('data', $data);

// Template rendern
$smarty->display('table.tpl');

Damit schafft man es die Daten von der Darstellung zu trennen. Aber es wurde eine Hürde aufgebaut: Immer wenn das Wort 'Berlin' für den key 'stadt' erscheint, soll noch eine Tabellenzeile mit dem Wort Highlight vorgesetzt werden. Das sieht dann im Template so aus:

<table border="1" cellspacing="0" cellpadding="5">
    <thead>
        <tr>
            {if $data|@count > 0}
                {foreach from=$data[0] key=k item=v}
                    <th>{$k|capitalize}</th>
                {/foreach}
            {/if}
        </tr>
    </thead>
    <tbody>
        {foreach from=$data item=row}
            {if isset($row.stadt) and $row.stadt == 'Berlin'}
                <tr>
                    <td colspan="{count($row)}"><strong>Highlight</strong></td>
                </tr>
            {/if}
            <tr>
                {foreach from=$row item=cell}
                    <td>{$cell}</td>
                {/foreach}
            </tr>
        {foreachelse}
            <tr>
                <td colspan="100%">Keine Daten verfügbar</td>
            </tr>
        {/foreach}
    </tbody>
</table>

Nun wurde es dem HTML-Entwickler alter Schule schon etwas mulmig. Zurecht, denn wie man sieht, ist hier durch Smarty nicht viel gewonnen.

PHP als Templatesprache - oder lieber nicht?

Mit reinem PHP-HTML-Template sieht das ähnlich aus:

<?php
// Beispiel-Daten
$data = [
    ['name' => 'Anna', 'alter' => 30, 'stadt' => 'Berlin'],
    ['name' => 'Ben', 'alter' => 25, 'stadt' => 'München'],
    ['name' => 'Carla', 'alter' => 28, 'stadt' => 'Berlin'],
];
?>
<!DOCTYPE html>
<html lang="de">
<head>
    <meta charset="UTF-8">
    <title>Tabelle mit Highlight</title>
    <style>
        table {
            border-collapse: collapse;
        }
        table, td, th {
            border: 1px solid black;
            padding: 5px;
        }
        .highlight {
            background-color: #ffffcc;
            font-weight: bold;
            text-align: center;
        }
    </style>
</head>
<body>
    <table>
        <thead>
            <tr>
                <?php if (!empty($data)) : ?>
                    <?php foreach (array_keys($data[0]) as $key): ?>
                        <th><?= htmlspecialchars(ucfirst($key)) ?></th>
                    <?php endforeach; ?>
                <?php endif; ?>
            </tr>
        </thead>
        <tbody>
            <?php if (!empty($data)) : ?>
                <?php foreach ($data as $row): ?>
                    <?php if (isset($row['stadt']) && $row['stadt'] === 'Berlin') : ?>
                        <tr>
                            <td class="highlight" colspan="<?= count($row) ?>">Highlight</td>
                        </tr>
                    <?php endif; ?>
                    <tr>
                        <?php foreach ($row as $cell): ?>
                            <td><?= htmlspecialchars($cell) ?></td>
                        <?php endforeach; ?>
                    </tr>
                <?php endforeach; ?>
            <?php else: ?>
                <tr>
                    <td colspan="100%">Keine Daten verfügbar</td>
                </tr>
            <?php endif; ?>
        </tbody>
    </table>
</body>
</html>

Das ist dann auch für viele Entwickler der vernünftige Weg.

Twig, bekannt durch das Symfony-Framework

Wie sieht es bei Twig, der überwiegend von Symfonie genutzen Templatesprache aus? Der PHP-Aufruf ist ähnlich wie bei Smarty, daher hier nur das Twig-Template:

<table border="1" cellspacing="0" cellpadding="5">
    <thead>
        <tr>
            {% if data|length > 0 %}
                {% for key in data|first|keys %}
                    <th>{{ key|capitalize }}</th>
                {% endfor %}
            {% endif %}
        </tr>
    </thead>
    <tbody>
        {% for row in data %}
            {% if row.stadt is defined and row.stadt == 'Berlin' %}
                <tr>
                    <td colspan="{{ row|length }}"><strong>Highlight</strong></td>
                </tr>
            {% endif %}
            <tr>
                {% for cell in row %}
                    <td>{{ cell }}</td>
                {% endfor %}
            </tr>
        {% else %}
            <tr>
                <td colspan="100%">Keine Daten verfügbar</td>
            </tr>
        {% endfor %}
    </tbody>
</table>

Große Unterschiede zu den vorigen Beispielen sind hier nicht zu entdecken.

Mustache - Logik light

Eine Template-Engine, nämlich Mustache.php, verfolgt jedoch ein etwas anderes Konzept und verzichtet auf direkte Programmierung im Template, dennoch gibt es Anweisungen für Schleifen und optionale Element.

Mustache-Template:

<table border="1" cellspacing="0" cellpadding="5">
  <thead>
    <tr>
      {{#headers}}
        <th>{{.}}</th>
      {{/headers}}
    </tr>
  </thead>
  <tbody>
    {{#rows}}
      {{#highlight}}
        <tr>
          <td colspan="{{colspan}}"><strong>Highlight</strong></td>
        </tr>
      {{/highlight}}
      <tr>
        {{#cells}}
          <td>{{.}}</td>
        {{/cells}}
      </tr>
    {{/rows}}
    {{^rows}}
      <tr>
        <td colspan="100%">Keine Daten verfügbar</td>
      </tr>
    {{/rows}}
  </tbody>
</table>

Die Vorarbeiten in PHP sind größer:

<?php
require 'vendor/autoload.php';

$mustache = new Mustache_Engine([
    'loader' => new Mustache_Loader_FilesystemLoader(__DIR__ . '/templates'),
]);

// Rohdaten
$data = [
    ['name' => 'Anna',  'alter' => 30, 'stadt' => 'Berlin'],
    ['name' => 'Ben',   'alter' => 25, 'stadt' => 'München'],
    ['name' => 'Carla', 'alter' => 28, 'stadt' => 'Berlin'],
];

// Header aus dem ersten Datensatz ableiten
$headers = array_keys($data[0] ?? []);

// Aufbereitung für Mustache
$rows = [];
foreach ($data as $row) {
    $highlight = isset($row['stadt']) && $row['stadt'] === 'Berlin';

    $rows[] = [
        'highlight' => $highlight,
        'colspan'   => count($row),
        'cells'     => array_values($row),
    ];
}

// Template rendern
echo $mustache->render('table', [
    'headers' => $headers,
    'rows'    => $rows,
]);

PbTpl - ganz ohne Logik im Template

Die Sache wäre aber auch nicht komplizierter, wenn man auch noch den letzten Schritt gehen würde, und nicht nur PHP sondern auch die letzten Bauanleitungen - Schleifen und optionale Elemente - aus dem Template selbst herauszunehmen. - Wie kann das gehen?

PbTpl schlägt einen anderen Weg ein als die genannten Template-Engines: Eine Templatedatei kann mehrere Templates enthalten, die häufig einfache Schnipsel sind und durch Bezeichner in der Templatedatei kenntlich gemacht werden. Ein Template-Objekt enthält alle Templates, die in einer Template-Datei stehen. Mehre Template-Objekte können kombiniert werden, um z.B. eine Webseite zu erzeugen.

Der Bezeichner [main] wird meist für das äußere Template verwendet. In diesemm Template finden sich dann Platzhalter, die dann entweder mit einfachen Inhalten oder mit weiteren, bereits gefüllten Templates gefüllt werden. Das ist ähnlich wie bei einem Verzeichnis, das Dateien oder weitere Verzeichnisse enthalten kann.

Bei der Arbeit mit den Templates gibt es jedoch die Besonderheit, dass ein Template mehrfach verwendet werden kann, um z.B. die Reihen einer Tabelle zu erzeugen.

Um die Sache verständlich zu mchen, baue ich das bisherige Beispiel mit PbTpl nach. (siehe Kasten rechts, bzw. unten bei kleineren Displays)

Ein Beispiel mit PbTpl

PbTpl nutzt die schnelle str_replace-Funktion von PHP.

Es gibt keine Programmierung im Template, keine Kontrollstrukturen und auch keine regulären Ausdrücke.

Beim Einlesen der Templatdatei wird ein Objekt erzeugt, das die in der Datei enthaltenen Templates, bzw. Template-Snippets für Manipulationen verfügbar macht.

Wir verwenden wieder das bisherige Beispiel und zeigen, wie es geht.

[main]
<!DOCTYPE html>
<html lang="de">
<head>
    <meta charset="UTF-8">
    <title>Tabelle mit Highlight</title>
    <style>
        table {
            border-collapse: collapse;
        }
        table, td, th {
            border: 1px solid black;
            padding: 5px;
        }
        .highlight {
            background-color: #ffffcc;
            font-weight: bold;
            text-align: center;
        }
    </style>
</head>
<body>
    <table>
        <thead><tr>
{TH_ROW}
            </tr></thead>
        <tbody>
{TBODY}
        </tbody>
    </table>
</body>
</html>
############# End of main-Template ####

### Zeile wird nur eingefügt, wenn die Stadt 'Berlin' folgt.
[highlight_row]
<tr><td class="highlight" colspan="{NUMBER_OF_COLS}">Highlight</td></tr>

### head ####
[th_cells]
<th>{VALUE}</th>

### body #####
[td_row]
<tr>{TD_CELLS}</tr>

[td_cells]
<td>{VALUE}</td>

Wie wir sehen, wurde die Seite in Einzelteile zerlegt. Elemente die vorher verschachtelt waren, stehen jetzt - technisch gesehen - gleichwertig nebeneinander. Die Elemente wissen nichts voneinander. Durch sinnvolle Anordnung und Kommentierung kann man aber Übersicht und Klarheit schaffen.

Die ganze Logik steckt jetzt also in unseren PHP-Programmcode, der für unser Beispiel so aussehen kann.

require dirname(__DIR__) . '/vendor/autoload.php';
use PbClasses\PbTpl;
use PbClasses\Util\Filter;

$tplFile = __DIR__ . '/templates/example-table.tpl';

// Beispiel-Daten
$data = [
    ['name' => 'Anna', 'alter' => 30, 'stadt' => 'Berlin'],
    ['name' => 'Ben', 'alter' => 25, 'stadt' => 'München'],
    ['name' => 'Carla', 'alter' => 28, 'stadt' => 'Berlin'],
];

$t = new PbTpl($tplFile);

$seRe = [];
$uppercaseHead = Filter::numericArr(array_keys($data[0]), 'STRTOUPPER');
$seRe['th_row'] = $t->fillRowTpl('th_cells', 'value', $uppercaseHead);

$seRe['tbody'] = '';
foreach($data as $row) {
    if ($row['stadt'] === 'Berlin') {
       $seRe['tbody'] .= $t->fillTpl('highlight_row', 'number_of_cols', count($row));
    }
    //htmlspecialchars auf das ganze Array anwenden
    $specCharValues = Filter::numericArr(array_values($row), 'HTMLSPECIALCHARS');
    $tdCells = $t->fillRowTpl('td_cells', 'value', $specCharValues);
    $seRe['tbody'] .= $t->fillTpl('td_row', 'td_cells', $tdCells);
}
echo $t->fillTpl('main', $seRe);

Verglichen mit dem für Mustache benötigten PHP-Code unterscheidet sich die Komplexität kaum.

Das Template selbst ist jedoch dadurch grundlegend anders, dass die Kontrollstrukturen bei PbTpl fehlen.

Weitere Beispiele

Das gezeigte Beispiel behandelt schon einen relativ komplizierten Fall. Häufig hat man es mit einfacheren Strukturen zu tun, die leicht von der Hand gehen.

Bei einer festen Tabelle, für die ein eigenes Template erstellt wird, ist es auch nicht immer nötig, die Zeilen aus kleinsten Teilen zusammenzusetzen.

Weiter Beispiele gibt es hier:

PbTpl-Beispiele auf meiner Website.

Der Code für die gesamte Demo ist bei Github zu finden, genau wie eine Mini-Version meines Frameworks PbClasses mit den benötigten Klassen.

Auf Github findet sich auch eine Anleitung, wie man eine Testumgebung mit der Demo per Docker installieren kann.

Links siehe unten auf der Seite oder über die eben verlinkte PbTpl-Demo.

Was kann PbTpl noch?

Wer sich die Klasse genauer ansieht, findet einige weitere Methoden.

Besonders häufig verwende ich die Methode hasTpl(). Damit ist es möglich, zu prüfen, ob ein Template vorhanden ist oder nicht.

Ein einfaches Beispiel wäre ein HTML-Block mit einem Standardtext
[info_default]
Dieser Block kann durch einen Block
[info_custom]
ersetzt werden. Mit $t->hasTpl('info_custom') kann ich prüfen, ob von dieser Möglichkeit Gebrauch gemacht wurde. Wenn ja, kann ich in meinem PHP-Code dafür sorgen, dass dieses Template-Snippet genutzt wird, ansonsten das Standard-Snippet.

In meinem Tabellensystem können Standard-Template-Schnipsel für bestimmte Tabellenzellen durch individuelle Templates ersetzt werden. In diesem Fall geschieht die Eretzung dann automatisch.

PbTpl halte ich für ideal für die Verwendung in CMS oder ähnlichen Systemen, um auf einfache Weise Anpassungen vornehmen zu können. Es bietet sich an, den PHP-Code für häufig benötigte Strukturen in Klassen auszulagern. So habe ich z.B. ein Umfqangreiches System für die Arbeit mit Tabellen, das ebenfalls die Templateklasse verwendet.

Natürlich gibt es auch Möglichkeiten, das Templateobjekt dynamisch zu verändern, einzelne Templates hinzuzufügen oder zu entfernen. Ich persönlich habe so etwas allerdings bisher nicht weiter benötigt. Bestimmt gibt es aber Anwendungsfälle, für die so etwas sinnvoll ist.

Man kann auch das Template-Objekt mit einem String statt mit einer Datei erzeugen. Das bietet sogar die Möglichkeit, eine Templatedatei vor der Verwendung noch mit einfachem str_replace() zu modifizieren bzw. aus Teilen zusammenzusetzen.

Wichtig ist auch noch, dass PbTpl nicht auf HTML beschränkt ist, sondern sich für alle Arten von Textformaten eignet besonders auch für XML. So ist es oft einfacher und übersichtlicher, ein XML-Template zu erstellen und Daten auszutauschen als das Dokument nach jeder Modifizierung neu zu generieren.

Fazit

Ob PbTpl für Sie passend ist oder ob Sie eine mächtige Template-Engine wie Twig bevorzugen, müssen Sie selbst entscheiden, sofern überhaupt eine Entscheidung ansteht. Natürlich haben die großen Template-Engines umfangreiche Features, um fast jeden Wunsch zu erfüllen. Allerdings geht das auch auf Kosten der Performance.

PbTpl ist vor allem eine Alternative für Entwickler, die in ihren Projekten meist auf eine Template-Engine verzichtet haben, sich aber eine klarerer Trennung von HTML und Programmcode wünschen.

Die Klasse ist klein und kann auch noch für eigene Wünsche angepasst oder erweitert werden, sollte man an die Grenzen kommen.