David Golding



Making Your Own Template Tags and Functions in Cake

By David Golding | Print This Post Print This Post

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:

1
<?=$html->image('filename.gif');?>

But, in the case of my project, I’m going to alter the syntax to this:

1
{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

1
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

1
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

1
app/views/templates

, so I’ll need to specify this view path in the constructor function.

1
2
3
4
5
6
7
8
9
10
11
12
<?
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

1
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

1
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

1
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

1
cake/libs/view/view.php

file, you’ll find the

1
_render()

function. I’ve copied and pasted this function into WnrView and will change it to work with my own parsing methods.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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

1
View::_render()

except that I’ve created my own file,

1
$___compiledFile

on line 22 and on line 23, I’ve pulled the view variables into

1
$___data

. That way, when line 24 performs

1
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

1
_parse()

function called on line 22.

Parsing the view

Now I can parse the template file with my own specs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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

1
tmp

file, reads the template view file, then on lines 9-14, it parses content that has

1
{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

1
$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

1
_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

1
app/views/templates

directory and store my own template files there. Then, I can override the normal view process by calling

1
$this->view = 'wnr';

in the controller actions where I want to run this templating engine.


Using Cake to Build Site Maps

By David Golding | Print This Post Print This Post

Making site map lists of your server directories can be a chore, especially if your web site has lots of nested folders. Thanks to Cake’s File and Folder utility classes, this can take a lot of the headache out of building dynamic site maps or creating site map files for Google and other search engines.

In the Controller

I’ve created a Files controller in which I’ll do the site mapping:

1
2
3
4
5
6
<?
class FilesController extends AppController {
    $name = 'Files';
    $uses = null;
}
?>

Next, create an action to run the site mapping loop. I’ve called mine

1
index()

. Then instantiate the folder which you’d like to map.

1
2
3
4
5
6
1    function index() {
2        App::import('Core','Folder');
3        $path = ROOT.'scan_this/';
4        $folder = new Folder($path);
5        $this->set('contents',$folder->read());
6    }

On line 2, you can see that I’ve run

1
App::import()

to bring the Folder utility class into the controller. Then on line 5, I’ve set the view variable

1
$contents

to the results of the Folder utility’s

1
read()

function. With

1
Folder::read()

, you essentially get an array of folders and files, like the

1
ls

Unix command.

In the View

Now in the

1
app/views/files/index.ctp

file, I can see the folder’s contents by displaying the contents of the

1
$contents

array:

1
<? debug($contents);?>
1
2
3
4
5
6
7
8
9
10
11
12
13
//contents of $contents
Array (
    [0] => Array (
        [0] => folder_1
        [1] => folder_2
        [2] => images
    )
    [1] => Array (
        [0] => a file.html
        [1] => another_file.html
        [2] => index.html
    )
)

Now, just cycle through this array, adding your own base URL, and your site map list is ready for the search engines. Of course, you may want to do this with XML if the web site is much more dynamic. For a tutorial on how to use Cake to dynamically create other file formats, check out my book, Beginning CakePHP: From Novice to Professional, Chapter 10: “Routes.” (The section, “Parsing Files with Extensions Other Than .php” explains how to use the RequestHandler component to have Cake map other extensions to Cake controllers and actions.)


Scaffolding Touch Ups

By David Golding | Print This Post Print This Post

I can’t rant and rave enough about CakePHP. When I come across rivalries like the classic PC-vs.-Mac or Adobe InDesign-vs.-Quark XPress, I always try to consider what each side offers. (Incidentally, I’m a Mac user who sticks with InDesign.) When it comes to Cake-vs.-Symfony or Cake-vs.-Zend Framework, or even Cake-vs.-CodeIgniter, I’m convinced Cake is the winner on almost all counts.

But — I must concede one point. Maybe it’s because I try to have an eye for design, or maybe it’s just noticing a better flavor when you taste it, but I think Cake’s scaffolding design could use some work. In this area, I think Ruby On Rails definitely comes out on top.

I noticed that on one project, I had adjusted the scaffolding views a lot because I knew that those peeking over my shoulder were Rails fans. By no means were my designs much better, but on the whole, they were at least more pleasing to me, and, ultimately, what makes you happy when coding an extensive application is worth it. Kinda like the music truckers listen to; it doesn’t make the shipment get there any faster, but it sure makes the journey more enjoyable.





It dawned on me that perhaps I should let other bakers try out my scaffolding touch-ups. So, here’s how to add these touch-ups to your Cake app.

  1. Download Scaffolding Touchups [zip]
  2. Unpack the .zip file
  3. Place the “cake.generic.css” file in the
    1
    app/webroot/css

    folder

  4. Place the “home.ctp” file in the
    1
    app/views/pages

    folder

  5. Place the “default.ctp” file in the
    1
    app/views/layouts

    folder

Now, don’t get me wrong. I think Cake’s scaffolding design is just fine. This is just my little monochromatic flavor is all. Hope you enjoy.


« Older Entries | Newer Entries »

Beginning CakePHP: From Novice to Professional by David Golding

Other Blogs

David Golding

A blog about CakePHP, web design, and grad studies in religion. © 2008, D. Golding