Templating with PHP

by electron** | Tuesday, July 03 2007 @ 16:36:24 GMT        
Writing easily maintainable code is a challenge, and you must plan ahead to make the process as painless as it can be. With a web-based language like PHP, one of the primary requirements for clear, lucid code is separation of logic and layout. This is an important thing, but what is the best way to achieve this? Unlike JSP or ASP.NET, which both promote separation of layout and logic by the themselves (eg. ASP.NET's codebehind feature, both technologies' special tags), or Ruby, Perl and Python, which are usually used with a web framework, PHP implies the opposite: by letting developers jump out of scripting into HTML and back in again with a simple "<?php" and "?>", PHP is the best language for creating a code soup by mixing HTML and PHP into an entropic, unmaintainable mess of a script.

Since few choose to use a whole PHP framework (and assuming you don't either), the only way you're going to achieve this effect is by willingly imposing on yourself certain rules of how to construct PHP pages. Fear not, there is a beautiful little method web developers use to split their code and layout, and that is: templates.

articles-templates
Many modern web sites utilise templates
Now, there's a chance you haven't heard of templates, or (more likely) you have heard of them in the context of web frameworks having a templating feature, but you aren't quite sure why templates are used, how to go about making a templating system of your own, or even if you'll benefit from having one. Well, think of it like this: imagine a large website, with thousands of dynamic pages printing all kinds of things for the site's users, that runs on PHP. Just imagine how many times the same HTML is sent to clients: thousands of different pages printing the same headers (perhaps with variations?), some of them use only certain parts of a HTML layout, others embed the same HTML within something else. It's a whole load of HTML. And of course there are the admins, constantly adding new pages, modifying the layout and improving it all the time - perhaps you don't want to grant FTP access to every one of them, but want a web-based panel where they can edit the layout. Now, if all this HTML was hard coded into the .php's, that would mean a damn lot of template bugs, broken pages, missing footers because the footer has been replaced with a better one, but several site areas still use the old one, ... you get the picture. It would be rather chaotic, in the least. There is no way to efficiently store all that HTML in the scripts without making the site insanely difficult to maintain (and you can't have a web-based layout editing panel then). Even worse, just imagine how much code would be (needlessly) repeated! Whew! Well, templates are the solution.

A templating system functions in the following way: you store some smaller chunks of HTML somewhere, with special placeholders within them that will get replaced by the content of your choice. Then, in your script, you just put the template code into a variable and replace it with the content you've generated. These template placeholders can also be replaced by whole other templates, so you can effectively break down your page into smaller pieces that you put together in the way you choose. This is quite possibly one of the most efficient and handy ways to control the output of your pages.

So like I've said, a template is a piece of HTML code containing some "dynamic stuff". What this is depends almost entirely on what the templating system can handle - it can be just variables that act as placeholders for data such as usernames, links, ID numbers, etc, or executable code (or pseudo-code) that you can use to control how the template is rendered from within the template itself. The rest of this article will assume you just want to insert variables into a template, since embedding executable code is a bit of a quick hack, and I've never had the need to do it.

A script using a templating system is usually a lot shorter and it looks like all you're doing is checking permissions and setting variables, and then at the end you call some procedures and a page is outputted. Having your site use a templating system will save you lots of time, and allow you to focus more on getting the site logic right (you have as much space as you like for doing it properly, there is no HTML crowding you and making your code difficult to read) and less time on thinking how your site will look like. There are other fantastic benefits. For example, if you decide to redesign your site, you can leave the code as it is, and just edit the templates. You can rearrange the template variables, remove some if you deem them unnecessary, and since no HTML remains directly coded into the website, the new web interface can have nothing in common with the previous, but it will still do the same things and contain the same information.

Or, for example, if more people are working on a web site, and some specialise in the design of a site, while some are coding it (splitting into two teams, design and coding is a common choice), a templating system is the best solution, since the web designers can make the web interface without knowing any PHP whatsoever (all they have to know is the names of the variables that should be embedded into the content, and what each will display) and the developers can write the server-side coding without having to pay any attention to making the output look nice. And if they are stored in a database, the templates can be loaded with exceptional ease and efficiency. With a decent templating system, a template needs only to be extracted and loaded once: afterwards it can be used a limitless number of times, and it can contain different data each time. Suffice to say, templates rock, and I'll leave the discovery of how templates make your application better in other ways to you. Now let's take a pleasant tour around the workings of a good templating system.

A templating system is simpler than it looks. The whole thing should start with a "template declaring" of sorts. This usually happens somewhere near the start of a script; but essentially before you "cache" the templates. What this really is is declaring an array of the names of the templates you're going to be using (every template having a unique name that you'll refer to it by) in your script, so they can all be retrieved by a caching process (this means you can fetch them all in one query if you choose to store them in a database, or one file open otherwise). I have the code for caching templates in a globally included script that loads all required classes and functions, so templates are loaded automatically for each page I make. The forum software MyBB does this by declaring a string containing a list of names separated by commas. Vanilla uses a similar thing, but in a pretty stange way. vBulletin makes it's template declaration as an array of strings, each the name of a template. I have found that using an array is the clearest, most practical way. Here's how this would look in PHP.

<?php 
$templates = array(
'gretting',
'navigation',
'userpanel',
);
?>
]]>

All the array does is state what templates may be used later in the script. A good templating system would allow the loading of templates not stated in the template pre-caching array, but would log this so that the developer could add the template into the declaration. And obviously, the less templates used, the better - so loading all your templates and using only several would be inefficient and unnecessary. This allows you to load what only that what you will use. The purpose of declaring them at the start of the script is so that all the names of the templates can be glued together and converted into a single SQL query: a query for each template wouldn't be as efficient. Here's how this might look:

<?php 
if (!isset($templates))
{
// disable all template functionality
define('ELECTRON_DISABLE_TEMPLATES', true);
$templates = array();
}
else
{
$templates = array_merge($templates, array(
'DEFAULT',
'doctype',
'sitestats_box',
'userbox_guest',
'userbox_member',
));
}

cache_templates($templates);
?>
]]>

This snippet is real code from an early version of this website. The first part checks if the templates were pre-cached. The second part adds some always-used templates to the array to be cached. This is useful if you have templates that you use on each page, and you don't want to write them out every time. In the first if statement, if the templates weren't pre-cached at all (no $templates was declared), the script assumes you don't want templates active at all (useful for some scripts like redirect pages, where you want to avoid the overhead of loading templates) and defines a constant that is checked for in cache_templates(). If it exists, the function just returns. But if it isn't, it takes the templates and puts the array pieces together into a string, and queries for templates. It then stores them in some variable to be used later in the script.

You may notice that one template has been written with all capital letter. This is just a convention I have in place: an uppercase template is one that prints a whole page out, from <html> to </html>, and lowercase templates are just HTML fragments, like the doctype declaration, a table of information or other. What other templates do I have uppercase? Well, for example, I chose to use templates for generating XML feeds too - one is called RSS, one ATOM and similar. They both output the main structure for their respective formats.

Ok, so now you reach the point where you have stated what templates you want to use, and they have been extracted from the database and are in a variable, a logical name for it might be $templatecache. This variable is an associative array of information with template names as keys and template HTML as values. It's likely to be very big (still, depending on the number of loaded templates) so make sure you don't "lose it" somewhere or create it twice. Memory leaks aren't possible in PHP, but you will still want to only make it once, naturally. You may want to fiddle around with the efficiency of your templating system, by echoing out the size of the $templatecache variable (using sizeof()). Then try and split up your HTML into more templates and see if that reduces it's size. If it does, it means that you have reused HTML, definitely a good thing. And it's important to know that as easy as it would be to simply copy and paste a whole page into the database and call it a template, it would be inefficient (lots of the same HTML) and that's not what a template system is about: it's about small pieces of HTML being put together. If you had basic calculus, you'll know that integrals only become accurate when dx approaches zero - I admit that's not entirely the case here, because it's easier on the developer (that means you) to have small templates, not one- or half-tag templates! This whole site has only a few uppercase, full-page templates, so if you want it efficient, take your time creating your templates. They will save you lots of work and are as efficient as it gets if implemented correctly.

A template cache is a big variable, and you just used a relatively large amount of time and memory to make it, so it's no good to you sitting idle! Let's see how you can put your templates to good use. The templates are currently just HTML with PHP variables in them. The PHP variables haven't been replaces with their values yet, so after you cache templates it's time to use your super coding skills to do whatever the script does. For instance, some globally-used script might check cookies and find out if the currently registered user is logged in, and then set a variable to contain a bunch of links for users or for guests (login, register and home for guests) and one for logged in users (logout usercp and home). This variable will take some place in your template, so you have to set it before you evaluate the variables in the template - have them replaced with their values. Your main template (DEFAULT, for example) might conatin the positions of other templates:

{$templatevars['doctype']} 
<html>
<head>
<title>{$templatevars['title']}</title>
</head>

<body>
{$templatevars['bodycontent']}
</body>
</html>
{$templatevars['title']} {$templatevars['bodycontent']} ]]>

$templatevars is an array of strings used in templates. $templatevars could be a string you set for the web page's title. $templatevars however, is in our example a template. The doctype template was one of the global templates for every page, so can you put the template into $templatevars and then use it in the DEFAULT template? Of course, that's how templates work. You fetch them whenever you need them and store their evaluated versions in a variable to be used in the next template. It may be difficult to keep track of where you define which template, so be careful to load them in the correct order (smaller templates first, and then put them all together in a big template). When you load a template, what happens is that you call a function (fetch_template() for instance) that takes a non-evaluated template from the templatecache variable and optionally puts comments around the HTML, so you know where which template begins and ends when you look at the source. It would not be practical to evaluate templates inside the function, because you would either have to declare every single template variable as global within the template function, or use $GLOBALS in the very templates... which is not very pleasant. So the best way to evaluate the template variables into their values would be like this:

<?php 
eval('$templatevars[\'doctype\'] = "' . fetch_template('doctype') . '";');
?>
]]>

fetch_template() simply returns the value of the 'doctype' key in $templatecache (or queries for the template and logs the error if it wasn't set in $templatecache), wrapping start and end comments around the template HTML, and since the string is double-quoted, PHP automatically replaces any variables in the string to their values. The result is an evaluated template, one ready to now be used in the main template. Note: in this case, the evaluation was unnecessary because the doctype template contains no variables to be evaulated -- you might as well have simply done $templatevars = fetch_template('doctype');. However, other templates will have variables to load, so it's better to evaluate them all like this.

Well, now you have the templates and all template variables (some of them containing sub-templates), so the final thing to do is to print out the final, DEFAULT template and replace the variables. Here's how it was done on an Electron page:

<?php 
// everything is done, print out the default template
eval('print_output("' . fetch_template('DEFAULT') . '");');
?>
]]>

The DEFAULT template is like a cast: it determines the locations of other templates in the final HTML, and also prints those constant tags like <html>. Of course, the one I provided was simple. It's likely you will have many more sub-templates, some for meta tags, some for tables and forms, lists, etc... Now, what happens here is that the last template is fetched, DEFAULT, and it is passed (double-quoted of course) into another function, print_output(). I made this one just send some headers and output. So there you have it. Your page has now been successfully built up out of individual fragments of HTML, arranged and outputted. And of course, all the variables have been changed from $templatevars to 'JoeSchmoe211', etc.

And that's how to go about writing your own templating system. You should write it to the needs of your site rather than copy-paste my code, but if it interests you, here is how I declared the templating functions:

<?php 
function cache_templates($templates)
{
global $electron;

// quit if templates are not required for this script
if (defined('ELECTRON_DISABLE_TEMPLATES'))
{
return;
}

$required = '';
foreach ($templates as $name)
{
$required .= ", '" . $name . "'";
}
// trim the comma from the front of the string
$required = substr($required, 2);

$templateres = $electron->db->query("
SELECT templatedata, templatename
FROM " . TABLE_PREFIX . "templates
WHERE templatename IN ($required)
ORDER BY templateid
");

while ($template_fetched = $electron->db->fetch_assoc($templateres))
{
$electron->templatecache["{$template_fetched['templatename']}"] = $template_fetched['templatedata'];
}
}

function fetch_template($templatename, $htmlcomments = true, $escape = true)
{
global $electron;

// quit if templates are not required for this script
if (defined('ELECTRON_DISABLE_TEMPLATES'))
{
return;
}
if (!isset($electron->templatecache["$templatename"]))
{
// this should not really happen - that's what template caching is for
// raise a low error, this will be noted in the debug information log
$electron->errorcollector->add('Template ' . $templatename .
' called but not cached.', __FILE__, __LINE__, ERROR_LOW);

$templateresult = $electron->db->query_first("
SELECT templatedata FROM " . TABLE_PREFIX . "templates
WHERE templatename = '$templatename'
");
$electron->templatecache["$templatename"] = $templateresult['templatedata'];
}

$template = $electron->templatecache["$templatename"];

if($htmlcomments &amp;&amp; $electron->params['template_comments'])
{
$template = "<!-- start of template: $templatename -->\n$template\n<!-- end of template: $templatename -->\n\n";
}
if($escape)
{
$template = str_replace("\\'", "'", addslashes($template));
}

return $template;
}
?>
db->query(" SELECT templatedata, templatename FROM " . TABLE_PREFIX . "templates WHERE templatename IN ($required) ORDER BY templateid "); while ($template_fetched = $electron->db->fetch_assoc($templateres)) { $electron->templatecache["{$template_fetched['templatename']}"] = $template_fetched['templatedata']; } } function fetch_template($templatename, $htmlcomments = true, $escape = true) { global $electron; // quit if templates are not required for this script if (defined('ELECTRON_DISABLE_TEMPLATES')) { return; } if (!isset($electron->templatecache["$templatename"])) { // this should not really happen - that's what template caching is for // raise a low error, this will be noted in the debug information log $electron->errorcollector->add('Template ' . $templatename . ' called but not cached.', __FILE__, __LINE__, ERROR_LOW); $templateresult = $electron->db->query_first(" SELECT templatedata FROM " . TABLE_PREFIX . "templates WHERE templatename = '$templatename' "); $electron->templatecache["$templatename"] = $templateresult['templatedata']; } $template = $electron->templatecache["$templatename"]; if($htmlcomments && $electron->params['template_comments']) { $template = "\n$template\n\n\n"; } if($escape) { $template = str_replace("\\'", "'", addslashes($template)); } return $template; } ?> ]]>

So how about it? Has this article on templates revolutionarised your way of percieving dynamic content generation? Have they saved you time when updating your site? Or something else? Comment below and share your experiences!
electron**

electron's avatar
Mar 13 2008 @ 00:16:15
The caching I implemented means you only run one query to get all the required templates, and they can be used as many times as necessary, which is not inefficient. Smarty uses complex parsing of template conditionals and control statements, which is definitely more bloated than this. And would I hate using a whole templating engine when I would use only 10% of those features.

If the leading bunch of forum software use this approach, it can't be bad or inefficient.
Navarr^

Navarr's avatar
Mar 12 2008 @ 20:50:57
This seems like a rather inefficient way to use PHP Templates. There is always the much more powerful Smarty PHP class for powerful templating and caching.