A fundamental principle of building (web) applications is to have a strict separation between logic and output – at least if your goal as a programmer is to stay sane. For outputting stuff most often there is a simple (template) language to prevent mixing too much logic into your templates.

For PHP template engines like Twig or Smarty solve the outputting part. But do we really need a template engine? Is it possible that PHP (without any additional libraries) is quite sufficient to do templating?

The main idea of template engines is:

  • Template engines remove logic on purpose – to concentrate complexity in your programming in a different spot, and to remove additional points of error.
  • The remaining control structures like if, while or for are more simple to use in a template engine.
  • Template engines take care of proper encoding / quoting whenever you are outputting a variable.
  • Template engines support re-use of template parts (called snippets or partials) in different context.

A perfect example for this philosophy is the template engine Mustache: It only contains a minimum of required methods in order to reduce complexity.

But do we really need the overhead of a proper template engine? Or is it possible to use plain PHP for our templating needs, without adding dependencies like template engines to our project?

Actually PHP has good capabilities to do templating – its founding idea revolves around templating, as a matter of fact.

File Extensions

It is common practice to put all your templates into a separate folder, and append a common file extension to all template files. For example Symfony with Twig puts all templates into a directory called /templates and use the file extension .twig.

Actually there is nothing stopping us from using the same /templates directory for our templates. As file extension we are using (like Twig and to keep our mental health):

Template output File extension (MIME-type)
HTML file *.html.php text/html
XML file *.xml.php text/xml or application/xml
JSON file *.json.php application/json
Text file *.txt.php text/plain
CSV file *.csv.php text/csv

This list is easily extended be even more MIME-types and corresponding file extensions. Important for us is that all files end with .php, so our code editor knows how to handle these files. This also secures the files if by mistake these files get called directly by a web server; files ending with .php will be executed as PHP, and their PHP secrets will be compiled and not shown to a world-wide audience.

It is good custom to name your template after the controller calling it. Accordingly a HTML template for an index-controller will be index.html.php, for an user-controller user.html.php. The XML template for the index-controller becomes index.xml.php.

Templates not attached to a controller (called snippets or partials) will be prefixed with a _, e.g. _meta.html.php. The purpose of these files will be explained later on.

Variable Output

We will take advantage of the fact that PHP returns every line of code not enclosed in PHP tags (<?php ?>) unaltered back to the browser. That allows us to concentrate on building our beautiful template output:

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

The most important task of templates is to output variables prepared by the controller. Like in every template language we open up a instruction block and execute a function for outputting a variable.

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

PHP kindly allows for shortening the output instruction:

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

But actually the above examples are very bad ideas, because both ignore quoting / escaping – a core function of any good template engine, because only this secures variable output against malicious intentions. Look at the above examples and imagine some HTML tags (or characters like a single <) inside $title – as you can see these will be embedded into the HTML output. In most cases this is undesirable, possibly breaking your site or (even worse) allowing attackers to make your site send harmful HTML to your visitor's browsers. Want to see an example?

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

But PHP has you covered: htmlspecialchars converts any string to safe HTML (or XML, for that matter). Characters like <, > or " will be safely encoded:

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

Because this main method of outputting variables will become tedious to write, we are introducing an alias for htmlspecialchars – the beginning of the world's smallest PHP template engine:

// Template.php

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

This function shortens our work to convert variables and will be our new main function to send variable content back to the browser:

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

More Quoting

PHP offers even more function for quoting / escaping. There is a function for encoding query parameters used in URLs, quoting characters like # or &: urlencode. These can be easily combined with our html function:

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

Sometimes we also have to convert our PHP variables to JavaScript variables. Not explicitly build for that purpose but highly recommendable is a function called json_encode. It actually converts the given variable to JSON – which is also valid JavaScript. Even better the function also converts PHP arrays and objects to JavaScript arrays and objects:

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

Conditions

The most important control structure in templates are conditions, with your best friend if to be mentioned first. But „pure“ PHP with its curly brackets used in conjunction with if tends to make matters confusing in your template, making it hard to see where your conditional block ends:

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

Do not despair, PHP has an alternative syntax for controls structures tailored for templating tasks. Instead of starting a block via { a simple : is used. And instead of ending a block via } something like end...; will be used:

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

Your if-conditions can use any operators and functions known to PHP. One of your most important conditions in templating will be a check if a given variable has content to output. This test is simply executed by using !empty or isset.

Of course there is also else and elseif available:

<?php if (!empty($results)): ?>
  <h4>We have found <?= html(count($results)) ?> results.</h4>
<?php else: ?>
  <h4>Sorry, we have found no results.</h4>
<?php endif ?>

Loops and Arrays

Then there are lists and tables in your templates. With PHP loops your are able to iterate over an PHP array to output lists and tables. Lucky for you there is also an alternate syntax for looping control structures:

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

This syntax does a proper job for for, foreach or while:

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

Snippets / Partials

Partial templates present in multiple controllers (like a header, footer or navigation) are called… partials (doh!) or snippets. These partials are prefixed with a single _, for example _header.xml.php.

These partials or snippets can be embedded into other templates by PHP's very own include instruction:

<!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>

Translations

You never would have guessed, but PHP also handles translations! The Gettext library is included in PHP and allows to translate strings, which Gettext will look up in a dictionary you are able to build. If a translation is found, Gettext will return it to PHP.

Most important for translations is to tell PHP the language / locale you are using. This is done via setlocale:

setlocale(LC_MESSAGES, 'de_DE'); // German in Germany
setlocale(LC_MESSAGES, 'en_GB'); // English in Great Britain

This also switches to the correct format for numbers and dates in the given language, as well as using the correct date terms for months and weekdays.

The strings you want translated are passed to the gettext / _ function:

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

The required dictionaries to translate your strings can be created by yourself. The required PO- and MO-files can be created using programmes like Poedit. Poedit also offers to search your whole project for translatable strings to automatically add them to your dictionaries.

If your happen to add variables to your translatable strings things can get a little hairy. This somewhat complex construction allows for a translatable string to also work with variables:

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

So here is our next alias to be added to our simple template engine: A shortcut for outputting translatable strings mixed with variables.

// 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)
  );
}

…which makes writing translatable strings mixed with variables somewhat less tedious:

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

Variable Dumping

It is very helpful for developers as well as template builders alike to have a look into complex variable constructs for debugging purposes – think of big objects or arrays to traverse. PHP offers print_r and var_dump / var_export for this purpose. These functions are helpful, but not in HTML as they do not produce any line breaks, dumping all your structured data into a single line. Again our little template engine comes to the rescue with a small shortcut:

// 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; color: #000; padding: 1em;">');
  echo(htmlspecialchars($extended
    ? var_export($mixed, true)
    : print_r($mixed, true)
  ));
  echo('</pre>');
}

This allows for complex variables to be dumped in a readable manner:

<?php debug($data) ?>

While your at it, you might consider using console.log for debugging output in the console of your browser.

Outputting different Content-Types

PHP was build to output HTML. This works by PHP telling the browser that the MIME-type of the document it is sending is text/html. But actually PHP is perfectly capable to send any other MIME-type header. The function header allows PHP to send custom headers to your visitor's browsers, replacing default headers while your at it.

The header used to set the MIME-type is Content-Type. So it is perfectly possible for PHP to send any conceivable MIME-type.

XML

For XML we choose the file extension .xml.php, for example in sitemap.xml.php. This template does not differ very much from a standard HTML template, and uses the same quoting / encoding routines. We just have to send a different Content-Type header and we are serving proper XML:

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

JSON

Outputting JSON is also quite simple with PHP. For this MIME-type we are using the file extension .json.php (like in feed.json.php) and need to set the correct Content-Type. To properly use quoting / escaping the inbuilt PHP function json_encode is very helpful, as it converts even complex PHP variables into structured JSON output:

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

If you like your JSON's source to look nice, there is also a parameter called JSON_PRETTY_PRINT for json_encode:

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

Actually PHP's json_encode precisely converts PHP variable types to the matching JSON types. A PHP integer will be a JSON integer, a PHP string will be a JSON string. So note the difference between 1 and „1“.

Get the right template

The examples above for fetching templates and setting the correct MIME-type can be solved with a general switch block:

// e.g. $template = 'index';
// e.g. $contentType = 'html';

switch ($contentType) {
  case 'xml':
    header('Content-Type: text/xml');
    break;
  case 'json':
    header('Content-Type: application/json');
    break;
  case 'txt':
    header('Content-Type: text/plain');
    break;
  case 'csv':
    header('Content-Type: text/csv');
    break;
  default:
    $contentType = 'html';
    break;
}

$templateFilename = __DIR__ . '/templates/' . $template . '.' . $contentType . '.php';
require($templateFilename);

Conclusion

EVery PHP project needs to have a clean separation between logic and output – but not always you will need a template engine like Twig or Smarty for this separation. Common off-the-shelf PHP (with few small helpers) can be a simple, fuss-free alternative for templating.

So here it is, our small manual of the most important PHP functions for templating:

Function Alias Description
htmlspecialchars html HTML escaping
urlencode - URL escaping
json_encode - JSON/JavaScript escaping
setlocale - Set language / locale for translation
gettext / _ _html Outputting translation
strftime - Output localized date format
localeconv - Get number format for current locale
printf _html Output formatted variables in string
nl2br - Convert line breaks to <br />
implode - Joins array members to a single string
var_dump debug Dump PHP variables
header - Change Content-Type in browser

The only thing missing from PHP is template inheritance, like in Twig's template inheritance or Smarty's template inheritance. Also have a look at CSS-Trick's comparison of template engines.


Es gibt auch eine deutsche Version von diesem Artikel, „Die kleinste PHP-Templating-Engine der Welt“.