Advertisement

Roll Your Own Templating System in PHP

by
Gift

Start a hosting plan from $3.92/mo and get a free year on Tuts+ (normally $180)

Push the limits of your PHP knowledge with this advanced tutorial. Implement techniques including object-oriented programming, regular expressions, and function currying to build a templating system from scratch.


Why Do I Need to Build a Templating System?

The short answer? You don't. So why bother? From the bottom of my geeky little heart, I believe that all developers should constantly be pushing themselves to learn new and/or difficult concepts. It's what makes us smarter, keeps our work interesting, and makes our day-to-day workload seem like less of a burden (because, you know, we're getting so much smarter). To that end, rolling your own templating system gives you the opportunity to hone your PHP chops with brain-bending concepts such as currying and regular expressions. And, hey! You just might find that you could put a templating system to use in one of your future projects.


Step 1 Plan the System

"All developers should constantly be pushing themselves to learn new and/or difficult concepts."

Before we just dive in and start programming, let's figure out exactly what we're trying to do. In building this templating system, we are hoping to:

  1. Separate HTML markup from our PHP scripts almost completely
  2. Make data from our back-end scripts more accessible to front-end designers
  3. Simplify maintenance by abstracting display logic from business logic

What We Need to Build

The templating system we're going to build in this exercise will consist of one class, Template, containing two properties and five methods, and a folder containing the template(s) to be parsed. Other than that, we just need a file for outputting some test data. Sounds pretty simple, right?

How the Templating System Works

To make it really easy for a designer to format the data coming out of our server-side scripts, we're going to create a system that allows for a template that has an optional header and footer with a loop that will be used to format the entries returned from the script. If we strip it down to its most basic state, this system will walk through the following steps:

  1. Load entries to be parsed as an array of objects
  2. Load the template file to be used
  3. Separate the header and footer from the loop
  4. Find the template tags using regular expressions
  5. Check if the template tag matches a property in the entry obect
  6. Replace the template tag with the matching property's data

Step 2 Outline the Class

To make any programming project (or anything for that matter) easier, I like to start by outlining the steps I should take. We'll use this tactic so that when we start developing, we're really only filling in the blanks.

Create the Folder Structure

First, create a folder to contain your project. I called mine templating. Inside the project folder, create two new folders: assets and system. The assets folder will contain the templates for our demo. The system folder will contain the Template class. Inside the assets folder, create two new folders: templates. Guess what it holds!

Create the Main Files

Next, create index.php in the main project folder. This is where we'll test the templating system once it's ready to go, and we'll use it to make sure our individual steps are working along the way as well. For now, however, you can leave it empty. In the system folder, create a new PHP file called class.template.inc.php. Inside, define the class, which we'll call Template, and let's take our steps from above to create a to-do list:

NOTE: The fact that the tag replacement method is static and that the currying function is in there at all is just something you'll have to trust me on for now. I'll explain everything if you stick with me.


Step 3 Define a "Template"

Before we start parsing templates, let's decide what our templates are going to look like. A template should be as simple to use as possible. If we do this right, any semi-competent HTML user should be able to easily create templates. This means keeping our template as close to writing HTML as possible.

What Would the HTML Look Like?

To define our template, let's start by simply mocking up an entry as we might see it on a webpage:

Great. That's simple enough. So let's separate out the pieces that would vary from article to article:

  • URL — the article URL
  • Title — the article title
  • Publisher — the site that published the article

Use the Variable Data to Choose Template Tags

Using the information we gathered above, we can determine that we need a total of three template tags for the example above.

But we can't just use the word "title" as the template tag for the title; what if someone uses the word "title" in her markup? It would result in every occurrence of the word being replaced with the entry title — obviously, this isn't the proper behavior. To avoid this, we need to wrap our template tags in something that is not likely to appear in the text. For this example, we'll use curly braces ({}).

By using a template tag that's relatively unlikely to appear in the text of average markup, we can be relatively certain that our templates will work as expected in pretty much all standard situations.


Step 4 Load the Entries

Because I'd like to get right into the internals of the templating engine, we're not going to spend much time on the entries themselves. I'm going to use the Envato API and implement the steps for href="http://net.tutsplus.com/tutorials/php/display-anything-you-want-from-the-envato-api-using-php/">using the Envato API from Drew Douglass. Open index.php in the root of your project folder and insert the following code.

As you can see in the comments, we're using cURL to send a request to the Envato API and storing the returned data in $ch_data. Then, assuming entries were returned, those entries are converted from the returned JSON format into an array of objects. NOTE: For further info on object-oriented PHP, check out my Nettuts article about object-oriented programming in PHP, my book Pro PHP and jQuery, or a quick explanation of OOP on Wikipedia. The entry objects contain the properties which we can use in our templates. To see this data, add the bold code below into index.php:

NOTE: Code snipped for brevity. If you load index.php in your browser, you'll see something similar to the following:

The class properties — $title, $url, $site, and $posted_at — will correspond to the template tags {title}, {url}, {site}, and {posted_at}. We'll get to exactly how that will work a little later on.

Tie the Entries to the Templating System

You may have noticed that the entries aren't being loaded or stored in the Template class right now. This is because we want our templating system to be compatible with any set of entries, and it's pretty easy to take any set of entries from a database or web service and organize them into an array of objects. However, we do need to store the entries in the templating system so they can be parsed. To keep this nice and simple, simple open up class.template.inc.php and create a public property called $entries. This will store the entries for parsing later.


Step 5 Load the Template File

Our next step is to create a method that will load the template file for parsing. Because this method should only run as part of the overarching templating process, we can make this method private. In the interest of following PEAR naming conventions we'll call this method _load_template(). In addition, we need two new properties to store the template file path as well as the loaded template, which we'll call $template_file and $_template, respectively.

NOTE: Don't forget to adjust the $entries property to account for the addition of $template.

Make Sure the File Exists

Our first step in loading the template is to make sure the file exists before attempting to open it. This helps avoid errors, and really, it's just a good idea to be sure. We'll do this using file_exists(). Also, because there's the outside chance that a file's permissions might not allow us to read its contents, we need to check that as well using is_readable(). Because we're trying to make this as simple as possible, we'll be adding a default template in case the provided template doesn't exist or doesn't load properly for some reason. After we've figured out the location of the template file, we can load it into our private $_template property using file_get_contents(). In class.template.inc.php, add the following bold code to _load_template() to load our template (or a default):


Step 6 Parse the Template

To parse our template, we need to plan out a list of steps that will be performed to properly handle the data:

  1. Remove any PHP-style tags from the template
  2. Extract the main entry loop from the file
  3. Define a regular expressions to match any template tag
  4. Curry the function that will replace the tags with entry data
  5. Extract the header and handle template tags if they exist
  6. Extract the footer and handle template tags if they exist
  7. Process each entry and insert its values into the loop
  8. Return the formatted entries with the header and footer

But First, a Word About Regular Expressions

With our template loaded and a plan of attack, we can start the process of parsing. This is a little trickier because we'll be getting into regular expressions, which can be both intimidating and intoxicating for developers. So before we go nuts, let's take a second. Take a deep breath, and repeat after me: "With great power comes great responsibility. I will only use regular expressions when there isn't a simpler way to accomplish the desired outcome. Because every time I abuse regular expressions, another programmer loses a weekend crying into his fourth Red Bull while trying to decipher the mess I've made." With that promise in mind, let's dig in. Create a new private method in the Template class called _parse_template() to handle the regexes we're about to write.

Get Ready for Testing

For testing purposes, we'll need three things:

  1. A template file for testing
  2. A method to return generated markup from the Template class
  3. Modifications to index.php that will output the returned markup

First, let's put together a sample template that will test all of the features we're going to build in _parse_template( . In assets/templates/, create a new file called template-test.inc and insert the following:

Next, we need to define our public method to generate markup, which will be called generate_markup(). This method simply calls _load_template() and _parse_template() and outputs the resulting HTML markup. The generate_markup() method will eventually accept additional data that can be inserted into the header or footer of a template, so we'll get ready for that feature as well by adding an argument to the method called $extra.

Finally, let's modify index.php to output the returned value of generate_markup(). To do this, use require_once to include the Template class file, then create an instance of it. With our new Template object, we can define the template file name and echo the result of generate_markup() to the browser:

Now all we have to do is temporarily
output the template contents at the bottom of _parse_template() so we can see what's happening as we parse the template:

Make sure this is working by loading index.php in your browser. It should produce the following:

Remove Comments from the Template File

Our first regexes will remove any PHP-style comments from a template file. While this isn't a strictly necessary step, keep in mind that we're trying to keep this system as user-friendly as possible. It would be extremely helpful to a potential template builder to know what tags are available, but it's probably not desirable to have those comments in your final markup (not to mention that a PHP-style comment will break HTML layouts). The two comment styles we're going to approach are the recommended comment styles in PHP:

First, let's focus on the regex that will capture block-level comments. This needs to match any string starting with /* and ending with */. Right out of the gate, our regex will look like this:

Because the standard regex delimiter is the forward slash (/), we're going to use an alternative delimiter to reduce the number of escaped characters required in our regex. The number sign (#) is a perfectly valid regex delimiter — though I usually recommend using the standard forward slash for clarity, in certain cases using the standard can decrease the readability of a regular expression. Compare the following:

If we insert this regex into _parse_template() as is, though, it doesn't yield the desired result. Add the bold code below to _parse_template() to see the result of our current regex:

NOTE: For the remainder of this article, docblock comments will be omitted to save space (unless they're new). The output in your browser doesn't change if you reload index.php; this happens because we haven't added the wildcard character with a modifier to account for zero or more characters between the comment opening and closing (.*). Additionally, we need to account for the fact that block-level comments are usually multi-line, we need to add the s modifier to account for this. Our modified regex should look like this:

Adjust this in _parse_template(), then reload index.php in your browser. Whoops! The output is:

We forgot to make the wildcard character lazy, so instead of stopping at the end of the first comment, it continued on to the end of the second block comment. This is easy to fix, though: simply add a question mark after the wildcard to make it lazy. It should look like this:

Adjust _parse_template(), then reload index.php. Much better!

Next, we need to target inline comments (those starting with two forward slashes (//). Because these aren't multi-line, this will actually be its own regex instead of expanding the block-level comment regex. For this regex, we need to find any text following two forward slashes (//). The only exception to this rule is the two forward slashes in a remote URL (http://) — to exclude this, we'll use a negative lookbehind. The finished regular expression should look like this:

Add this to _parse_template() by changing the variable $comment_pattern to an array with our block-level regex as the first item and the inline comment regex as the second:

Now the comments are properly stripped when you reload index.php in your browser:

Separate the Header, Footer, and Loop for Processing

Our next task is to split the template into three sections: the header, the loop, and the footer. These won't necessarily exist
in all cases, so we'll have to check for that as well.

Isolate the Main Entry Loop

First, we'll grab the loop by catching all content between the {loop} and {/loop} template tags. This regex will match the whole template and use a capturing group to identify the loop:

Modify _parse_template() as shown in bold to test that the entry loop is being extracted properly:

NOTE: Don't forget to alter the function to return $entry_template so you can see the proper output. By using preg_replace() to "replace" the whole template with just the captured loop template, we've successfully isolated the main loop. Reload index.php in your browser and you should see the following:

Isolate the Header

Next, let's get the header out of the template. The regex to do this will be similar to the one that grabbed the main loop, but this time we're going to capture the content before the {loop} template tag. To match the header, we need to start from the beginning of the template and capture everything up until the {loop} tag. Since we're using preg_replace() to extract the header, we also need to match everything after the {loop} tag as well to make sure it gets removed when the replacement occurs. This regex, when it's completed, should look like this:

Because some templates won't require a header, we also need to check that the data returned in header isn't the whole template. If that happens, the header should be set to NULL to avoid duplicated data. Modify _parse_template() with the bold code and set it to return the extracted header:

As expected, reloading index.php in your browser will result in the header data being displayed:

Isolate the Footer

The footer is very similar to the header in how it's extracted, except this time we're capturing the data after the {/loop} tag and making sure it doesn't match the whole template using this regex:

Plug this into _parse_template() and set the method to return the footer content just like we did with the header using the bold code:

Reload the index file to see the footer output:

Identify Template Tags with Regular Expressions

The next step in our process is to put together a regex that will match any template tag so that we can replace them with entry data. Unlike the others we've written, this one shouldn't match the whole template. This pattern should match only the template tag that will be replaced. To accomplish this, we can use the shorthand to match any word character (\w is equivalent to [A-Za-z0-9_]) and match one or more characters between curly braces. The complete regex looks like this:

We'll verify that this works in just a bit, but for now let's just define the pattern for later by adding the bold code to _parse_template():

With all of our regular expressions ready to rock, we can move on to the next big task.


Step 7 Get Ready for Template Tag Replacement: Currying

In order to replace our template tags with the properties from our entries, we need to use a concept called currying, which is the act of making a function that takes multiple arguments into a chain of functions that accept a single argument.

Wait. What Did You Just Say?

Currying is kind of a weird concept to get your head around, so let's take a second to go over exactly how it works. The goal of a currying function is to make it possible to supply one argument at a time to a function that requires multiple aruguments without causing errors. So, for instance, let's look at a simple math function:

If we wanted to call this function normally, we'd simply do the following:

But let's say that — for whatever reason — we needed to call the add() function incrementally; we can't simply add one argument now and another later (by calling add(1)). That issues a warning:

So we need an intermediate step that will check if the proper number of arguments were passed. If so, the function executes as usual. However, if there are too few arguments, a new function will be returned with the function name and the first argument stored. This way, when the second argument is passed the original function can be called properly. Using our add() function as an example and assuming that we can curry this function using the imaginary function curry(), we can demonstrate this process:

Write the Currying Method

Now that we know how currying works, let's start writing the function. The currying function itself will always return a function, which we'll do using create_function(). The created function will check if the proper number of arguments exist and execute the curried function as usual if so using call_user_func_array(). If there aren't enough arguments, another function will be returned using create_function(). Because putting all of this together requires creating functions within created functions, there's a lot of escaping. This makes our currying method look more confusing than it really is. Add it to the Template class with the following bold code:

How Does Currying Apply to the Templating System?

Right now it might not be clear how all of this applies to the templating system. When we get to the next step we'll go over this in detail, but in short, we need to be able to call a function with two arguments in order to replace the template tags: one argument is the entry from which data should be pulled, and the other is the template tag to be replaced. Since we're using regular expressions to replace the tags, we need to use preg_replace_callback() to make the replacements. However, since the callback passed to that function can only accept one argument — the matched text — we need to pass a curried function that already has the entry stored inside of it. Make sense? Let's make it happen!


Step 8 Replace Template Tags with Matching Entry Data

All the pieces are in place. Now we just need to connect the dots and get it done.

Write the Tag Replacing Method

Replacing tags is fairly simple on its own: take the matched text from the template tag and see if a property exists in the entry object by that name. If so, return the data stored in said property; if not, just return the template tag so the designer can see that something went wrong (or, in edge cases, the odd string that was wrapped in curly braces doesn't break). This function is going to be called replace_tags(), and it will be static so that it can be passed to the currying function as a valid callback. Add it to the Template class using the bold code below:

NOTE: The call to unserialize() at the top of this method is due to an issue with passing an object through a currying function. We'll serialize the object in the next step.

Modify the Template Parsing Method to Replace Template Tags

To complete our templating system, we first need to get our curried callback function ready for use with all of the replacement calls. This is done by currying Template::replace_tags() and storing it in a variable called $callback. Add this to _parse_template() with the bold code below:

Next, we need to set up a loop to go through each entry object in the $entries array. With each entry, we need to call preg_replace_callback() with the template tag regex as the first argument, the callback with the serialized entry object as the second argument, and the loop as the third argument. The markup returned from each call should be appended to a variable called $markup, which stores all of the entry markup to be output to the browser. Add this to _parse_template() with the bold code:

Before we can test this, we need to create an entry so that the engine has something to loop through. In index.php, add a dummy entry with a property called
$test to match the template tag in our testing template file:

With a dummy entry present, we can test the template tag replacement by reloading index.php in our browser. The output should read:

This means that we've effectively created a templating engine! However, we're not quite done yet: we still need to add the ability to replace template tags in the header and footer of our template file.

Replace Template Tags in the Header and Footer of the Template

The process for replacing header and footer template tags is identical to the process for those in the loop, but different data is necessary in order to do so. You may remember that we included an argument called $extra when we were writing the generate_markup() and _parse_template() methods; this variable is to be used as an object that will store data for replacing the header and footer template tags. For our purposes, $extra can contain two properties, $header and $footer, both of which will store an object whose properties will be used to replace template tags in the corresponding section of the template. Obviously, if no header or footer tags exist, then no data will be stored in $extra for processing. For that reason, we start by checking if $extra is an object. If so, we'll loop through its properties and run preg_replace_callback() with the template tag regex, the callback after being passed the serialized header or footer object, and the header or footer section of the template. Add the bold code below to complete our templating system:

If you reload index.php in your browser, you'll see the following output:

The last thing to do is to test the header and footer template tag replacement. Open template-test.inc and add two new template tags, one in the header and one in the footer:

Next, go back to index.php and add a new object called $extra with two objects
stored in its $header and $footer properties that have properties corresponding to the new template tags:

NOTE: Don't forget to pass $extra to generate_markup()! Save these changes, reload the file in your browser, and you'll see the following:


Step 9 Use Real Entries

As a final exercise, let's use some real entries from the Envato Marketplace and design a template to display them.

Create a Template

For a template, let's create a new on in the templates folder called entry-list.inc. Inside, add the following code:

Load Real Entries and Use the New Template

All we need to do to load real entries from the Envato API is call the function we wrote earlier in this tutorial. In
index.php, alter the code to use the new template file and to load the latest entries from audiojungle:

NOTE: I added in a doctype declaration and basic HTML tags to avoid character encoding issues.


Summary

At this point, you've successfully combined object-oriented PHP, regular expressions, and function currying into an easy-to-use templating system. The techniques in this tutorial can be added to your development arsenal for use in future projects, and hopefully you're feeling like a better developer right about now. Did you spot a shortcut I missed? Can you think of a way to improve the templating system? How do you feel about using this system in a project to help keep markup separate from business logic? Let me know your thoughts in the comments!

Advertisement