A few weeks ago I was discussing a project with Charlie Madison of ProWebScape. He mentioned H2O, that he would rather use that than Smarty. So I checked it out and it seemed like the natural evolution in PHP templating.

On a side note, I’ve worked with a big legacy system now for roughly 3 months, it’s using simple PHP for templating, it’s not very pretty. So more than ever I can speak based on my own experience, templating with PHP is not very ideal, especially when you compare to something like H2O. Which is kind of ironic.

Anyway, Charlie wanted to use PHP Doctrine for the project so I went to work, a work long overdue, replacing Smarty in Smoc with something else.

Let’s review what had to be done. To begin with h2o is an object oriented system, templates inherit each other, you don’t reuse template snippets like I had done in Smoc where templates are inserted into each other repeatedly. One index template could have for instance a menu call and a main content area. What would appear in the main area was controlled by the request and rendered there. The template being rendered in the main area could then in turn call upon other smaller templates to be inserted and so on, kind of like a functional approach. All that had to go, in h2o you instead specify the parent template in each sub-template.





Apart from those big structural differences h2o will not allow for as much logic to take place in the template code as Smarty (AFAIK anyway). So I moved some logic out of the templates into the PHP code. Performance wise we’re worse off but as far as pretty template code goes we’re way better off. I’m really happy how it turned out, and I’m much happier with h2o than I was with Smarty.

All the Smarty stuff in Ctrl.php is gone of course and replaced with this:

function assign($key, $value){ $this->h2o_vars[$key] = $value; } function fetch($tpl, $arr = array()){ $h2o = $this->getH2o($tpl); if(empty($arr)) return $h2o->render($this->h2o_vars); else return $h2o->render($arr); } function partial($tpl, $arr = array()){ $h2o = $this->getH2o($tpl, ''); if(empty($arr)) return $h2o->render($this->h2o_vars); else return $h2o->render($arr); } function render($tpl){ $h2o = $this->getH2o($tpl); echo $h2o->render($this->h2o_vars); } function getH2o($tpl, $dir = false){ if($dir === false) $dir = $this->name.'/'; else if($dir != '') $dir = $dir.'/'; return new H2o($dir."$tpl.html", array('searchpath' => "templates")); }

No big difference there between fetch() and partial(). The latter will render templates that reside in the root of the templates folder, the former will use the path to the current view to render custom stuff.

As you can see we’ve wrapped the assignment of variables to h2o in a similar fashion it worked in Smarty which was neat.

So the index template could for instance look like this:

<html> <head> <title>Foxy Admin</title> <link rel="stylesheet" href="{{path}}css/styles.css" type="text/css" /> <script type="text/javascript" src="{{path}}js/jquery.js"></script> </head> <body> <table style="width: 100%"> <tr> <td valign="top" style="width: 200px"> {% block menu %} {% if ctrl.isLoggedIn %} <a href="{{site_dir}}user/add">Add User</a><br> <a href="{{site_dir}}user/listAll">List Users</a><br><br> {% endif %} {% endblock %} </td> <td valign="top"> {% block content %} {% endblock %} </td> </tr> </table> <br /> <br /> <a href="{{site_dir}}user/welcome">Back</a> </body> </html>

Two things to note here:

1.) Note the ctrl.isLoggedIn call, that’s the way you call methods of assigned objects. Note also that you can’t pass any arguments in the call (at the time of writing anyway).

2.) The blocks, menu block and content block. These are the areas which each sub-template will overwrite with its own logic, if we want it to.

A sub can look like this:

{% extends "index.html" %} {% block content %} <form {{ form_head }} > <table> {% for name, html in ui_fields %} {% if name != "id" %} <tr> <td>{{ name }}: </td> <td>{{ html }} </td> </tr> {% endif %} {% endfor %} </table> <input type="submit" value="Submit"> </form> {% endblock %}

Note how this will be inserted into the content block in index.html in an automagic fashion.

The above is actually ui.html (formerly ui.tpl) if you check the old Smoc code you will see the big difference in template complexity. However, we can’t get away from it so it now resides in AdminCtrl instead:

function getUiField($key, $info){ if(!empty($this->relations[$key])){ $info_arr = array( 'lbl' => $this->relations[$key]['label'], 'mdl' => $this->relations[$key]['Mdl'], 'idname' => $key ); if($this->relations[$key]['foreign'] == true) return $this->checkBoxes($info_arr); else return $this->selBox($info_arr); }else if($info['type'] == 'boolean') return $this->radioBox(array('idname' => $key)); else if(in_array($info['type'], array('integer', 'float', 'double', 'decimal'))) return $this->textBox(array('idname' => $key)); else if($info['length'] > 500) return $this->textArea(array('idname' => $key)); else if($key == 'password') return $this->textBox(array('idname' => $key, 'pwd' => true)); else return $this->textBox(array('idname' => $key)); }

This is the new place where we map basic information in the model to a correct field. In Smoc we called these methods directly in the template but now we call them here instead. Let’s go through textBox, in Ctrl.php:

function textBox($p){ $p['type'] = empty($p['pwd']) ? 'text' : 'password'; return $this->commonBox($p, 'text_box', $p['popfrom']); } function commonBox($p, $tpl, $pop = false){ $p = array_merge($p, array('error' => $this->error[$p['idname']])); $p = array_merge($p, array('prepop' => !$pop ? $this->getPop($p['idname']) : $this->getPop($pop))); return $this->partial($tpl, $p); }

Here we can infer that we are calling a template called text_box.html in the root of the templates folder:

<div class="error">{{ error }}</div> <input class="{{ class }}" type="{{ type }}" id="{{ idname }}" name="{{ idname }}" value="{{ prepop }}" />

Simple enough, the ui_fields array in ui.html will therefore contain the html of each field, that is the main difference compared to Smoc in how we render CRUD forms automatically.

To make all this work we need to first set the relations:

function setRelations($hide = true, $hide_with = array()){ $hide_with = empty($hide_with) ? $this->hidden : $hide_with; foreach($this->t->getRelations() as $key => $value){ if($value instanceof Doctrine_Relation_Association){ $relations[ $key ]['foreign'] = true; $this->columns += array($key => array()); }else if($value instanceof Doctrine_Relation_LocalKey) $key = $value->getLocal(); $relations[ $key ]['label'] = $this->getCtrl($value->getClass())->label; $relations[ $key ]['Mdl'] = $value->getClass(); } $this->relations = $relations; $columns = $hide ? array_diff_key($this->t->getColumns(), $hide_with) : $this->t->getColumns(); $this->columns = $columns + $this->columns; }

Only after this has been done can we call getUiFields():

function getUiFields(){ foreach($this->columns as $key => $info) $ui[$key] = $this->getUiField($key, $info); return $ui; }

Experimental code with sample project code: h2octrine





Related Posts