In the process of developing Lilliput CMS I had to think about how to do templating with PHP. There’s a lot of material available regarding PHP and templating and most of it is really weird. Having had a look at the Top 25 PHP template engines I can’t for the life of me understand why I would want to use something like Smarty, Savant or phptal. Obviously a lot of love and attention has been poured into these solutions but I can’t escape the feeling that these template engines are recreating PHP and its innate templating function. This feeling was confirmed when reading the “Templates and template engines” article on the php patterns website.
The article inspired the following train of thought:
- Don’t try to recreate PHP and its innate templating functions, it’s fine as it is. This doesn’t necessarily mean you should only use PHP for templating, rather that you shouldn’t rebuild PHP
- There’s no sense in separating logic and content as they are intertwined and dependent on each other; focus on separating content and presentation (with the help of clean XHTML and CSS)
- Use XHTML for templates instead of HTML 4 Strict so you can benefit from the strictness introduced by XML parsers
- Templates are created by designers, they shouldn’t need to learn a new language to accomplish their goals so the templates should use no frills XHTML
- Because the templates use XHTML and the output format is XHTML there is no need for a template processing/transformation language, i.e. XSLT
- There’s only one standardized API for manipulating content and that’s the DOM so use this for interaction with template and content
- It should be simple to the point of sacrificing functionality to keep it simple, it won’t be all things to all people
- It needs to be extensible, you can’t predict all use cases and you need to offer a way for people to introduce different behavior whilst maintaining forward compatibility
So what has this resulted in? The template needs to be valid XHTML and the designer doesn’t need to learn new language constructs (so no phptal like solution). The simplest solution is to use content elements in the template that are replaced with the generated content resulting from business logic processing. Because I’m treating the XHTML templates as real XML files we need to manipulate the content via a formal API for XML, i.e. the DOM. The ground rules for this are laid out in two succinct articles “On Postel, Again” and “Are You an XML Bozo?”. The replaceable elements are identified by XHTML id’s, found via XPath queries and replaced via a DOM manipulation as demonstrated by the following example code.
Template code:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> <head> <title>Lilliput CMS</title> <meta http-equiv="content-type" content="text/html; charset=utf-8" /> <link rel="shortcut icon" href="/lilliput/favicon.ico" /> <link rel="stylesheet" href="/lilliput/template/lilliput/css/style.css" type="text/css" media="screen, projection" /> <script src="/lilliput/template/lilliput/javascript/script.js" type="text/javascript"></script> </head> <body> <div id="wrapper"> <div id="header"> <p class="description">Helping you eat boiled eggs since 2006.</p> <h1><a href="http://feedme.mind-it.info#">Lilliput CMS</a></h1> <ul id="menu"> </ul> </div> <div id="content" /> <div id="footer"> <p>This text to be replaced.</p> </div> </div> </body> </html>
The PHP controller code (invoking model actions for retrieving data) that generates the content and passes it on to the view logic:
function controller_addUser() {
// invoke model logic to get the data
$results = model_addUser();
// start new DOM instance for document creation
$dom = new DomDocument('1.0', 'UTF-8');
$selectElement = $dom->createElement('select');
$selectElement = $dom->appendChild($selectElement);
$selectElement->setAttribute('id', 'fm-role');
$selectElement->setAttribute('name', 'fm-role');
$resultsCount = count($results);
for ($row = 0; $row < $resultsCount; $row++) {
$role = $results[$row]["role"];
$optionElement = $dom->createElement('option');
$optionElement = $selectElement->appendChild($optionElement);
$optionElement->setAttribute('value', $role);
$elementValue = $dom->createTextNode($role);
$elementValue = $optionElement->appendChild($elementValue);
}
// pass string data to view and get rendered XHTML structure back
$content["html"] = view_addUser($dom->saveXML($selectElement));
// return rendered XHTML structure to index.php dispatcher for inclusion in page template
return $content;
}
PHP template handling code using PHP 5 XML and DOM functions:
function processTemplate($templateFile, $content) {
// Initialize DOM document
$dom = new DomDocument('1.0', 'utf-8');
// load the template file into the DOM document
$returnValue = $dom->load($templateFile);
$xml = explode("\n", file_get_contents($templateFile));
// call the replaceDomContent function that substitutes <div id="content"/> with the generated content
replaceDomContent($dom, '//xhtml:div[@id="content"]', $content["html"]);
// call the getTitle function to retrieve the title element and replace the content with the generated title
$title = getTitle($dom);
replaceNodeContent($dom, "//xhtml:title", $title);
// export the DOM structure into an XML format
return $dom->saveXML();
}
function getTitle($dom) {
$xpath = new DOMXPath($dom);
$resultNode = $xpath->query("//h2");
$title = $resultNode->item(0)->nodeValue;
return $title;
}
function replaceDomContent($dom, $query, $content) {
$xpath = new DOMXPath($dom);
// register the xhtml namespace otherwise xpath queries will fail
$xpath->registerNamespace("xhtml", "http://www.w3.org/1999/xhtml");
$nodelist = $xpath->query($query);
$oldnode = $nodelist->item(0);
libxml_use_internal_errors(true);
$contentImport = new DOMDocument();
$returnValue = $contentImport->loadXML($content);
$xml = explode("\n", $content);
// import the converted XML content into the DOM structure
$newnode = $dom->importNode($contentImport->documentElement, true);
// Replace the old content with the new content
$oldnode->parentNode->replaceChild($newnode, $oldnode);
return $dom;
}
function replaceNodeContent($dom, $query, $content) {
$xpath = new DOMXPath($dom);
// register the xhtml namespace otherwise xpath queries will fail
$xpath->registerNamespace("xhtml", "http://www.w3.org/1999/xhtml");
$nodelistContent = $xpath->query($query);
$nodelistContent->item(0)->nodeValue = $content;
return $dom;
}
No related posts.
al said:
Hey just wondering if you found any resources on the performance hits you might get from using DOMDocument?
I’ve looked around and it seems the consensus is that its a bit slower.
Personally I agree 100% with what you’ve written above. And have made my one templating systems in the past with exactly those issues coming up.
November 30, 2009 at 10:35 am
Meint Post said:
Hi Al,
You might be interested in this test, looks like there is a performance hit but like the commenter at the blog states you could offset this by caching the output of the DOM operation.
Meint
November 30, 2009 at 1:13 pm