ITT #16: Create a Template Parsing System with PHP

Using a template to ease the editing of pages

Cooking Up a Currying Function to Parse Template Files

One of my most recent obsessions has been with simplifying my custom CMS to allow me to generate full sites without changing anything but HTML template files and a basic configuration file. In this week's Instant Tip Tuesday, we'll explore one of the ways we can dynamically generate output and wrap it in a template file.

NOTE: This is a proof-of-concept type of thing, not a production-ready script. Be aware that I haven't used a script like this on a real site, and therefore have no idea if it's fast, effective, or even useful. For now, this is just a "hey, cool!" sort of thing; use with discretion.

Download the Files

So, What Are We Trying to Do?

To try and explain what our goal is, here, let's look at a couple examples. For our examples, we'll be using the following simple array:


$entries = array(
	array(
		'title' => 'Title One',
		'entry' => 'This is some text.'
	),
	array(
		'title' => 'Title Two',
		'entry' => 'This is some more text.'
	)
);

The Usual Method

In a traditional script, we would need to write something similar to the following in order to output both entries in clean HTML:


<?php
foreach($entries as $e) {
?>

	<h1> <?php echo $e['title'] ?> </h1>
	<p> <?php echo $e['entry'] ?> </p>

<?php
} // End foreach
?>

While this isn't bad, it can get confusing. It's especially tricky when we start dealing with entries that have more elements, such as the author, date posted, images, and so on.

The Template Method

What if we could put together some scripts that would allow us to simply edit an HTML file to lay out our pages:


<h1> {title} </h1>
<p> {entry} </p>

That's it. Seriously.

To use this in our scripts, we just call a quick function between the <body> tags of our public HTML file:


<html>

<head>
	<title> Template Testing </title>
</head>

<body>
	<?php displayEntries($entries, $template) ?>
</body>
</html>

Each entry would be processed and formatted according the template we specified in an external, easy to edit file.

How Do We Do That?

Planning Our Steps

In order to parse a template file, we're going to need to use several advanced programming techniques. The steps we'll be using are:

  1. Build a public HTML page to act as a container
  2. Build a template file
  3. Load the contents of the template file into a variable
  4. Run a regular expression to find all template tags
  5. Use preg_replace_callback() with a currying function to send the matches to a replacement function
  6. Match the template tag text with an array index and use the corresponding data from the array
  7. Set up a loop that will use the template for each entry in the array

Setting Up Our Project

This project will consist of three files:

  • index.php — The public HTML file
  • template.html — The entry template
  • functions.php — Our parsing functions

Step 1 — Build a Public HTML Page

Because this is a very basic example, our container file (index.php) will be very basic. Create the file and insert the following code into it:


<html>

<head>
	<title> Template Testing </title>
</head>

<body>
	
</body>
</html>

This file will eventually call the function that displays our entries.

Step 2 — Build a Template File

Again, with this being an extremely simple example, our template.html file is only two lines:


<h1> {title} </h1>
<p> {entry} </p>

We're going to use curly braces to wrap our template tags. These tags need to correspond to a key in our array, so to access $entry['title'] we need to use {title}. This will work for any named array index, so if we want to manipulate data, we can just add a new named value to the array.

Step 3 — Loading the Template

This step is the easiest. Our template from above will be saved in an external HTML file called template.html. In order to perform our replacement functions on the contents of the file, we need to load the entire file as into a variable in index.php:


<?php

// Load the template into a variable
$template = file_get_contents('template.html');

?>

Now we have the contents of template.html available to run our regular expression with.

Step 4 — Parse the Template with a Regular Expression

At this point, we have a file to display our output, and a template to shape our output. All that's left to do is write the scripts that will fit our data into the template.

As we move into this code, it might look a little overwhelming at first. We'll step through everything by the end of the article, though.

The Regular Expression

In order to find our template tags, we need to locate any data enclosed within curly braces. The data needs to be a letter with no spaces or punctuation except underscores. To accomplish this, we use the following regular expression:


$pattern = "/{(w+?)}/"; // Matches any template tag

The basics of what we're doing here go something like this: we start and finish with a forward slash because these are delimiters for regular expressions; the curly braces need to be escaped because they are special characters, so we use a backslash to do so ({); to capture the tag name for later use, we need to enclose it in parentheses, which means we can access it as a match later; the w+ means, "match any word character (a-z and underscores) one or more times"; the question mark means the expression is lazy and will stop matching at the first closing curly brace it finds.

That's a lame crash course in regular expressions, but that's not our focus today. We can rest assured knowing that it works and move on.

Parsing the Template

Armed with our regular expression, we need to use preg_replace_callback() to parse our template. The way this works is that we supply a pattern, a function to be called on the matches, and the string on which we're running the regex.

We have to jump ahead a little bit here, so stay with me. The callback function we're passing to preg_replace_callback() is going to be a currying function. We'll get to what that is and how it works in just a minute.

The Parsing Function

With the regex ready and an idea of how we need to parse the template, we can write our function, called parseTemplate():


function parseTemplate($replacement_array, $template)
{
	$pattern = "/{(w+?)}/i"; // Matches any template tag

	/* 
	 * Call the currying function, tell it to accept two
	 * arguments, and only pass it one: the array containing
	 * values to replace the tags with.
	 */
	$callback = curry('replaceTags', 2);

	return preg_replace_callback($pattern, $callback($replacement_array), $template);
}

Step 5 — Currying

This is by far the most complicated part of this project. Currying is a concept by which we can call a function halfway. Essentially, we're able to call, for instance, a function that accepts two arguments, but only pass one of the arguments, storing a new function in a variable that will allow us to pass the other argument and complete the function later.

Because I don't know all of the theory behind currying, I'm not going to get into a whole lot of detail lest I put my foot in my mouth. All we need to know for this project is that using currying, we'll be able to pass a half-cocked replacement function with our entry array to preg_replace_callback(), then add the matches returned into the mix and come up with a properly parsed template.

The currying function looks like this:


function curry($func, $arity) {
	/* 
	 * Creates an anonymous function to accept incomplete
	 * argument lists and allow the rest of the arguments
	 * to be supplied later
	 */ 
	return create_function('', "
		// Get an array of the arguments passed to $func
		$args = func_get_args();

		/*
		 * If the number of arguments supplied to the passed
		 * function is greater than or equal to the number of 
		 * arguments defined in $arity, call the function and
		 * return its output
		 */
		if(count($args) >= $arity)
		{
			return call_user_func_array('$func', $args);
		}

		/*
		 * Otherwise, save the function paramters in a variable
		 * and return a function that will accept more parameters
		 * while preserving the original passed arguments
		 */
		$args = var_export($args, 1);
		return create_function('','
			$a = func_get_args();
			$z = ' . $args . ';
			$a = array_merge($z,$a);
			return call_user_func_array('$func', $a);
		');
	");
}

When we called our currying function, we pass replaceTags() as the first parameter, and 2 as the second parameter. This means that we are creating an anonymous function that will accept two parameters. We then pass the replacement array as the first parameter in preg_replace_callback(). The returned matches are passed as the second parameter when the callback fires, effectively completing replaceTags() and returning our parsed template.

Step 6 — Match the Template with Our Array

We're ready to write the function replaceTags() that is called by our currying function. This is a pretty straightforward function, in which we check that the template tag exists in the array as a key, then return the value. If the tag doesn't exist, the tag is returned as plain text.

When we write our funciton, it looks like this:


function replaceTags($transformations, $matches)
{
	// To avoid errors, make sure the array contains a key that matches
	if(array_key_exists($matches[1], $transformations))
	{
		// Return the array value whose key matches the template tag
		return $transformations[strtolower($matches[1])];
	}

	// If not, return the template tag as is
	else
	{
		return '{'.$matches[1].'}';
	}
}

Step 7 — Loop Through the Entries

We're pretty much done at this point. All we need is one more function to loop through our entry array and put each one into the template.

Our function, called displayEntries(), simply sets up a foreach loop and pushes each array item through the template parsing functions, echoing the output to the browser. We could just as easily store the output in a variable, but for this example it's not necessary.


function displayEntries($entries, $template)
{
	foreach($entries as $e)
	{
		echo parseTemplate($e, $template);
	}
}

Putting Everything Together

To implement our functions, simply add the following to the top of index.php:


<?php

// Include the parsing functions
include_once 'functions.php';

// Load the template into a variable
$template = file_get_contents('template.html');

// Grab the information we need to parse
$entries = loadEntries();

?>

Notice we're using a function called loadEntries(). On a live site, this would probably grab entries from a database. In this example, the function simply returns the array we declared above:


function loadEntries()
{
	return array(
		array(
			'title' => 'Title One',
			'entry' => 'This is some text.'
		),
		array(
			'title' => 'Title Two',
			'entry' => 'This is some more text.'
		)
	);
}

We'll place loadEntries() in functions.php for easy access.

The final step is to call displayEntries between the <body> tags to output our templated entries. The finished index.php looks like this:


<?php

// Include the parsing functions
include_once 'functions.php';

// Load the template into a variable
$template = file_get_contents('template.html');

// Grab the information we need to parse
$entries = loadEntries();

?>
<html>

<head>
	<title> Template Testing </title>
</head>

<body>
	<?php displayEntries($entries, $template) ?>
</body>
</html>

Download the Files

Summary

In this entry, we created a simple templating system as a proof of concept. We also explored currying functions and regular expressions on a basic level.

What do you think? Any ideas for improvements? Any constructive criticism? I'm all ears! Hit me in the comments!

Posted May 19, 2009 by Jason Lengstorf.
This entry is filed under instant tip tuesday, php, regular expressions, and currying.

Want more content like this? Subscribe for FREE!

Comments for This Entry

GravatarAshton Sanders05:10PM on May 19, 2009

That's pretty sweet. I think I'm finally understanding Currying now. ;) Only took two explanations.

btw, you didn't close this line in step 5:
return create_function('', "

GravatarBrenelz05:34PM on May 19, 2009

What would you need to make this production material. Security issues?

GravatarJason Lengstorf11:16AM on May 20, 2009

@Ashton Sanders:
That line is closed; it just encloses the rest of the function. We're returning an anonymous function there, so we just broke the string up over multiple lines.


@Brenelz:
Honestly, it just needs trial and error. I don't think there are any inherent security risks beyond what you face with a typical PHP script. I was just trying to cover my ass in case someone used it and found out it slowed their website to a crawl or started sending them threatening emails. :)

My next step with this is to figure out a loop structure within the template that's easy to use. Then I'll try it out in some personal projects and see how it performs.

Thanks for the feedback!

GravatarMark McDonnell01:05AM on May 27, 2009

Keep us informed on how you get on with this in future as I'm very keen to start using template files with PHP (just to clean up the front-end files of server-side code).

I haven't tested your code above so I don't know yet if it works but I'm looking forward to trying it out when i get home.

Thanks for posting.

Gravatarlukas12:15AM on May 28, 2009

Shouldn't ther be some kind of caching? I am Using Smarty and it is still way overweight for me, because I dont use inline scripts, but the chaching is doing the trick.
Maybe you want to include somthing in this direction in your next version?
Nice entry anyway.

GravatarJason Lengstorf12:00PM on May 29, 2009

@Mark McDonnell:
Let me know if you find anything that could be improved in the script. I'll definitely post my findings here as I get time to work on the script.

@lukas:
Caching is definitely on the to-do list. This still has a ways to go before it would be production-ready. Thanks for your input!

GravatarKevin Lo07:54AM on January 27, 2010

This is an exceptionally great piece of work.
However, there is an error, when I tried to use "function.php" as a class.

Warning: call_user_func_array() [function.call-user-func-array]: First argument is expected to be a valid callback, 'replaceTags' was given in /Applications/xampp/xamppfiles/htdocs/hkhscms/_class/classTEMPLATE.php(73) : runtime-created function(27) : runtime-created function on line 11

Because I don't understand quite well the logic of the function, "curry($func, $arity)"...; is there a way to fix this error?

GravatarJason Lengstorf06:26PM on January 27, 2010

@Kevin Lo:
If I understand this correctly, you've converted the functions in functions.php into a class?

If so, the call to replaceTags needs to be updated in the curry() function to call $this->replaceTags instead of replaceTags because it's now a method.

If that's not what you're trying to do, maybe email me what you've got so I can have a look at it.

Thanks!

Post a Comment

Want to show your face? Get a gravatar!

ALLOWED TAGS: <tt><strong><em>