1

I have a mail script that I use in one of my projects, and I'd like to allow customization of this letter. the problem is that parts of the email are dynamically generated from the database. I have predefined tokens that I use to describe what should replace the token, but I'd like to simplify this but writing a better parser that can interpret the token and figure out which variable to use to replace it.

Right now I have a very large array with all the possible tokens and their corresponding values, like so:

$tokens['[property_name]'] = $this->name;

and then I run through the template and replace any instance of the key with it's value.

I'd prefer to just run through the template, look for [] or whatever I use to define a token, and then read what's inside and convert that to a variable name.

I need to be able to match a few levels of relationships so $this->account->owner->name; as an example, and I need to be able to reference methods. $this->account->calcTotal();

I thought I might be able to take the example [property_name] and replace the instance of _ with -> and then call that as a variable, but I don't believe it works with methods.

1
  • I added an example that has a template class to parse the tokens now with a resolver class which shows how to access both object members and properties via template tags - regardless of the depth. Commented Aug 6, 2011 at 10:32

3 Answers 3

2

PHP is already an excellent templating system on it's own.

I use a simple Template class, which accepts variables (via __set()) and then when it's time to render, just do an extract() on the array of variables, and include the template file.

This can obviously be combined with output buffering if you need to capture the result to a string rather than sending the result straight back to the browser/shell.

This gives you ability to have very simple templates, but also gives you advanced functionality if you need it (i.e. for loops, using helper classes, etc)

Sign up to request clarification or add additional context in comments.

Comments

1

You're creating sort of a template system. You can either re-invent the wheel (sort of) by coding this on your own or just using a lighweight template system like mustache.

For a very lightweight approach you can make use of regular expressions to formulate the syntax of your template variables. Just define how a variable can be written, then extract the names/labels used and replace it at will.

A function to use for this is preg_replace_callback. Here is some little example code (Demo) which only reflects simple substitution, however, you can modify the replace routine to access the values you need (in this example, I'm using a variable that is either an Array or implements ArrayAccess):

<?php
$template = <<<EOD
This is my template,

I can use [vars] at free [will].
EOD;

class Template
{
    private $template;
    private $vars;
    public function __construct($template, $vars)
    {
        $this->template = $template;
        $this->vars = $vars;
    }
    public function replace(array $matches)
    {
        list(, $var) = $matches;
        if (isset($this->vars[$var]))
        {
             return $this->vars[$var];
        }
        return sprintf('<<undefined:%s>>', $var);

    }
    public function substituteVars()
    {
        $pattern = '~\[([a-z_]{3,})\]~';
        $callback = array($this, 'replace');
        return preg_replace_callback($pattern, $callback, $this->template );
    }

}

$templ = new Template($template, array('vars' => 'variables'));

echo $templ->substituteVars();

This does not look spectacular so far, it's just substituting the template tags to a value. However, as already mentioned you can now inject a resolver into the template that can resolve template tags to a value instead of using an simple array.

You've outlined in your question that you would like to use the _ symbol to separate from object members / functions. The following is a resolver class that will resolve all global variables to that notation. It shows how to handle both, object members and methods and how to traverse variables. However, it does not resolve to $this but to the global namespace:

/**
 * Resolve template variables from the global namespace
 */
class GlobalResolver implements ArrayAccess 
{
    private function resolve($offset)
    {
        $stack = explode('_', $offset);

        return $this->resolveOn($stack, $GLOBALS);
    }

    private function resolveOn($stack, $base)
    {
        $c = count($stack);
        if (!$c)
            return array(false, NULL);

        $var = array_shift($stack);
        $varIsset = isset($base[$var]);

        # non-set variables don't count
        if (!$varIsset)
        {
            return array($varIsset, NULL);
        }

        # simple variable
        if (1 === $c)
        {
            return array($varIsset, $base[$var]);
        }

        # descendant    
        $operator = $stack[0];
        $subject = $base[$var];
        $desc = $this->resolvePair($subject, $operator);

        if (2 === $c || !$desc[0])
            return $desc;

        $base = array($operator => $desc[1]);
        return $this->resolveOn($stack, $base);
    }

    private function resolvePair($subject, $operator)
    {
        if (is_object($subject))
        {
            if (property_exists($subject, $operator))
            {
                return array(true, $subject->$operator);
            }
            if (method_exists($subject, $operator))
            {
                return array(true, $subject->$operator());
            }
        }
        if (is_array($subject))
        {
            if (array_key_exists($operator, $subject))
            {
                return array(true, $subject[$operator]);
            }
        }
        return array(false, NULL);
    }

    public function offsetExists($offset)
    {
        list($isset) = $this->resolve($offset);
        return $isset;
    }
    public function offsetGet($offset)
    {
        list($isset, $value) = $this->resolve($offset);
        return $value;
    }
    public function offsetSet ($offset, $value)
    {
        throw new BadMethodCallException('Read only.');
    }
    public function offsetUnset($offset)
    {
        throw new BadMethodCallException('Read only.');
    }
}

This resolver class can be used then to make use of some example values:

/**
 * fill the global namespace with some classes and variables
 */
class Foo
{
   public $member = 'object member';
   public function func()
   {
       return 'function result';
   }
   public function child()
   {
       $child->member = 'child member';
       return $child;
   }
}

$vars = 'variables';
$foo = new Foo;

$template = <<<EOD
This is my template,

I can use [vars] at free [foo_func] or [foo_member] and even [foo_child_member].
EOD;

/**
 * this time use the template with it's own resolver class
 */
$templ = new Template($template, new GlobalResolver);

echo $templ->substituteVars();

See the full demo in action.

This will only need a slight modification to fit your needs then finally.

6 Comments

wow this is over the top help :p thanks, i honestly didn't expect so much help so fast. not used to this! i started working on moving some code around to simplify some of my classes so i can't try your example yet but i'll go over it as soon as i can! thank you so much for taking the time to respond :)
I finished up what i was working on and was able to go over your provided solution, it looks great. i'm going to need to rewrite some code for this to work, but it should be great. I have some methods that require parameters that I don't want to use when users are modifying the template. thanks again :)
It's concepts like this that give PHP and PHP Developers a bad name in the development community.
@Stephen: To which concept are you referring to? That $GLOBALS is hardencoded in this mock-up? It's easy to make this a dependency with any other array to provide, this is just a mock-up.
@Stephen: I too would like to know what is wrong with hakre's solution. If there is something here that I shouldn't be doing you should what it is and why. His seemed to be the best solution for my problem.
|
0

I have used something similar for email templating:

function call_php_with_vars( $_t_filename, $_t_variables ){
  extract( $_t_variables );

  ob_start();
    include $_t_filename;
    $_t_result = ob_get_contents();
  ob_end_clean();

  return $_t_result;
}

echo call_php_with_vars('email_template.php',array(
  'name'=>'Little Friend'
 ,'object'=>(object)array(
    'field'=>'value'
  )
));

email_template.php:

Hello, <?php echo $name; ?>
<?php echo $object->field; ?>

1 Comment

thanks but i forgot to mention my end goal is to allow users to edit their own version of the email, so i'd like tokens that make a little more sense for them and are less intimidating.

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.