Code: Select all
<?php
/*
myTpl, A PHP Templating class using HTML comments
This unversioned release this class is not licensed.
It's a proof of concept only and will be developed further
-- to take away some of the logic required for handling loops
-- and other tricky areas.
A GPL license will be applied only when the author considers this
-- class ready for commercial use.
Author: Chris Corbyn (d11wtq, DevNetwork.net)
Date: 2006-03-03
*/
class myTpl
{
protected
$overallSource,
$stylesheet, // _STYLESHEET_
$javaScript, // _JAVASCRIPT_
$extraHead, // _EXTRA HEAD_
$caseSensitive = true,
$removeExtras = false;
public
$tplVars = array(); //Holds the parsed template as an array of tokens
/*
$obj = new myTpl( string $path [, array $tpl_vars [, bool $remove_extras] ] )
*/
function __construct($path, &$tpl_vars=false, $remove_extras=false)
{
if (file_exists($path))
{
$this->overallSource = $this->load($path);
$tpl_vars = $this->parseTplVars($this->overallSource);
$this->tplVars =& $tpl_vars;
$this->removeExtras = $remove_extras;
}
else die ('No template file could be opened at '.$path);
}
// (string)
private function load($path)
{
ob_start();
require($path);
return ob_get_clean();
}
//Recursive
// (array)
private function parseTplVars($source)
{
$ret = array();
$full_re = '@(?:<!-- ::([\w ]*?) START:: -->(.*?)<!-- ::\\1 END:: -->)|(?:<!-- ::([\w ]*?):: -->)@s';
if (!$this->caseSensitive) $full_re .= 'i';
if (preg_match_all($full_re, $source, $matches, PREG_OFFSET_CAPTURE)) //This is a "block" of code, or just a marker, we don't know just yet
{
foreach ($matches[0] as $i => $arr)
{
if (!empty($matches[1][$i][0])) //Name for a "block"
{
$ret[$matches[1][$i][0]] = array(); //We use the name as a key
if (preg_match($full_re, $matches[2][$i][0])) //Has "blocks" inside itself
{
$ret[$matches[1][$i][0]] = $this->parseTplVars($matches[2][$i][0]); //Returns array of course
$ret[$matches[1][$i][0]]['@content'] = $matches[2][$i][0]; //Keep a copy of the "block" to which these parts apply
}
else
{
$ret[$matches[1][$i][0]] = $matches[2][$i][0]; //No blocks inside this block so just store it
}
}
elseif (!empty($matches[3][$i][0])) //Not a "block", just a marker in the template
{
$ret[$matches[3][$i][0]] = ''; //Make a var, but just leave it empty
}
}
}
$ret['@content'] = $this->overallSource;
return $ret;
}
// (void)
public function setCaseSensitive($bool)
{
$this->caseSensitive = $bool;
}
//Replace a defined segment of the template
// (string)
public function replace($part, $with, $source)
{
$re = '@(?:<!-- ::'.$part.' START:: -->(.*?)<!-- ::'.$part.' END:: -->)|(?:<!-- ::('.$part.'):: -->)@s';
if (!$this->caseSensitive) $re .= 'i';
return preg_replace($re, $with, $source);
}
// (array)
public function getTplVars()
{
return $this->tplVars;
}
// (string)
public function getSource()
{
return $this->overallSource;
}
// (string)
public function getExtraHead()
{
return $this->extraHead;
}
// (string)
public function getJavaScript()
{
return $this->javaScript;
}
// (string)
public function getStylesheet()
{
return $this->stylesheet;
}
// (void)
public function setRemoveExtras($bool)
{
$this->removeExtras = $bool;
}
//Recursive
// (string)
public function render($array=false)
{
$ret = '';
if (!$array) $array = $this->tplVars;
$ret = $array['@content'];
foreach ($array as $k => $v)
{
if (!is_array($v) && $k != '@content')
{
if ($this->removeExtras)
{
$style_re = '/^_STYLESHEET_$/';
if (!$this->caseSensitive) $style_re .= 'i';
if (preg_match($style_re, $k))
{
$this->stylesheet = $v;
$v = '';
}
$js_re = '/^_JAVASCRIPT_$/';
if (!$this->caseSensitive) $js_re .= 'i';
if (preg_match($js_re, $k))
{
$this->javaScript = $v;
$v = '';
}
$head_re = '/^_EXTRA HEAD_$/';
if (!$this->caseSensitive) $head_re .= 'i';
if (preg_match($head_re, $k))
{
$this->extraHead = $v;
$v = '';
}
}
$ret = $this->replace($k, $v, $ret);
}
elseif ($k != '@content')
{
if ($this->removeExtras)
{
$style_re = '/^_STYLESHEET_$/';
if (!$this->caseSensitive) $style_re .= 'i';
if (preg_match($style_re, $k))
{
$this->stylesheet = $this->render($v);
$v = array('@content' => '');
}
$js_re = '/^_JAVASCRIPT_$/';
if (!$this->caseSensitive) $js_re .= 'i';
if (preg_match($js_re, $k))
{
$this->javaScript = $this->render($v);
$v = array('@content' => '');
}
$head_re = '/^_EXTRA HEAD_$/';
if (!$this->caseSensitive) $head_re .= 'i';
if (preg_match($head_re, $k))
{
$this->extraHead = $this->render($v);
$v = array('@content' => '');
}
}
$ret = $this->replace($k, $this->render($v), $ret);
}
}
return $ret;
}
}
?>I wanted to create a template class that would tie in perfectly with a fully OOP/PHP5 environment working with MVC. I also wanted this class to work nicely for less-savvy OOP'ers and Procedural coders.
Templates are essentially HTML files... even the syntax used in the files is HTML. Comments are used to define blocks of markup and markers in the template files... this means that you can partially parse templates if you want to without breaking the output.
Syntax inside the templates:
Everything is done using comments. Some comments (3 so far) are special keywords that myTpl will use to take copies of CSS blocks, JS blocks and additional items that need to go in the <HEAD> of the page.
Defining a section of code:
Code: Select all
<html>
<head>
<title>Example</title>
</head>
<body>
<!-- ::EXAMPLE START:: -->
<div>
This is an example
</div>
<!-- ::EXAMPLE END:: -->
</body>
</html>Defining a single marker in the code is easier still. A marker is just a point in the code where something like text may be inserted.
Code: Select all
<html>
<head>
<title>Example</title>
</head>
<body>
<div>
This div says <!-- ::EXAMPLE:: -->
</div>
</body>
</html>We can nest our syntax infinitely too:
Code: Select all
<html>
<head>
<title>Example</title>
</head>
<body>
<!-- ::BLOCK ONE START:: -->
<div>
This div says <!-- ::EXAMPLE TEXT:: -->
</div>
<!-- ::BLOCK ONE END:: -->
</body>
</html>The names of the sections/markers apply in the scope of the parent block only and absolutely.... this means that you don't have to worry about name clashes in large templates providing you only use a name once in each block you define (even nests).
For example, this will work fine, nothing clashes:
Code: Select all
<html>
<head>
<title>Example</title>
</head>
<body>
<!-- ::BLOCK START:: -->
<div>
This div says <!-- ::TEXT:: -->
</div>
<!-- ::BLOCK END:: -->
<!-- ::TEXT:: -->
</body>
</html>Code: Select all
<html>
<head>
<title>Example</title>
</head>
<body>
<div>
This div says <!-- ::TEXT:: -->
</div>
<!-- ::TEXT:: -->
</body>
</html>The template file is passed to the constructor of myTpl as a string path. When the object is instantiated myTpl runs through the template file recursively reading your defined blocks of code and markers whilst storing them in a multidimensional array. This multi-dimensional array also contains some extra information that myTpl will use to rebuild the array into a working markup (it contains values for '@content' in each array).
You can opt to pass a variable name into the second constrcutor paramater too... this forces a reference to be created both internally and externally between the array in myTpl and the variable you pass. This makes it easy to work with the array in procedural environment since changes made to the global or local variable will still reflect inside the object (quite deliberately).
The idea is that the values in the array are modified as desired before running the render() method in myTpl to fetch the resulting markup.
Syntax rules:
There aren't really many rules... the only ones I created were to avoid issues with non-standard compliant code in partially parsed templates.
The names for the tags can only contain spaces, numbers (0-9), letters (a-zA-Z) and underscores.
Magic keywords:
The magic keywords so far are "_JAVASCRIPT_", "_STYLESHEET_" and "_EXTRA HEAD_".
This work like any other tag names unless you set the $removeExtras option to TRUE, either in the 3rd constructor paramater, or by calling the method $mytpl->setRemoveExtras(true).
If $removeExtras as set TRUE these blocks of code will not be send back in the resulting markup when render() is run. Instead they will be assigned to properties of the myTpl object and access using the accessor methods:
$mytpl->getJavaScript();
$mytpl->getStylesheet();
$mytpl->getExtraHead();
The idea behind those methods is so that templates which are *not* full HTML pages can defined style sto be applied to that overall page. It's up to the developer at present to make use of the return values from these methods to apply style to a page if required.
Basic example:
Let's take a look at this template:
Code: Select all
<html>
<head>
<title>Example</title>
</head>
<body>
<!-- ::BLOCK START:: -->
<div>
This div says <!-- ::TEXT:: -->
</div>
<!-- ::BLOCK END:: -->
<!-- ::TEXT:: -->
</body>
</html>Now we fire up myTpl.
Code: Select all
//Note that $foo doesn't have to be defined, it will be created in the constructor
// Nor does $foo even need to be passed if you're using pure OOP
$tpl = new myTpl('test.tpl', $foo);Code: Select all
Array
(
[BLOCK] => Array
(
[TEXT] =>
[@content] =>
<div>
This div says <!-- ::TEXT:: -->
</div>
)
[TEXT] =>
[@content] => <html>
<head>
<title>Example</title>
</head>
<body>
<!-- ::BLOCK START:: -->
<div>
This div says <!-- ::TEXT:: -->
</div>
<!-- ::BLOCK END:: -->
<!-- ::TEXT:: -->
</body>
</html>
)OK, forget about the '@content' parts.... these are for myTpl to use.
So, we see we have [BLOCK] which contains [TEXT], we also have [TEXT] in the first level of the array. These are the values we can tickle
Code: Select all
$foo['BLOCK']['TEXT'] = 'Text inside out block';
$foo['TEXT'] = 'Text in the main body';Code: Select all
echo $tpl->render();Code: Select all
<html>
<head>
<title>Example</title>
</head>
<body>
<div>
This div says Text inside out block
</div>
Text in the main body
</body>
</html>Obviosuly we can do fancy things like building dynamic tables... that's where you need to use some clever logic but I'm racking my brain for clever way to ask myTpl to do the hard work for you
Like I say, don't use this thinking it's gonna be a perfect solution (Smarty).... but it's certainly a stroll down another road
Final considerations:
myTpl uses a bit of output buffering while it loads a file. This means that you can actually include small amounts of PHP in your files if you really really need to (defeats the object but you can do it). myTpl reads the parsed PHP only... it will not display PHP code to a browser.
You can also view the source and example here: http://w3style.co.uk/~d11wtq/mytpl/source.php