Build Your Own Template Engine in PHP - Rendering & Echo
Let's build a tiny template engine for PHP!
This post will focus on rendering the template and echoing out data that can be escaped with htmlspecialchars()
.
Before we start writing code we need to take care of the most important part of any programming project — giving the project a name. I'm going to call it Stencil.
The templates themselves will all be plain PHP. We won't be creating any special syntax like Twig or Blade, we'll focus solely on templating functionality.
We'll begin by creating the main class.
class Stencil
{
public function __construct(
protected string $path,
) {}
}
The Stencil
class will need to know where the templates are located — so that gets passed in through the constructor.
To actually render our templates, we'll need a render()
method.
class Stencil
{
// ...
public function render(string $template, array $data = []): string
{
// ?
}
}
The render()
method accepts the name of the template and an array of data variables that will be accessible inside of said template.
We now need to do three things:
- Form a path to the requested template.
- Make sure the template exists.
- Render the template with the provided data.
class Stencil
{
// ...
public function render(string $template, array $data = []): string
{
$path = $this->path . DIRECTORY_SEPARATOR . $template . '.php';
if (! file_exists($path)) {
throw TemplateNotFoundException::make($template);
}
// ?
}
}
The first two on the list are easy to do. Stencil will only look for .php
files so forming a path is just a case of concatenating some strings. If the requested template contains any directory separators, that will handle the nesting of templates inside of directories.
If the file doesn't exist, throw a custom TemplateNotFoundException
.
To cover the third point in the list, actually rendering the template, we'll want to make a new class called Template
. This will house all of the methods available to the template and handle the real rendering side of things.
class Template
{
public function __construct(
protected string $path,
protected array $data = [],
) {}
public function render(): string
{
// ?
}
}
class Stencil
{
// ...
public function render(string $template, array $data = []): string
{
$path = $this->path . DIRECTORY_SEPARATOR . $template . '.php';
if (! file_exists($path)) {
throw TemplateNotFoundException::make($template);
}
return (new Template($path, $data))->render();
}
}
To obtain the rendered template as a string, we'll take advantage of PHP's output buffers. When you call ob_start()
PHP will start to capture anything that the application attempts to output (echo
, raw HTML, etc).
You can retrieve that as a string and then stop capturing the output using ob_get_clean()
. A combination of these two functions and an include
will let us evaluate a template file.
class Template
{
// ...
public function render(): string
{
ob_start();
include $this->path;
return ob_get_clean();
}
}
This will handle rendering the template, but it doesn't do anything to let the template access those data variables stored inside of $data
. PHP being the wonderful language it is provides another function called extract()
that accepts an array of key value pairs.
The key for each item in the array will be used to create a new variable in the current scope using the associated value. Since include
and its relatives always execute the PHP file in the current scope, the template will be able to access the extracted variables.
class Template
{
// ...
public function render(): string
{
ob_start();
extract($this->data);
include $this->path;
return ob_get_clean();
}
}
Perfect! Now we can render a template and give it access to the data variables provided. There is one thing that we haven't considered... if we wanted to create some variables inside of the render()
method, our template would also be able to access those. That's not what we want!
To solve that problem, we need to wrap the extract()
and include
calls in an immediately-invoke closure — that way, the template will only have access to the variables inside of the closure.
class Template
{
// ...
public function render(): string
{
ob_start();
(function () {
extract($this->data);
include $this->path;
})();
return ob_get_clean();
}
}
The final piece of the puzzle is a method for escaping values when echoing them. Closures inherit $this
which means our template will be able to call any method we define on the Template
class. Let's create an e()
method that accepts a value and escapes it using htmlspecialchars()
.
class Template
{
// ...
public function e(?string $value): string
{
return htmlspecialchar($value ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
}
And like that we have a little template engine for our PHP projects.
<h1>
Hello, <?= $this->e($name) ?>!
</h1>
The template above can be rendered using our engine:
$stencil->render('hello', [
'name' => 'Ryan'
]);
And will output the following HTML:
<h1>
Hello, Ryan!
</h1>
In a future blog post we'll implement support for partials, allowing us to separate out common templates and use them in multiple places.