Creating a Simple Template Engine with PHP: Part 1

Series Parts

Creating a Simple Template Engine with PHP: Part 2


So lately I've been writing a few composer packages which all have something to do with working with files or the file system; Affinity4\File, Affinity4\Config and Affinity4\Concat. While writing these packages I have needed to test the packages work on files in a consistent way. For example, in the beginning my tests where passing on my Windows machine and failing when I committed them. TravisCI was telling me the paths didn't match. Obviously because of the directory separators for files paths didn't match from Windows to Linux.

So, while searching for a better way to handle testing files, I came across some virtual file system StreamWrappers, in particular vfsStream. However, not all file based tests can be rewritten to use vfsStream. Most notably tests where your class/method uses SplFileInfo such as this snippet from Affinity4\File:

$pattern = '/^test[\w\d-]*.txt$/';
$dir = 'tests/files/01/02';
$this->assertContainsOnlyInstancesOf(
'SplFileInfo',
$this->file->find($pattern)->in($dir)->get()
);

How I got the idea

While learning about vfsStream and working on the Affinity4\Concat package (still in development as I write this), I spent about a day looking into StreamWrappers and there various uses. During this, I noticed two interesting and appealing things about these "fake" files:

  • They could be handled as streams, obviously. This means they are stored in a variable and can be modified very easily in memory without the need of an actual file.
  • Once such modifications where done the file could be included/required just like a regular PHP file.

 

So I came up with an idea to write a simple template language where the template file is opened in a stream, modified and converted to correct PHP, then another stream is used to mock the include file, which is displayed using the output buffer and an include. This way there are no temporary files stored and filters can be created and applied to the stream rather easily, making the template syntax itself very easy to change and extend.

So let's get started.

The syntax

What I write here isn't going to be a very full featured template language. However, the repository for the full template engine can be found at Affinity4/Template. It is also available on Packagist. It has been inspired by various sources such as Twig, Blade and even KnockoutJS, and Sass/Scss (and some Coldfusion) with some ideas of my own.

The first focus of this template engine will be that raw PHP will just work, so the template specific syntax is optional. This is inspired by Scss, and is something I really liked about Sass when first learning it (although now I only use the indented Sass syntax whenever possible). The fact I could gradually use more advanced features as I learned them and fall back to plain CSS whenever I needed made it much easier to adopt Sass in the early days.

The second key part of this template engine is that the template-specific syntax will NEVER be displayed in absence of the Template Engine or an error compiling the stream to PHP. This means, if you decide to move to another template language or simply include the files directly without using the template engine then you won't see {{ var }} or @foreach(thingy in thingys) left behind when you view the page.

The third feature is that the syntax should work in all IDE's and text editors out-of-the-box and also be something non-PHP developers can recognise and read easily. Not a new syntax wrapped in curly braces and other symbols. Instead something anyone who's written HTML can recognise.

To achieve the second and third requirements of our template language we have a very simple solution: HTML comments!

Here's an example:

<h1><!-- :title --></h1>
<!-- @if :show_list is true -->
<ul>
<!-- @each :item in :items -->
<li><!-- :item --></li>
<!-- @/each -->
<ul>
<!-- @/if -->

The downside is it's a small bit more verbose, however it can be quickly written using the comment keyboard shortcut in your editor or IDE (usually Ctrl + / or Cmd + /).

The upsides (as I mentioned above) include:

  1. Out of the box IDE/Editor support (it's just HTML after all)
  2. You can use raw PHP if the syntax doesn't offer an alternative syntax for what you need. Great for the early development phase to allow quick adoption.
  3. The template specific language will be easily ignored by anyone just focused on working with the markup itself. 

For now we'll focus on the syntax only so our template engine won't feature layouts or blocks as you would find in Twig, Blade, Latte etc. These are important features, and I plan to implement them in the future. But for the sake of this article I'll only focus on implementing  what you see above.

Setup

Let's setup our project. We'll ignore testing for the sake of brevity. We'll also place everything in two classes One to handle One for our syntax rules (the regex) and one for opening our source views, compiling into raw php, extracting variables and finally rendering/outputting the compiled view.

It sounds complicated, however with the aid of vfsStream it becomes rather trivial at times to do everything, compared to the more traditional method of creating lexers/parsers, tokenizers and dealing with some form of an AST (Abstract Syntax Tree).

We'll use Composer to require vfsStream and also autoload our own classes.

composer require mikey179/vfsStream

Now we'll create a folder in the root of our project called src

Inside the src folder create two files for your classes, Engine.php and Syntax.php. Your folder structure should now be like so:

template
|-- composer.json
|-- src
|-- Engine.php
|-- Syntax.php
|-- views
|-- home.php

What I like to do first is figure out what I want the final realized idea to look like. So I ask myself "How do I want this to be called or used?". Then I'll type it out in an index.php file to see if it seems possible or does it raise further questions or challenges. For the Template Engine it's rather simple. So we want to be able to do this:

File: index.php

<?php
use Template\Engine;

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

$template = new Engine;
$template->render('views/home.php', [
'show_title' => true,
'title' => 'Home'
]);

So this means to get a working version to build on we need:

  1. A render method
  2. The render method must take in the path of the view as it's first argument
  3. An optional array of parameters to be passed to the view
  4. The parameters should be extracted to variables if the second argument is not an empty array
  5. Load the view into a vfsStream for later processing
  6. Include the processed (compiled) "file" in an output buffer to display it
  7. A view file: views/home.php

So let's create our view with some plain PHP first so we know our variables are being passed to the view when the time comes:

File: views/home.php

 <?php if ($show_title) : ?>
    <h1><?php echo $title;  ?></h1>
<?php endif; ?>

Now we can focus on the simplest implementation of our Engine class.

File: src/Engine.php

<?php
namespace Template\Engine;

use org\bovigo\vfs\vfsStream;

class Engine
{
    public function render($view, $params = [])
    {
// 1. Extract params if not an empty array
// 2. If our view exists include it in an output buffer
// 3. If view file cannot be found throw an exception.
    }
}

For now this how we'll get a working version of our render method, albeit without our desired functionality.

Before we go any further we will need to add our src directory to our PSR-4 autoloader in our composer.json file:

File: composer.json 

{
"autoload": {
"psr-4": {
"Template\\": "src/"
}
},
"require": {
"mikey179/vfsStream": "^1.6"
}
}
Now run we need to regenerate our autoload files and the composer.lock file:
composer dumpautoload

Now we can implement the logic to get our view to display with the parameters we passed in.

File: src/Engine.php

<?php
...
class Engine
{
    public function render($view, $params = [])
    {
// 1. Extract params if not an empty array
if (!empty($params)) extract($params);

// 2. If our view exists include it in an output buffer
if (file_exists($view)) {
ob_start();
include $view;
ob_get_flush();
} else {
// 3. If view file cannot be found throw an exception.
throw new \Exception(sprintf('The file %s could not be found.', $view));
}
    }
}

Now spin up a development server in PHP. Navigate to the root of your project and open a terminal. In the terminal type:

php -S localhost:80

You'll see a message telling you the server is running. Open you browser at http://localhost/ and you should see:

Image of unstyled webpage in Chrome browser

So now we know our concept for the render method is correct and will work as expected. In the next part we'll incorporate vfsStream to duplicate the contents of our view so we can process it easily.

Comments

I'm working on getting Disqus setup in the next few days

If anyone has questions or simply wants to reach out you can reach me on Twitter at https://twitter.com/LukeWatts85