PHP Looping Template Engine - From Scratch

Pez Cuckow picture Pez Cuckow · Feb 16, 2011 · Viewed 14.2k times · Source

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!

Answer

Thai picture Thai · Feb 16, 2011

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.