开发者

dynamic token parsing

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['[pr开发者_StackOverflow社区operty_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.


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)


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.


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; ?>
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜