PHP - Simple Template Engine Part 2

In part 1, I explained how to develop a simple find and replace template engine which substituted placeholders in a template file with scalar values. In this tutorial, I will explain how to access object properties and array elements in the template.
If you haven't already done so, I would suggest reading Simple Template Engine Part 1, which explains the find and replace technique.

Part 2

The str_replace() function is used to replace a search string with a new string. It can't however replace a string with an object or an array because it doesn't know which object property value or array element you want to replace it with. In this case, the str_replace() function becomes useless. Fortunately there is an alternative, which is to use a regular expression to extract all placeholders from the template and examine the content in each placeholder.

The first thing we need to do is determine the syntax for our placeholder. In the previous article the syntax for placeholders was {@placeholder}. To use object properties and array indexes, we can modify the syntax to be {@placeholder.propertyName} or {@placeholder.arrayKey}. We need to keep in mind that object properties can contain other objects and the same applies to arrays, which can contain other arrays or objects. For example, the following placeholder {@user.address.postcode} is a valid placeholder which suggests that property "address" is an object and has a "postcode" property. This means that placeholders can contain any number of properties and arrays.

Now that the placeholder syntax has been defined, we need to extract all placeholders from the template. This can be done with a simple regular expression. Since we want to replace the placeholder with a value we can use the preg_replace_callback() function. Unlike the str_replace() function, this function will allow us to use a regular expression and a callback. Let's take a look at some code for a better understanding.

  1. echo preg_replace_callback('/\{@[0-9a-zA-z.]+\}.*?/',  function($match){   
  2.        
  3.     return "Some text";  
  4.   
  5. },$template);  
In the code sample above the preg_replace_callback() function accepts 3 arguments, a regular expression that can match a single placeholder, a callback function that is executed when a match is found and the template code. In order to replace the matched placeholder, the callback function must return the replacement string.

The next step, is to clean and extract tokens from the matched placeholder. The match in the example code will contain the curly braces and the @ symbol. These characters can be removed easily by using the substr() function. You can use a group in the regular expression to only capture the inner string of the placeholder syntax but in this case we can use the substr() function as mentioned to clean the placeholder.

Once the placeholder has been cleaned, the next step is to determine, if the placeholder contains any properties/array keyes. We do this by checking for the presence of the "." character. Checking for the "." character tells us that this placeholder will be replaced by a non scalar value. Either an object or an array. Let's take a look at the revised code from above.

  1. echo preg_replace_callback('/\{@[0-9a-zA-z.]+\}.*?/', function($match) {   
  2.        
  3.     if (isset($match[0])) {   
  4.         $placeHolder = substr($match[0], 2, -1);   
  5.   
  6.         if (strpos($placeHolder, '.') > -1) {   
  7.             // Placeholder is an object or an array   
  8.         } else {   
  9.             // Placeholder is a simple string, do a lookup   
  10.                
  11.             if (isset($parameters[$placeHolder])) {   
  12.                 return $parameters[$placeHolder];   
  13.             } else {   
  14.                 throw new Exception(sprintf("Property '%s' not found", $placeHolder)); 
  15.             }   
  16.         }   
  17.     }   
  18. } ,$template);  

As you can see on line 4, the placeholder is being cleaned. Once cleaned, the next part of the code determines if the placeholder contains a ".", which tells us it is either an object or an array replacement and further processing is require. If it's a simple placeholder, then we simply lookup its value in an array collection as discussed in Simple Template Engine Part 1. Notice on line 14 an exception is thrown if the placeholder is not found in the collection.

The final step is to process placeholders that need to be replaced with either an object or an array. To do this, the placeholder string is split into tokens at every "." character resulting in a array. We can iterate over each token and check if the token exists in the $parameters collection. This is better explained with an example.

Consider the following placeholder {@user.first_name}. Once cleaned and tokenized, it will result in an array containing two elements: array(user, first_name);

Also consider the following user object placed in the $paramaters collection:

$user = new stdClass();
$user->first_name = 'John';
$emailTemplate->addParameter('user', $user);

By iterating over the tokens, we can check if the first token "user" exists in the $parameters collection. In this case the check will evaluate to true since a parameter with the name "user" has been added. Once validated, the value ($user object)  will be inspected to determine the variable type using both the is_object() and is_array() functions. If the value is of type object, we can use the get_object_vars() function to extract all the properties from the object as an array. This array will then replace the $parameters collection temporarily and the loop will continue to the next token which is the "first_name" token. Since the $parameters collection has been temporarily replaced with an array containing all of the $user object properties, the "first_name" token will be checked against this collection and the type checking process will start again, however since the value for the "first_name" property is a scalar value ("John"), the check to determine if the "first_name" value is an object or an array will fail and in this case the value is assumed to be a scalar and will be returned. This is demonstrated in the code below.

  1. $tmp = $parameters;   
  2.   
  3. foreach ($tokens as $token) {   
  4.     if (isset($tmp[$token])) {   
  5.         $item = $tmp[$token];   
  6.   
  7.         if (is_object($item)) {   
  8.             $tmp = get_object_vars($item);   
  9.         } elseif (is_array($item)) {   
  10.             $tmp = $item;   
  11.         } elseif (is_scalar($item)) {   
  12.             return $item;   
  13.         }   
  14.     } else {   
  15.         throw new Exception(sprintf("Property or index '%s' not found", $token));   
  16.     }   
  17. }  

Using this approach you can iterate over any number of tokens, while dealing with both objects and arrays. The final code smaple below shows the revised template class from part 1, with a few usages.
  1. <?php   
  2.   
  3.     class Template {   
  4.            
  5.         private $template;   
  6.         private $parameters = array();   
  7.            
  8.         public function __construct($file = null) {   
  9.             if ($file) {   
  10.                 $this->load($file);   
  11.             }   
  12.         }   
  13.            
  14.         public function load($file) {   
  15.             if (file_exists($file)) {   
  16.                 $this->template = file_get_contents($file);   
  17.             }   
  18.         }   
  19.            
  20.         public function addParameter($key, $value) {   
  21.                 $this->parameters[$key] = $value;   
  22.         }   
  23.            
  24.         public function render() {   
  25.            
  26.             echo preg_replace_callback('/\{@[0-9a-zA-z.]+\}.*?/', function($match) {   
  27.                    
  28.                 if (isset($match[0])) {   
  29.                     $placeHolder = substr($match[0], 2, -1);   
  30.   
  31.                     if (strpos($placeHolder, '.') > -1) {   
  32.                         $tokens = explode('.', $placeHolder);   
  33.                         return $this->parse($tokens, $this->parameters);   
  34.                     } else {   
  35.                         if (isset($this->parameters[$placeHolder])) {   
  36.                             return $this->parameters[$placeHolder];   
  37.                         } else {   
  38.                             throw new Exception(sprintf( "Property '%s' not found", $placeHolder));   
  39.                         }   
  40.                     }   
  41.                 }   
  42.                
  43.             },$this->template);   
  44.         }   
  45.            
  46.         private function parse($tokens, $tmp) {   
  47.             foreach($tokens as $token) {   
  48.                 if (isset($tmp[$token])) {   
  49.                     $item = $tmp[$token];   
  50.   
  51.                     if (is_object($item)) {   
  52.                         $tmp = get_object_vars($item);   
  53.                     } elseif (is_array($item)) {   
  54.                         $tmp = $item;   
  55.                     } elseif (is_scalar($item)) {   
  56.                         return $item;   
  57.                     }   
  58.                 } else {   
  59.                     throw new Exception(sprintf( "Property or index '%s' not found", $token));   
  60.                 }   
  61.             }   
  62.         }   
  63.     }   
  64.        
  65.     // Usage.   
  66.        
  67.     $user1 = new stdClass();   
  68.     $user1->first_name = 'John';   
  69.     $user1->last_name = 'Smith';   
  70.        
  71.     $user2 = new stdClass();   
  72.     $user2->first_name = 'Syed';   
  73.     $user2->last_name = 'Hussain';   
  74.        
  75.     $users = array($user1, $user2);   
  76.        
  77.     // Load the template file.   
  78.     $emailTemplate = new Template('template.txt');   
  79.        
  80.     // Assign placeholder variable.   
  81.     $emailTemplate->addParameter('users', $users);   
  82.     $emailTemplate->addParameter('message', 'Welcome to our new mailing list.');   
  83.        
  84.     // Render the template   
  85.     echo $emailTemplate->render();   
  86.        
  87. ?>  
Template.txt

  1. <div>   
  2.     <h1>Hi {@users.0.first_name} {@users.0.last_name},</h1>   
  3.        
  4.     <p>{@message}</p>   
  5.     <p>   
  6.         Kind regards,<br/>   
  7.         {@users.1.first_name} {@users.1.last_name}   
  8.     </p>   
  9. </div>  

This brings us to the end of this tutorial. If you're thinking of developing your own template engine consider reading up on Lexers and Parsers.

No comments:

Post a Comment