2
\$\begingroup\$

I need help refactoring code for my PHP template engine. I just have this feeling that the code is too disorganized and can be improved performance-wise. My main concerns are running a preg_match on every line in the file and hard-coding functionality for conditional and repeat blocks.

<?php
class Template extends Component
{
 private static $actions = array(); // Template actions
 private $data = array(); // Template data
 private $path = null; // Path to template file
 private $in_block = false; // In conditional block
 private $execute_block = false; // Whether to execute block
 private $repeat_block = false; // In repeat block
 private $repeat_data = null; // Repeat data
 private $repeat_start = 0; // Start of repeat index
 private $repeat_end = 0; // End of repeat index
 private $repeat_buffer = array(); // Stores repeated code
 // Registers a template action, makign it available to use inside a template
 public static function registerAction($action_name, $function_handler)
 {
 if (!is_callable($function_handler))
 trigger_error(sprintf('Function %s does not exist in Template::registerAction', $function_handler), E_USER_ERROR);
 self::$actions[$action_name] = $function_handler; 
 }
 // Runs a template action and returns the result
 public function executeAction($action, $arguments)
 {
 if (array_key_exists($action, self::$actions))
 {
 // If there is just one argument being passed, then don't pass an array
 if (count($arguments) == 1)
 $arguments = $arguments[0]; 
 return call_user_func_array(self::$actions[$action], array($arguments, $this)); 
 }
 }
 // Initializes the Template component
 public function __construct($path, $data = null)
 {
 $this->path = $path; 
 if (is_array($data))
 $this->data = $data; 
 }
 // Adds or modifies an entry in the template data
 public function set_data($key, $value)
 {
 $this->data[$key] = $value; 
 }
 // Returns an entry in the template data
 public function get_data($key)
 {
 return $this->data[$key]; 
 }
 // Returns all template data
 public function all_data()
 {
 return $this->data; 
 }
 // Starts a conditional block
 public function startBlock($execute)
 {
 $this->in_block = true; 
 $this->execute_block = $execute ? true : false; // Only allow boolean values
 }
 // Starts a repeat block
 public function startRepeatBlock($repeat_data)
 {
 // If data does not exist, then do not execute the block
 if (empty($this->data[$repeat_data]) || count($this->data[$repeat_data]) == 0)
 return; 
 $this->repeat_data = $repeat_data; 
 $this->repeat_block = true; 
 $this->repeat_end = is_array($this->data[$repeat_data][0]) ? count($this->data[$repeat_data]) : 1; 
 }
 // Ends a conditional or repeat block. If latter, then executes all code in repeat buffer. 
 public function endBlock()
 {
 $this->in_block = false; 
 $this->execute_block = false; 
 // If a repeat block ended, then process the entire block
 if ($this->repeat_block)
 {
 $data = $this->data[$this->repeat_data];
 $final = ''; 
 // If there are no arrays in the array, then change it into that form
 if (!is_array($data[0]))
 $data = array($data);
 // Begin processing
 while ($this->repeat_start < $this->repeat_end)
 {
 $entry = $data[$this->repeat_start++]; 
 // Set the appropriate variables
 foreach ($entry as $key => $value)
 {
 $key = $this->repeat_data . '[' . $key . ']'; 
 $this->data[$key] = $value;
 }
 // Process the entire block once
 foreach ($this->repeat_buffer as $arr)
 {
 if (count($arr) == 3)
 {
 list($before, $after, $entry) = $arr; 
 $line = $before . $this->processEntry($entry) . $after; 
 }
 else
 {
 $line = $arr; 
 }
 $final .= $line; 
 }
 }
 // Reset the repeat variables
 $this->repeat_block = false; 
 $this->repeat_data = null; 
 $this->repeat_start = 0; 
 $this->repeat_end = 0; 
 $this->repeat_buffer = array(); 
 // Return the final result result
 return $final; 
 }
 }
 // Returns whether the current line of code is allowed to be executed or not
 public function canExecute()
 {
 return !$this->in_block || ($this->in_block && $this->execute_block);
 }
 // Renders the template, displaying it to the user
 public function render()
 {
 echo $this->content(); 
 }
 // Returns the final, parsed template data
 public function content()
 {
 if (!file_exists($this->path))
 return;
 /* 0 = before, 1 = entry #1, 2 = entry #2, 3 = after */
 $content = file_get_contents($this->path); 
 $pattern = '/^(.*)\<\%\s*(.+)\s*\%\>(.*)$/'; 
 $lines = explode("\n", $content); 
 $javascript = false; 
 foreach ($lines as $line)
 {
 // Pattern match the line
 preg_match($pattern, $line, $matches); 
 $result = ''; 
 // If there are no matches, then don't parse it and add the raw string to output
 if (count($matches) == 0)
 {
 // Only add if the "block conditions" are correct and repeat mode is off
 if ($this->canExecute() && !$this->repeat_block)
 $final .= $line; 
 // If repeat block mode is on, then add it to the repeat buffer
 if ($this->repeat_block)
 $this->repeat_buffer[] = $line; 
 continue; 
 } 
 // Parse the matches and run the appropriate action 
 $before = $matches[1]; 
 $after = $matches[3]; 
 $entry = $matches[2]; 
 $return = ''; 
 // If the entry signals a block end of "end()" then close the block
 if (trim($entry) == "end()")
 $final .= $this->endBlock();
 // Only run the entry if it is in an executable block
 if (!$this->canExecute())
 continue; 
 // If this is a repeating block, then don't process, instead "record" the line
 if ($this->repeat_block)
 {
 $this->repeat_buffer[] = array($before, $after, $entry);
 continue; 
 }
 // Process the entry
 $return = $this->processEntry($entry);
 // Add the processed line to the final result if not empty
 $result = $before . $return . $after; 
 if (!empty($result)) $final .= $result . "\n"; 
 }
 return $final; 
 }
 // Processes a special block 
 public function processEntry($entry)
 {
 // If "()" exists in the entry, then it's a function call
 if (strpos($entry, ')') !== false)
 {
 list($function, $arguments) = explode('(', $entry, 2);
 $arguments = array_map('trim', explode(',', substr($arguments, 0, strlen($arguments) - 2)));
 return $this->executeAction($function, $arguments); 
 }
 // Otherwise, assume it's a variable
 return html($this->data[clean($entry)]); 
 }
}
/* Define some basic template actions */
function TemplateAction_raw($string, $obj)
{
 return html_entity_decode($obj->get_data($string), ENT_QUOTES); 
}
function TemplateAction_multiline($string, $obj)
{
 return nl2br(html($obj->get_data($string))); 
}
function TemplateAction_json($data, $obj)
{
 return json_encode($obj->get_data($data));
}
function TemplateAction_include($location, $obj)
{
 $path = SNIPPETS . $location . '.php'; 
 $template = (new Template($path, $obj->all_data())); 
 return $template->content(); 
}
function TemplateAction_setTitle($title, $obj)
{
 $obj->set_data('page_title', $title);
}
function TemplateAction_title($_, $obj)
{
 $title = $obj->get_data('page_title'); // For some reason I need a separate variable
 if (empty($title))
 return Config::read('HTML.default_title');
 return html(Config::read('HTML.title_prefix') . $obj->get_data('page_title') . Config::read('HTML.title_postfix'));
}
function TemplateAction_flash($_, $obj)
{
 return html($obj->get_data('flash_message'));
}
function TemplateAction_no_flash($_, $obj)
{
 $obj->startBlock(strlen($obj->get_data('flash_message')) == 0);
}
function TemplateAction_iterate($string, $obj)
{
 $obj->startRepeatBlock($string);
}
/* Register template actions */
Template::registerAction('raw', 'TemplateAction_raw'); 
Template::registerAction('multiline', 'TemplateAction_multiline');
Template::registerAction('json', 'TemplateAction_json');
Template::registerAction('include', 'TemplateAction_include'); 
Template::registerAction('setTitle', 'TemplateAction_setTitle');
Template::registerAction('title', 'TemplateAction_title'); 
Template::registerAction('flash', 'TemplateAction_flash'); 
/* Register template blocks */
Template::registerAction('no_flash', 'TemplateAction_no_flash'); 
/* Register the one and only repeat block */
Template::registerAction('iterate', 'TemplateAction_iterate');
?>
asked Jan 11, 2012 at 1:35
\$\endgroup\$

2 Answers 2

3
\$\begingroup\$

Once I saw a similar templating engine, and it was so unnecessarily complex that I replaced it with something stupidly simple:

function php_as_template( $_t_filename, $_t_vars = null ){
 // make local variables from values in $_t_vars
 if( $_t_vars ) foreach( $_t_vars as $_t_k => &$_t_v) $$_t_k =& $_t_v;
 ob_start();
 include $_t_filename;
 $_t_result = ob_get_contents();
 ob_end_clean();
 return $_t_result;
}

Usage example:

echo php_as_template('hello.php',array('a'=>'Hello','b'=>array('World!')));

hello.php:

<h1><?php echo $a; ?><?php echo $b[0]; ?></h1>
<?php
function php_as_template( $_t_, $_t_vars = null ){
 // make local variables from values in $_t_vars
 if( $_t_vars ) foreach( $_t_vars as $_t_k => &$_t_v) $$_t_k =& $_t_v;
 ob_start();
 $_t_ = '?'.'>'.$_t_;
 $_t_ = preg_replace_callback( '/<\%\s*(.+?)\s*\%\>/', 'pattern_callback', $_t_ );
 echo 'PROCESSED TEMPLATE: '.str_replace( "\n","<br>",htmlspecialchars( $_t_ ));
 eval( $_t_ );
 $_t_result = ob_get_contents();
 ob_end_clean();
 return $_t_result;
}
function pattern_callback( $matches ){
 global $template_functions;
 $s = $matches[1];
 if( preg_match('/^(\w+)\s*\((.*)\)/', $s, $m )){ // start of function call
 if( array_key_exists( $m[1], $template_functions )){ // function exists
 return '<?php echo call_user_func_array('
 .'$GLOBALS["template_functions"]["'.$m[1].'"], array('.$m[2].')); ?>';
 }
 }
}
$template_functions = array(
//'action name called in template' => anything call_user_func_array accepts
 'json' => array('TemplateActions','json') // static class function
 ,'dump' => 'var_dump' // plain func
);
class TemplateActions {
 static function json( $data ){ return json_encode( $data ); }
}
$a = 'Hello';
$b = array('World!');
$template = '
 <h1> <?php echo $a; ?> <?php echo $b[0]; ?> </h1><br>
 The JSON is:<br>
 <% json( array(1,$a,$b) ) %><br>
 <% dump( array(1,$a,$b) ) %>
';
echo php_as_template( $template ,compact('a','b'));
answered Jan 12, 2012 at 16:45
\$\endgroup\$
2
  • \$\begingroup\$ Yes, that is a very simple template engine. But what if i want more advanced functionality? For example in my engine I have template actions which are basically "functions" that can be called inside of the templates. How would I go about implementing that without making things overly complex? \$\endgroup\$ Commented Jan 13, 2012 at 1:41
  • \$\begingroup\$ See new example, hopefully it helps. \$\endgroup\$ Commented Jan 13, 2012 at 9:27
1
\$\begingroup\$

For a standard (W3C) and complete (with PHP registered functions) template engine, see this tutorial for use "XSLT 1.0 + PHP" templating: http://en.wikibooks.org/wiki/PHP_Programming/XSL/registerPHPFunctions

To build your "every no standard" template engine, a good start point is the "smallest PHP template engine": https://code.google.com/p/smallest-template-system/wiki/PHP

PS: if you reply comments here, we can extend this answer.

answered May 2, 2013 at 11:32
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.