16

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!

Pez Cuckow
  • 14,048
  • 16
  • 80
  • 130

6 Answers6

22

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.

Thai
  • 10,746
  • 2
  • 45
  • 57
  • 12
    `eval (func_get_arg(0));` OUCH!! – RobertPitt Mar 10 '11 at 16:45
  • 3
    Actually, I don't think that `eval` is that bad in PHP. Some frameworks actually do `eval` a lot. – Thai Mar 10 '11 at 17:11
  • Can you name some please, and just because its within a framework does not mean its perfect. – RobertPitt Mar 10 '11 at 17:13
  • As an example, Yii uses `eval` to render some contents, such as `cssClassExpression` property, and use `require` to render view files, as their view files are already in PHP and does not need preprocessing. About being perfect, I already said in the answer that, this is not the best method, and of course, is not perfect. – Thai Mar 10 '11 at 17:26
  • 1
    Another good point of generating a PHP script is that it can be improved to compile into a PHP template then included later, that would eliminate the use of `eval`. – Thai Mar 10 '11 at 17:28
  • In the end I used this comment and others to build a basic parsing engine implementing your ideas, that is almost as fasr as some mainstream parsing/caching engines but only one (relatively short) page! http://labs.pegproductions.com/pegParse – Pez Cuckow Jul 17 '12 at 08:43
  • 1
    "include" is eval(file_get_contents($filename)); :) – nerkn Aug 13 '13 at 18:25
  • can we add count for loop ? Thanks – SayJeyHi May 07 '16 at 18:19
6

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

RobertPitt
  • 56,863
  • 21
  • 114
  • 161
1

If I'm not worried about caching or other advanced topics that would push me to an established template engine like smarty, I find that PHP itself is a great template engine. Just set variables in a script like normal and then include your template file

$name = 'Eric';
$locations = array('Germany', 'Panama', 'China');

include('templates/main.template.php');

main.tempate.php uses an alternative php tag syntax that is pretty easy for non php people to use, just tell them to ignore anything wrapped in a php tag :)

<h2>Your name is <?php echo $name; ?></h2>
<?php if(!empty($locations)): ?>
  <ol>
    <?php foreach($locations as $location): ?>
    <li><?php echo $location; ?></li>
    <?php endforeach; ?>
  </ol>
<?php endif; ?>
<p> ... page continues ... </p>
Anony372
  • 494
  • 2
  • 15
  • Smarty only cache's its on mess. – RobertPitt Feb 16 '11 at 14:45
  • Here is someone who wrote a tutorial for what you're trying to do. I didn't read enough to comment on the quality of the code, but it looks like something you could take inspiration from to at least get you started: http://www.codewalkers.com/c/a/Display-Tutorials/Writing-a-Template-System-in-PHP/ – Anony372 Feb 16 '11 at 14:48
  • Unfortunately doesn't include loops, but looks interesting! Thanks – Pez Cuckow Feb 16 '11 at 15:02
  • Oh, I must have completely missed that! /me Reads over it again! – Pez Cuckow Feb 16 '11 at 15:14
  • @Ericson578 I was talking about the link you sent xD – Pez Cuckow Feb 16 '11 at 15:41
  • oh my mistake. Yeah that link was for a pretty basic template engine. More like a starter project :) Was that along the lines of what you were looking for? – Anony372 Feb 16 '11 at 16:12
  • It looks like that is what I am looking for, am working from that and what you posted atm. – Pez Cuckow Feb 16 '11 at 16:35
1

I had a very basic answer to something KINDA like this back before I started using DOMXPath.

class is something like this (not sure if it works quite like what you want but food for thought as it works very simple

<?php
class template{
private $template;

function __CONSTRUCT($template)
{
    //load a template
    $this->template = file_get_contents($template);
}

function __DESTRUCT()
{
    //echo it on object destruction
    echo $this->template;
}

function set($element,$data)
{
    //replace the element formatted however you like with whatever data
    $this->template = str_replace("[".$element."]",$data,$this->template);
}
}
?>

with this class you would just create the object with whatever template you wanted and use the set function to place all your data.

simple loops after the object is created can probably accomplish your goal.

good luck

steve
  • 290
  • 1
  • 11
-5

Smarty :) ...

php:

$smarty->assign("people",$peopleArray)

smarty template:

{foreach $people as $person}
<b>{$person.name}</b> {$person.surname}<br />
{/foreach}

Couple other things to do but that's what smarty will be like essentially.

Brian
  • 8,418
  • 2
  • 25
  • 32
  • note, you can assign tile etc in same way. then {$title} and so on to display them. – Brian Feb 16 '11 at 14:33
  • 1
    "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." – Pez Cuckow Feb 16 '11 at 14:34
  • well this is exactly what Smarty's for and is used extensivley by professionals for exactly this type of solution. No point re-inventing the weel! – Brian Feb 16 '11 at 14:35
  • 1
    I am not trying to be a professional, I am just trying to create something for experience and control over what it does. Thanks for your answer anyway however. – Pez Cuckow Feb 16 '11 at 14:40
-12

Use Smarty.

powtac
  • 40,542
  • 28
  • 115
  • 170
  • 3
    "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." – Pez Cuckow Feb 16 '11 at 14:33
  • This is not a trivial question. Better ask why people recommend Smarty! Maybe they have already the experience you are looking for. – powtac Feb 16 '11 at 14:42
  • 1
    I am aware that quite a few people recommend Smarty, however as I said I would much prefer to use something I have created and understand 100%, rather than learn syntax for some template engine that I do not have a full understanding of how it works. I have seen engines very similar to what I am looking created by people before with a little effort – Pez Cuckow Feb 16 '11 at 14:44