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

?>
\$\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'));
\$\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\$
    – Ryan
    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.

\$\endgroup\$

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.