Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PHP Looping Template Engine - From Scratch

For a group project I am trying to create a template engine for PHP for the people less experienced with the language can use tags like {name} in their HTML and the PHP will replace that tag with a predefined variable from an array. As well as supporting loops.

This is well beyond the expectations of the project, but as I have experience with PHP I thought it would be a good challenge to keep me busy!

My main questions are, how do I do the loop part of the parser and is this the best way to implement such a system. Before you just recommend an existing template system, I would prefer to create it myself for experience and because everything in our project has to be our own.

At the moment the basic parsing is done with regex and preg_replace_callback, it checks if $data[name] exists and if it does replaces it.

I have tried to do the loop a variety of different ways but am not sure if I am on the correct track!

An example if the data the parsing engine was given is:

Array
(
    [title] => The Title
    [subtitle] => Subtitle
    [footer] => Foot
    [people] => Array
        (
            [0] => Array
                (
                    [name] => Steve
                    [surname] => Johnson
                )

            [1] => Array
                (
                    [name] => James
                    [surname] => Johnson
                )

            [2] => Array
                (
                    [name] => josh
                    [surname] => Smith
                )

        )

    [page] => Home
)

And the page it was parsing was something like:

<html>
<title>{title}</title>
<body>
<h1>{subtitle}</h1>
{LOOP:people}
<b>{name}</b> {surname}<br />
{ENDLOOP:people}
<br /><br />
<i>{footer}</i>
</body>
</html>

It would produce something similar to:

<html>
<title>The Title</title>
<body>
<h1>Subtitle</h1>
<b>Steve</b> Johnson<br />
<b>James</b> Johnson<br />
<b>Josh</b> Smith<br />
<br /><br />
<i>Foot</i>
</body>
</html>

Your time is incredibly appreciated with this!

Many thanks,

P.s. I completely disagree that because I am looking to create something similar to what already exists for experience, my well formatted and easy to understand question gets down voted.

P.p.s It seems there is a massive spread of opinions for this topic, please don't down vote people because they have a different opinion to you. Everyone is entitled to their own!

like image 590
Pez Cuckow Avatar asked Feb 16 '11 14:02

Pez Cuckow


2 Answers

A simple approach is to convert the template into PHP and run it.

$template = preg_replace('~\{(\w+)\}~', '<?php $this->showVariable(\'$1\'); ?>', $template);
$template = preg_replace('~\{LOOP:(\w+)\}~', '<?php foreach ($this->data[\'$1\'] as $ELEMENT): $this->wrap($ELEMENT); ?>', $template);
$template = preg_replace('~\{ENDLOOP:(\w+)\}~', '<?php $this->unwrap(); endforeach; ?>', $template);

For example, this converts the template tags to embedded PHP tags.

You'll see that I made references to $this->showVariable(), $this->data, $this->wrap() and $this->unwrap(). That's what I'm going to implement.

The showVariable function shows the variable's content. wrap and unwrap is called on each iteration to provide closure.

Here is my implementation:

class TemplateEngine {
    function showVariable($name) {
        if (isset($this->data[$name])) {
            echo $this->data[$name];
        } else {
            echo '{' . $name . '}';
        }
    }
    function wrap($element) {
        $this->stack[] = $this->data;
        foreach ($element as $k => $v) {
            $this->data[$k] = $v;
        }
    }
    function unwrap() {
        $this->data = array_pop($this->stack);
    }
    function run() {
        ob_start ();
        eval (func_get_arg(0));
        return ob_get_clean();
    }
    function process($template, $data) {
        $this->data = $data;
        $this->stack = array();
        $template = str_replace('<', '<?php echo \'<\'; ?>', $template);
        $template = preg_replace('~\{(\w+)\}~', '<?php $this->showVariable(\'$1\'); ?>', $template);
        $template = preg_replace('~\{LOOP:(\w+)\}~', '<?php foreach ($this->data[\'$1\'] as $ELEMENT): $this->wrap($ELEMENT); ?>', $template);
        $template = preg_replace('~\{ENDLOOP:(\w+)\}~', '<?php $this->unwrap(); endforeach; ?>', $template);
        $template = '?>' . $template;
        return $this->run($template);
    }
}

In wrap() and unwrap() function, I use a stack to keep track of current state of variables. Precisely, wrap($ELEMENT) saves the current data to the stack, and then add the variables inside $ELEMENT into current data, and unwrap() restores the data from the stack back.

For extra security, I added this extra bit to replace < with PHP echos:

$template = str_replace('<', '<?php echo \'<\'; ?>', $template);

Basically to prevent any kind of injecting PHP codes directly, either <?, <%, or <script language="php">.

Usage is something like this:

$engine = new TemplateEngine();
echo $engine->process($template, $data);

This isn't the best method, but it is one way it could be done.

like image 119
Thai Avatar answered Nov 07 '22 05:11

Thai


Ok firstly let me explain something tell you that PHP IS A TEMPLATE PARSER.

Doing what your doing is like creating a template parser from a template parser, pointless and to be quite frank it iterates me that template parser's such as smarty have become so well at a pointless task.

What you should be doing is creating a template helper, not a parser as there redundant, in programming terms a template file is referred to as a view and one of the reasons they was given a particular name is that people would know there separate from Models, Domain Logic etc

What you should be doing is finding a way to encapsulate all your view data within your views themselves.

An example of this is using 2 classes

  • Template
  • TemplateScope

The functionality of the template class is for the Domain Logic to set data to the view and process it.

Here's a quick example:

class Template
{
    private $_tpl_data = array();

    public function __set($key,$data)
    {
        $this->_tpl_data[$key] = $data;
    }

    public function display($template,$display = true)
    {
        $Scope = new TemplateScope($template,$this->_tpl_data); //Inject into the view
        if($display === true) 
        {
            $Scope->Display();
            exit;
        }
        return $Scope;
    }
}

This is extreamly basic stuff that you could extend, oko so about the Scope, This is basically a class where your views compile within the interpreter, this will allow you to have access to methods within the TemplateScope class but not outside the scope class, i.e the name.

class TemplateScope
{
    private $__data = array();
    private $compiled;
    public function __construct($template,$data)
    {
        $this->__data = $data;
        if(file_exists($template))
        {
            ob_start();
            require_once $template;
            $this->compiled = ob_get_contents();
            ob_end_clean();
        }
    }

    public function __get($key)
    {
        return isset($this->__data[$key]) ? $this->__data[$key] : null;
    }

    public function _Display()
    {
        if($this->compiled !== null)
        {
             return $this->compiled;
        }
    }

    public function bold($string)
    {
        return sprintf("<strong>%s</strong>",$string);
    }

    public function _include($file)
    { 
        require_once $file; // :)
    }
}

This is only basic and not working but the concept is there, Heres a usage example:

$Template = new Template();

$Template->number = 1;
$Template->strings = "Hello World";
$Template->arrays = array(1,2,3,4)
$Template->resource = mysql_query("SELECT 1");
$Template->objects = new stdClass();
$Template->objects->depth - new stdClass();

$Template->display("index.php");

and within template you would use traditional php like so:

<?php $this->_include("header.php") ?>
<ul>
    <?php foreach($this->arrays as $a): ?>
        <li><?php echo $this->bold($a) ?></li>
    <?php endforeach; ?>
</ul>

This also allows you to have includes within templates that still have the $this keyword access to then include themselves, sort of recursion (but its not).

Then, don't pro-grammatically create a cache as there is nothing to be cached, you should use memcached which stores pre compiled source code within the memory skipping a large portion of compile / interpret time

like image 25
RobertPitt Avatar answered Nov 07 '22 06:11

RobertPitt