Location: PHPKode > scripts > PHP MicroTemplate > php-microtemplate/MTPL.php
<?php // $Id: MTPL.php,v 1.1.1.1 2001/11/28 18:24:51 ramenboy Exp $

/*
 * MTPL (MicroTemplate) class
 *
 * This class provides an extremely fast, lightweight templating system for
 * HTML and other text-based documents.
 *
 * Like FastTemplate and its many clones, it supports nested blocks and
 * looping. However, the entire implementation is done without a single
 * regular expression. The PHP interpreter is leveraged for its built-in
 * variable interpolation, and explode() is used to separate block markers
 * from content using a simple delimiter. As a result, the overhead of
 * reading and parsing template files is minimal. This should eliminate
 * the need for a cache and improve the overall performance of template-
 * driven sites.
 *
 * Since PHP's dollar sign notation (ie. "$var") is used to identify content
 * placeholders, it is sometimes necessary to escape dollar signs in your
 * template (ie. "\$$amount"). You can use arrays with the form
 * $array[element]; just don't quote the index. $array['element'] or
 * $array["element"] will generate PHP parse errors due to the way PHP
 * evaluates hashes in a string context. For the same reason, multi-level
 * arrays will not work ("$a[b][c]" becomes the value of $a[b] followed by
 * the literal text "[c]").
 *
 * Global variables can be accessed using the usual $GLOBALS[] array method.
 * For instance, to get the script URL, you can use $GLOBALS[PHP_SELF].
 *
 * The block markers look like "** begin: some_block **" as opposed to an
 * HTML-comment or fake HTML tag style. This means that the markers will
 * show up in a web browser if you view the template file. For a more
 * HTML-friendly version with fake tags and form element utility functions,
 * take a look at the HTPL class (HTPL.php).
 *
 * There are a few other differences. The assign() method found in most
 * PHP template classes is gone. Instead, all assigned variables must be
 * provided as a hash that is passed to the parse() method. Since in almost
 * every single case a series of assign()'s is done followed by a single
 * call to parse(), this eliminates some redundancy. It also provides a
 * more strict scoping to template variables which should result in
 * scripts that are easier to debug.
 *
 * Two variations on parse() exist: parseLoop() and parseOut(). parseLoop()
 * takes a list of hashes instead of a single hash of variables to assign
 * and calls parse() on the same block for each. This allows you to
 * populate tables and drop-downs without the need to write loops into your
 * code. parseOut() iteratively calls parse() for a given block and each of
 * its parents in order. For instance, parseOut('main.section.sub') will
 * parse 'main.section.sub', then 'main.section', then 'main'. Both of these
 * methods should reduce the amount of code you need to write for typical
 * operations.
 *
 * You will also notice that there is no out(), text(), or print() method.
 * Rather than holding onto all output internally, the parse() and related
 * methods return the resulting output. To output the result of a parse
 * operation, simply: echo parse(...);
 *
 * PHP's native error handling is used for all errors and warnings.
 * In addition, by adding "error_handling(E_ALL);" to your scripts, the
 * PHP interpreter will warn you about any uninitialized template variables.
 * Granted, the error messages are a bit odd (since interpolation is done
 * inside of an eval()), but this can be a good way to track down typos.
 * Hopefully PHP will provide stack traces on errors soon.
 *
 * If this script is opened directly, it will look for a sample template
 * file called "test.mtpl" and run some brief test cases that should
 * demonstrate how this class is to be used.
 *
 * Any comments, code suggestions, or bug-fixes would be appreciated.
 * Don't expect any big feature additions, though; my goal was to keep this
 * fast, simple, and effective. If you're looking for a more robust,
 * feature-laden solution, I'd suggest you take a look at Smarty. On the
 * other hand, if speed and transparency are your primary concerns, this
 * may be just what you have been looking for.
 *
 * Author:  Dave Benjamin <hide@address.com>
 * Source:  http://www.ovumdesign.com/
 * Created: Fri Nov 23 21:18:28 PST 2001
 * Version: $Revision: 1.1.1.1 $
 */

// Delimeter to separate block markers from content
define('MTPL_SEP', '**');

// Delemiter between block marker command and its name argument
define('MTPL_MARKER_SEP', ':');

// Block marker commands (case insensitive)
define('MTPL_MARKER_BEGIN', 'begin');
define('MTPL_MARKER_END',   'end');

// Error codes
define('E_MTPL_FILE', 1);
define('E_MTPL_END',  2);
define('E_MTPL_BLK',  3);
define('E_MTPL_UNT',  4);

// Error types (256 for error, 512 for warning) and messages for error codes
$MTPL_ERRORS = array(
    E_MTPL_FILE => array(256, 'Unable to read file: <b>%s</b>'),
    E_MTPL_END  => array(512, 'Unexpected block end marker: <b>%s</b> '
                            . '(expected <b>%s</b>)'),
    E_MTPL_BLK  => array(256, 'Unknown block: <b>%s</b>'),
    E_MTPL_UNT  => array(512, 'Unterminated block: <b>%s</b>'),
);

class MTPL {

// public:

    // Constructor - Processes a string of template content
    // To read a file, call MTPL::readFile() instead.
    function MTPL($str) {
        $out   = array();
        $stack = array();
        $alt   = false;

        foreach (array_slice($this->explodeTemplate($str), 1) as $in) {
            $stack and $path = implode('.', $stack)
                   and (isset($out[$path]) or $out[$path] = '');

            // The if-block gets executed for the first iteration,
            // then then else-block for the second, then the if-block
            // again and so forth until the template input is
            // exhausted. The if-block processes commands, and the
            // else-block processes content.

            if ($alt = !$alt) {
                // Separate the command from its argument.
                list($cmd, $name) = $this->explodeMarker($in);

                if (strtolower($cmd) == MTPL_MARKER_BEGIN) {
                    // Begin a block.
                    $stack and $this->subBlock[$path][$name] = true
                           and $out[$path] .= "\$in[$name]";
                    array_push($stack, $name);

                } elseif (strtolower($cmd) == MTPL_MARKER_END) {
                    // End a block.
                    $name == end($stack)
                        or $this->error(E_MTPL_END, $name, end($stack));
                    array_pop($stack);
                }
            } else {
                // Add content to the current block.
                $stack and $out[$path] .= ltrim($in);
            }
        }

        // If the stack isn't empty, one or more blocks were not terminated
        // properly, so generate a warning.
        $stack and $this->error(E_MTPL_UNT, end($stack));

        $this->data = $out;
    }

    // Reads a template file and returns an MTPL object
    function readFile($file) {
        $text = @file($file) or MTPL::error(E_MTPL_FILE, $file);
        return new MTPL(implode('', $text));
    }

    // Parses a block given its path and a hash of variables to assign
    function parse($path, $vars = array()) {
        $in = array();

        // Make sure that the block exists.
        isset($this->data[$path]) or $this->error(E_MTPL_BLK, $path);

        // Insert the contents of all sub-blocks, then reset them.
        if (isset($this->subBlock[$path])) {
            foreach (array_keys($this->subBlock[$path]) as $sub) {
                $in[$sub] = '';
                $subPath  = "$path.$sub";

                if (isset($this->out[$subPath])) {
                    $in[$sub] = $this->out[$subPath];
                    unset($this->out[$subPath]);
                }
            }
        }

        // Interpolate variables within the block.
        $out = $this->subst($this->data[$path], compact('in') + $vars);

        // Hold onto output if it might be needed for a parent block.
        if (strchr($path, '.')) {
            isset($this->out[$path]) or $this->out[$path] = '';
            $this->out[$path] .= $out;
        }
        
        return $out;
    }

    // Parses a block for each in a list of hashes of variables to assign
    // An optional third parameter may contain additional variables to
    // assign for every call to parse().
    function parseLoop($path, $list, $vars = array()) {
        $out = '';
        foreach ($list as $lvars) $out .= $this->parse($path, $lvars + $vars);
        return $out;
    }

    // Parses a block and each of its parents in order
    function parseOut($path, $vars = array()) {
        do $out = $this->parse($path, $vars);
        while ($path = substr($path, 0, strrpos($path, '.')));
        return $out;
    }

// protected:

    // Triggers an error given an error code and error message arguments
    function error(/* $code, ... */) {
        $args = func_get_args();
        list($lvl, $msg) = $GLOBALS['MTPL_ERRORS'][array_shift($args)];
        return trigger_error(MTPL::vsprintf($msg, $args), $lvl);
    }

    // Returns a command and argument for a block marker
    function explodeMarker($str) {
        $result = explode(MTPL_MARKER_SEP, $str) + array('', '');
        return array(trim($result[0]), trim($result[1]));
    }

    // Returns an alternating list of block markers and content
    function explodeTemplate($str) {
        return explode(MTPL_SEP, $str);
    }

    // Performs variable substitution (interpolation) on a string
    function subst($str, $vars) {
        extract($vars);
        return eval('return "' . str_replace('"', '\\"', $str) . '";');
    }

// private:

    // Calls sprintf() for an array of arguments
    // This function is supposedly available in the CVS version of PHP.
    // Here's an implementation for the rest of us. =)
    function vsprintf($str, $args) {
        $etc = '$args[' . implode('], $args[', array_keys($args)) . ']';
        return $args ? eval("return sprintf(\$str, $etc);") : sprintf($str);
    }
}

// Test cases
// ----------

// Evaluate the following code if this script is called directly.
if (realpath($SCRIPT_FILENAME) == __FILE__):

// Report all errors, warnings, and notices.
error_reporting(E_ALL);

// Start the timer.
list($a, $b) = explode(' ', microtime()); $time = $a + $b;

// Initialize a list of variable assignments for the parseLoop() example.
$vars = array(
    array('b' => 5, 'c' => 6),
    array('b' => 7, 'c' => 8),
);

// Read the template file.
$mtpl = MTPL::readFile('test.mtpl');

// Demonstrate parseLoop() and parseOut().
$mtpl->parseLoop('main.sub', $vars, array('user' => array('name' => 'joe')));
echo $mtpl->parseOut('main.sub2', array('a' => 4));

// Demonstrate basic parse().
$mtpl->parse('main.sub2');
$mtpl->parse('main.sub2');
echo $mtpl->parse('main', array('a' => 2));

// Stop the timer.
list($a, $b) = explode(' ', microtime());
$time = round($a + $b - $time, 4);
echo "<p>Time elapsed: $time seconds.</p>";

endif; // this script is called directly

?>
Return current item: PHP MicroTemplate