Using a Template Processor Class in PHP 5

Welcome to part two of the series “Separating logic from presentation.” Comprised of three articles, this series walks you through the development of an extensible template processor in PHP 5, which you might find quite useful for separating the logic of your PHP applications from their visual presentation.

Introduction

As you’ll know (and assuming that you already took a look at the first article of the series), in the first tutorial I explained how to build in a PHP 5-driven template processor class. It exposed a few handy methods aimed at parsing the respective template file passed as an argument, by replacing its placeholders with actual data provided as an array structure.

If the above concepts are quite familiar to you, then you’ll recall that I provided my template processor class with certain interesting features, such as the ability to  recursively replace placeholders, parse dynamic PHP files, process MySQL result sets, and run PHP code included as part of the input tags array. Of course, as I said before, the class can be easily expanded, either by defining more methods or modifying the existing ones, in order to fit the particular requirements of more demanding applications. As in most cases, deciding when and how to expand the template processor class will depend on the size and complexity of the Web project where the class will be included.

Now, bringing our attention to this second installment of the series, I’ll show you how to use the template processor class by coding some sample codes, so you can have an exact idea of how to include this class either in your own PHP projects or as part of a larger shared application.

Having defined the goals for this article, let’s leap forward and begin learning more about how to take advantage of this PHP 5 template processor class. Let’s go!

{mospagebreak title=Getting started using the “TemplateProcessor” class: a quick look at its definition}

Before I proceed to demonstrate how to use the “TemplateProcessor” class, I need to remind you of how it looks, so it can be fresh in your mind. That said, here’s the corresponding definition for the class, as I wrote it originally in the first article:

// define ‘TemplateProcessor’ class
class TemplateProcessor {
    private $output=”;// set default value for general class
output
    private $rowTag=’p';// set default value for database row tag
    private $tags=array();// set default value for tags
    private $templateFile=’default_template.htm’;// set default
value for template file
    private $cacheFile=’default_cache.txt’;// set default value
for cache file
    private $expiry=10;// set default value for cache expiration
    public function __construct($tags=array()){
        if(count($tags)<1){
            throw new Exception(‘Invalid number of tags’);
        }
        if($this->isCacheValid()){
            // read data from cache file
            $this->output=$this->readCache();
        }
        else{
            $this->tags=$tags;
            // read template file
            $this->output=file_get_contents($this->templateFile);
            // process template file
            $this->processTemplate($this->tags);
            // clean up empty tags
            $this->output=preg_replace(“/{w}|}/”,”,$this-
>output);
            // write compressed data to cache file
            $this->writeCache();
        }
        // send gzip encoding http header
        $this->sendEncodingHeader();
    }
    // check cache validity
    private function isCacheValid(){
        // determine if cache file is valid or not
        if(file_exists($this->cacheFile)&&filemtime($this-
>cacheFile)>(time()-$this->expiry)){
            return true;
        }
        return false;
    }
    // process template file
    private function processTemplate($tags){
        foreach($tags as $tag=>$data){
            // if data is array, traverse recursive array of tags
            if(is_array($data)){
                $this->output=preg_replace(“/{$tag/”,”,$this-
>output);
                $this->processTemplate($data);
            }
            // if data is a file, fetch processed file
            elseif(file_exists($data)){
                $data=$this->processFile($data);
            }
            // if data is a MySQL result set, obtain a formatted
list of database rows
            elseif(@get_resource_type($data)==’mysql result’){
                $rows=”;
                while($row=mysql_fetch_row($data)){
                    $cols=”;
                    foreach($row as $col){
                        $cols.=’&nbsp;’.$col.’&nbsp;’;
                    }
                    $rows.=’<’.$this-
>rowTag.’>’.$cols.’</’.$this->rowTag.’>';
                }
                $data=$rows;
            }
            // if data contains the ‘[code]' elimiter, parse data
as PHP code
            elseif(substr($data,0,6)=='[code]'){
                $data=eval(substr($data,6));
            }
            $this->output=str_replace('{'.$tag.'}',$data,$this-
>output);
        }
    }
    // process input file
    private function processFile($file){
          ob_start();
          include($file);
          $contents=ob_get_contents();
          ob_end_clean();
          return $contents;
    }
    // write compressed data to cache file
    private function writeCache(){
        if(!$fp=fopen($this->cacheFile,'w')){
            throw new Exception('Error writing data to cache
file');
        }
        fwrite($fp,$this->getCompressedHTML());
        fclose($fp);
    }
    // read compressed data from cache file
    private function readCache(){
        if(!$cacheContents=file_get_contents($this->cacheFile)){
            throw new Exception('Error reading data from cache
file');
        }
        return $cacheContents;
    }
    // return overall output
    public function getHTML(){
          return $this->output;
    }
    // return compressed output
    private function getCompressedHTML(){
        // check if browser supports gzip encoding
        if(strstr($_SERVER['HTTP_ACCEPT_ENCODING'],'gzip')){
            // start output buffer
            ob_start();
            // echo page contents to output buffer
            echo $this->output;
            // crunch (X)HTML content & compress it with gzip
            $this->output=gzencode(preg_replace("/
(rn|n)/","",ob_get_contents()),9);
            // clean up output buffer
            ob_end_clean();
            // return compressed (X)HTML content
            return $this->output;
        }
        return false;
    }
    // send gzip encoding http header
    public function sendEncodingHeader(){
        header('Content-Encoding: gzip');
    }
}

I suppose at this point, after listing the “TemplateProcessor” class, you’re ready to see an illustrative hands-on example, in order to learn how you can use it for parsing your template files. Want to learn how this will be achieved? Please, read the next few lines.

{mospagebreak title=Parsing template files: defining the input tags for the “TemplateProcessor” class}

Right, I think the best way to demonstrate the functionality of my “TemplateProcessor” class is simply by feeding it a considerable variety of data, which will be eventually parsed and displayed within the corresponding template file, after performing the replacement of placeholders. Considering this, I’ll first define two dynamic files, “header.php” and “footer.php” respectively, which will compose the header and footer sections of a sample web page. Take a look at these two basic PHP files:

<?php
echo '<h1>This is the header section and was generated at the
following time '.date('H:i.s').'</h1>';
?>

<?php
echo '<h1>This is the footer section and was generated at the
following time '.date('H:i.s').'</h1>';
?>

Now, after listing the above PHP files, I’ll include a simple MySQL result set as part of the input of the “TemplateProcessor” class. To do this, I’ll use two additional MySQL wrapping classes, which are listed below:

// define 'MySQL' class
class MySQL{
    private $host;
    private $user;
    private $password;
    private $database;
    private $connId;
    // constructor
    function __construct($options=array()){
        if(!is_array($options)){
            throw new Exception('Connection options must be an
array');
        }
        foreach($options as $option=>$value){
            if(empty($option)){
                throw new Exception('Connection parameter cannot
be empty');
            }
            $this->{$option}=$value;
        }
        $this->connectDb();
    }
    // private 'connectDb()' method
    private function connectDb(){
        if(!$this->connId=mysql_connect($this->host,$this-
>user,$this->password)){
            throw new Exception('Error connecting to MySQL');
        }
        if(!mysql_select_db($this->database,$this->connId)){
            throw new Exception('Error selecting database');
        }
    }
    // public 'query()' method
    public function query($sql){
        if(!$result=mysql_query($sql)){
            throw new Exception('Error running query '.$sql.'
'.mysql_error());
        }
        return new Result($this,$result);
    }
}
class Result{
    private $mysql;
    private $result;
    // constructor
    public function __construct($mysql,$result){
        $this->mysql=$mysql;
        $this->result=$result;
    }
    // public 'fetch()' method
    public function fetch(){
        return mysql_fetch_array($this->result,MYSQL_ASSOC);
    }
    // public 'count()' method
    public function count(){
        if(!$rows=mysql_num_rows($this->result)){
            throw new Exception('Error counting rows');
        }
        return $rows;
    }
    // public 'get_insertId()' method
    public function getInsertId(){
        if(!$insId=mysql_insert_id($this->mysql->connId)){
            throw new Exception('Error getting insert ID');
        }
        return $insId;
    }
    // public 'seek()' method
    public function seek($row){
        if(!int($row)&&$row<0){
            throw new Exception('Invalid row parameter');
        }
        if(!$row=mysql_data_seek($this->mysql->connId,$row)){
            throw new Exception('Error seeking row');
        }
        return $row;
    }
    // public 'getAffectedRows()' method
    public function getAffectedRows(){
        if(!$rows=mysql_affected_rows($this->mysql->connId)){
            throw new Exception('Error counting affected rows');
        }
        return $rows;
    }
    // public 'getQueryResource()' method
    public function getQueryResource(){
        return $this->result;
    }
}

Now that you saw the source code of the two MySQL wrapping classes listed above, which I’ll utilize for fetching a trivial MySQL result set, the next thing to do is define the structure of a sample template file, named “default_template.htm,” that will be parsed in turn by the “TemplateProcessor” class. Please look at the signature of this example template file:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>{title}</title>
<meta http-equiv="Content-Type" content="text/html; charset=iso-
8859-1" />
<link rel="stylesheet" type="text/css" href="style.css" />
</head>
<body>
<div id="header">{header}</div>
<div id="navbar">{navbar{subnavigationbar}{subnavigationbar2}}</div>
<div id="leftcol">{leftcontent}</div>
<div id="content">{maincontent}</div>
<div id="rightcol">{rightcontent}</div>
<div id="footer">{footer}</div>
</body>
</html>

As you can see, the sample template file shown above has some placeholders located in different sections of the web page, which will be removed and replaced with actual data. In this example, both {header} and {footer} placeholders will be replaced with the dynamic output of the “header.php” and “footer.php” files respectively, while {maincontent} will be filled with the MySQL data set that I mentioned before. In addition, the nested {navbar{subnavigationbar1}{subnavigationbar2}} placeholders will be replaced with the contents of the following navigational PHP files:

<?php
// definition for 'subnavbar1.php' file
for($i=1;$i<=5;$i++){
    echo '<a href="file'.$i.'.php?fid='.$i.'">Primary
Link'.$i.'</a> | ';
}
?>

<?php
// definition for 'subnavbar2.php' file
for($i=1;$i<=5;$i++){
    echo '<a href="file'.$i.'.php?fid='.$i.'">Secondary
Link'.$i.'</a> | ';
}
?>

Right, I think that all the above dynamic PHP files, in addition to the MySQL dataset that will be fetched in turn, are enough input data for feeding the input of the “TemplateProcessor” class and seeing how it works. Therefore, go ahead read the next section to see how the corresponding template file is parsed.

{mospagebreak title=Going one step further: seeing the “TemplateProcessor” class in action}

Having previously defined some of the most representative data sources, such as dynamic PHP files and MySQL result sets, all of them included within the corresponding array of input tags, the final step to demonstrate how the “TemplateProcessor” class can be used consists of putting together all the pieces in a single PHP script and showing the pertinent parsed (X)HTML output. Below is a snippet of code that shows the template processor in action:

try{
    // include 'MySQL' class files
    require_once 'mysqlclass.php';
    require_once 'resultclass.php';
    // connect to MySQL
    $db=new MySQL(array
('host'=>'host','user'=>'user','password'=>'password',
'database'=>'database'));
    // run SQL query
    $result=$db->query('SELECT * FROM users');
    // get query resource
    $queryResult=$result->getQueryResource();
    // define input tags for template processor class
    $tags=array('title'=>'PHP 5 Template Processor','header'=>'header.php','navbar'=>array
('subnavigationbar1'=>'subnavbar1.php','navigationbar2'=>
'subnavbar2.php'),
'leftcontent'=>'leftcontent','maincontent'=>$queryResult,
'rightcontent'=>
'rightcontent','footer'=>'footer.php');
    // instantiate a new template processor object
    $tpl=new TemplateProcessor($tags);
    // display compressed page
    echo $tpl->getHTML();
}
catch(Exception $e){
    echo $e->getMessage();
    exit();
}

As you can see, the above snippet demonstrates how to include all the input tags that I defined previously in one array, which is passed as argument to a template processor object. After parsing the example “default_template.htm” template file and displaying the processed web page, the output that I get on my browser is similar to this:

In the above screenshot, you can see how the corresponding placeholders have been replaced with real data coming from the previous sample PHP files, as well as from the MySQL dataset. In this case, I displayed only a couple of rows from a “users” database table, but I’m sure you get the idea of how result sets are processed by the “TemplateProcessor” class.

Also, with regard to the above image, the last thing worth noting is the recursive replacement of the “{navbar{subnavigationbar1}{subnavigationbar2}}" placeholders with the respective “subnavbar1.php” and “subnavbar2.php” PHP files. Even when the source code of this class is easy to grasp, you can see that its parsing features are really useful.

Now that I have demonstrated how the “TemplateProcessor” class is used with a variety of mixed data sources, feel free to tweak it and modify it, in order to meet your specific requirements. The experience is fun and instructive.

Bottom line

In this article, you hopefully learned how to use the “TemplateProcessor” class, using a mixture of data sources, such as simple strings, dynamic PHP files and MySQL datasets. Also, you saw its recursive replacement capabilities in action, which can be very handy when working with complex template files.

Nevertheless, this series isn’t finished yet. In the last tutorial, I’ll show you how the base structure of the template processor can be used to develop a production-level class, which not only exposes the features that you saw before, but also implements the required logic for working with chunked caching and multiple template files. You won’t wan to miss it!

Google+ Comments

Google+ Comments