Making Your Own Template Tags and Functions in Cake
To make a long story short, I’m currently working on a project where the markup in view files needs to be simplified. I thought of using Smarty or HAML, but eventually opted for building my own template engine due to the unique circumstances of this project. The syntax has to be simple and map to actions and helper functions in the Cake app. To demonstrate, say I want to insert an image in a template view file. Normally, you do this with:
<?=$html->image('filename.gif');?>
But, in the case of my project, I’m going to alter the syntax to this:
{image('filename.gif')}
If this were only one instance, I might just run a filter during the normal rendering process. But since I know that what I’m doing will eventually affect every template view, I decided to override the default View::_render() method. You may want to do something similar or employ an outside template engine to Cake. This is especially handy for content management system projects, since they often require numerous custom methods in the views themselves. If you’re crazy enough to want to do this ;-) then let me explain how I’ve been able to pull it off.
Write the custom View object
First, you need to create a new view class to extend the current one. Create the app/views/wnr.php file. (I’m calling this “WNR” because of my own custom project, but this can be whatever you want it to be.) This new view object will pull all of its view files from a folder stored as app/views/templates, so I’ll need to specify this view path in the constructor function.
<?
class WnrView extends View {
var $tmp = null;
function __construct(&$controller) {
parent::__construct($controller);
$this->ext = '.wnr';
$this->layoutPath = 'templates';
$this->viewPath = 'templates';
$this->autoRender = false;
$this->tmp = TMP.'wnr'.DS;
}
The tmp property is a placeholder for the path to the temp file that will store the view. Cake’s method for rendering views is to pull together all of the surrounding layouts, helpers, and controller data then run the include function of the view file. Since I’m overriding this method with my own rendering functions, I’ll need to include a file of some kind, but one after I’ve parsed the view file. This leaves me with making a separate file, one that is parsed like a standard Cake view file, and this is what will eventually by the tmp property.
Run the _render method
Since the WnrView class extends the View object, I can run any of View’s functions in WnrView and they will automatically override the normal view rendering processes. In the cake/libs/view/view.php file, you’ll find the _render() function. I’ve copied and pasted this function into WnrView and will change it to work with my own parsing methods.
1 function _render($___viewFn, $___dataForView, $loadHelpers = true, $cached = false) {
2 $loadedHelpers = array();
3
4 if ($this->helpers != false && $loadHelpers === true) {
5 $loadedHelpers = $this->_loadHelpers($loadedHelpers, $this->helpers);
6
7 foreach (array_keys($loadedHelpers) as $helper) {
8 $camelBackedHelper = Inflector::variable($helper);
9 ${$camelBackedHelper} =& $loadedHelpers[$helper];
10 $this->loaded[$camelBackedHelper] =& ${$camelBackedHelper};
11 }
12
13 foreach ($loadedHelpers as $helper) {
14 if (is_object($helper)) {
15 if (is_subclass_of($helper, 'Helper') || is_subclass_of($helper, 'helper')) {
16 $helper->beforeRender();
17 }
18 }
19 }
20 }
21
22 $___compiledFile = $this->_parse($___viewFn);
23 $___data = Set::merge($___dataForView,$this->viewVars);
24 extract($___data, EXTR_SKIP);
25
26 ob_start();
27
28 if (Configure::read() > 0) {
29 include ($___compiledFile);
30 } else {
31 @include ($___compiledFile);
32 }
33
34 unlink($___compiledFile); //delete tmp file
35
36 if (!empty($loadedHelpers)) {
37 foreach ($loadedHelpers as $helper) {
38 if (is_object($helper)) {
39 if (is_subclass_of($helper, 'Helper') || is_subclass_of($helper, 'helper')) {
40 $helper->afterRender();
41 }
42 }
43 }
44 }
45 $out = ob_get_clean();
46 return $out;
47 }
The key to this function is where I’ve inserted my own methods. Almost everything is the same as View::_render() except that I’ve created my own file, $___compiledFile on line 22 and on line 23, I’ve pulled the view variables into $___data. That way, when line 24 performs extract(), any new variables that my parsing engine wants to make available to the view will get pulled in.
Line 34 deletes the temp file. The only thing left is to build the _parse() function called on line 22.
Parsing the view
Now I can parse the template file with my own specs.
1 function _parse($tpl) {
2 App::import('Core','File');
3 $view =& new File($tpl); //the view file to parse
4 $data = $view->read(); //contents of the view file
5 $_seed = md5(srand(time()*rand()));
6 $_tmp =& new File($this->tmp.$_seed); //random tmp file for compiling
7
8 /** THE ACTUAL PARSER; PUT YOUR OWN REGEX RULES HERE **/
9 preg_match_all('/\{([a-zA-Z0-9]+)\((.*?)\)\}/is',$data,$matches);
10 for($i=0; $i<count($matches[0]); $i++) {
11 $tag = $matches[0][$i];
12 $str = '<?=$html->'.$matches[1][$i].'('.$matches[2][$i].');?>';
13 $data = str_replace($tag,$str,$data);
14 }
15
16 $_tmp->write($data);
17 $_tmp->close();
18 $view->close();
19 return $this->tmp.$_seed;
20 }
Notice that this function creates the tmp file, reads the template view file, then on lines 9-14, it parses content that has {function(params)} and passes it to the HTML helper. Of course, you could multiple rules where I’ve done line 9-14, and even add data as view variables with $this->set(). I’ve actually done a lot more work mapping functions to the AppController object and from there working with models and controllers, then returning data from all over the application. This allows me to invert the typical MVC process and allow the template file to tell Cake what should be rendered. But I suppose that’s for another day.
If you wanted to use an outside vendor or parsing engine, like Smarty, then you could pass the view file contents through the vendor instead of through the _parse() function, like I’ve done here.
Using the new WnrView class
Now that it’s all in place, I only have to create the app/views/templates directory and store my own template files there. Then, I can override the normal view process by calling
$this->view = 'wnr';
in the controller actions where I want to run this templating engine.

