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');
?>