Creating a Simple Template Engine with PHP: Part 2

Series Parts

Creating a Simple Template Engine with PHP: Part 1


In the last part we covered setting up our project and getting our Engine::render() method to render our view file and interpolate the variables passed into it. Already it is more useful that using PHP on it's own to display pages. However, we want more than that. We want to eventually be able to have a new syntax for out variables, loops and conditional statements.

In this part we will use vfsStream to allow us to modify the content of the original view without needing to store everything in variables (sort of) or create any additional files which need to be included, after all we need to include the new view somehow.

Standardizing the View Path

We need to do a few things to the path so we can use it effectively to create our vfsStream file. Mainly we need to account for Windows absolute URLs.

Firstly, we must ensure it is a relative path and not an absolute one because we want simply create a vfsStream with the same path. Basically if we tried to create a vfsStream url with a Windows url such as C:\xampp\htdocs\... we would end up with the following stream handler vfs://c:\xampp\htdocs\... and that won't work. It's not as much the backslashes as the second semicolon that will gives us an error. Either way, a relative URL will be better.

 Secondly, we can't assume it will not contain backslashes if used on a Windows machine (why Windows, WHY??!!).

Finally, we'll split our path into the path (e.g. views or views/pages) and the file home.php

Let's start with the first task, cleaning our path so we can correctly create a vfsStream URL from it. First we will replace any backslashes. However we also want to ensure the path is absolute at this point so we always know what we're dealing with:

File: src/Engine.php

public function render()
{
if (!empty($params)) extract($params);

if (file_exists($view)) {
$view = str_replace(DIRECTORY_SEPARATOR, '/', realpath($view));

ob_start();
include $view;
ob_get_flush();
} else {
throw new \Exception(sprintf('The file %s could not be found.', $view));
}
}

So what we're doing here is using realpath($view) to ensure even we are passed a relative path such as view/home.php our $view variable will be an absolute path.

Our First Regex Pattern

Next we need to remove any unwanted parts from the beginning of our absolute path. Depending on the operating system we should now have a path like C:/xampp/htdocs/sites/template/views/home.php or /var/www/template/view/home.php. For the Windows path we want to remove the C:/ and for Linux/Unix we want to remove the first slash /. To do this we can use preg_replace to cover us for both scenarios:

File: src/Engine.php

public function render($view, $params = [])    
{
if (!empty($params)) extract($params);

if (file_exists($view)) {
  $view = str_replace(DIRECTORY_SEPARATOR, '/', realpath($view));
  $vfs_viewpath = preg_replace('~^([\w.-]:)?/+(.*)$~', '$2', $view);

  ob_start();
  include $view;
  ob_get_flush();
} else {
throw new \Exception(sprintf(The file %s could not be found.', $view));
}
}

 So let's breakdown what our preg_replace is doing. Firstly, we are using the tilde character ~ as our delimiter. That we can use / in out pattern without needing to escape it, after all we want to be able to detect the first / and anything before it.

The first part of the pattern ^([\w.-]+:)? matches anything which could be a stream handler or a Windows drive. The ^ character tells it that it MUST be matched at the very beginning of the string. Next we have a "group". Everything in parentheses becomes a group which is stored in a numbered variable for use else where in the pattern or for the replacement. This will be in a variable $1. If we were using preg_match('~^([\w.-]+:)?/(.*)$~', $view, $match) we could retrieve it with $match[1].

Inside the group we have [\w.-]+: . This matches any word charachters suchas letters or underscores, any dots . and any - characters followed by a :. Basically this will match C:, E:, file:, some.custom-stream: or another_stream:. It's important also when trying to match a hypen - inside square brackets that the hypen comes last, otherwise it will be treated as range (e.g [a-z] matches any letter from a-z). The + after the closing square bracket tells it we want to match 1 or more charachters. Then we close the group with a semicolon : which means the group won't match unless the semicolon is after it. Finally, the question mark after the group ? means the ([\w.-]+:) group is optional. It can be ignored or matched only once. So if our path does not start with this pattern the rest of the pattern can still be matched.

We then have a forward slash  /. This will match absolute Unix-style paths such as /var/www/template/views/home.php. Finally we have a second group, which is what we want to use as our vfs filepath (.*). This basically says match any character any amount of times. It's a common way of finding everything. We close our pattern with a dollar sign $ which tells it to match whatever came right before it ONLY if it is the end of the string.

We'll be using regular expressions a lot more when we get to writing our Syntax class, but I won;t be explaining each pattern, so if you need help with Regex's then I would suggest practicing with PHP Live Regex.

Once our pattern matches we use $2 in our replacement string to tell it to place whatever was match by the second group (.*), which is anything other than the leading C:/ or /.

Creating our vfsStream and our vfs File

Next we will want to create our vfsStream, based off of our cleaned up view path.

public function render($view, $params = [])    
{
if (!empty($params)) extract($params);

if (file_exists($view)) {
  $view = str_replace(DIRECTORY_SEPARATOR, '/', realpath($view));
  $vfs_viewpath = preg_replace('~^([\w.-]:)?/+(.*)$~', '$2', $view);

vfsStream::create($vfs_viewpath);
$vfs_file = vfsStream::url($vfs_viewpath . '.php');

  ob_start();
  include $view;
  ob_get_flush();
} else {
throw new \Exception(sprintf(The file %s could not be found.', $view));
}
}

The reason we append '.php' on to the end of the vfsStream::url is so the template files can have an extension other than .php, such as .tpl or .html or whatever the user wants. Our final included file will still be a PHP file.

We now have a file stream at the virtual path (in my case) vfs://projects/template/views/home.php.php, however for all intensive purposes we can think of this as a real file. And therefore we can use any of the file handling functions or stream handling functions on it. We can also include it like any other file. To create the file we need to add some contents to it. We can then include it in our output buffer.

public function render($view, $params = [])    
{
if (!empty($params)) extract($params);

if (file_exists($view)) {
  $view = str_replace(DIRECTORY_SEPARATOR, '/', realpath($view));
  $vfs_viewpath = preg_replace('~^([\w.-]:)?/+(.*)$~', '$2', $view);

vfsStream::create($vfs_viewpath);
$vfs_view = vfsStream::url($vfs_viewpath . '.php');

file_put_contents($vfs_view, file_get_contents($view));

  ob_start();
  include $vfs_view;
  ob_get_flush();
} else {
throw new \Exception(sprintf(The file %s could not be found.', $view));
}
}

Once again, you will be able to view your page in the browser, however now, we have a separate file stream which we can modify independently of the original view file. This is what will allow us to replace our custom syntax from the origin $view and put plain PHP in our $vfs_view.

In the next part we will begin writing our Syntax class, and you'll see how easy it is to create any template language you want.