Smarty and Doctrine combination
I’ve had problems with both Zend Session and the Zend Controller. It just feels like too much fighting and since the plan is to replace Zend DB with Doctrine there is only the controller and session left and unwanted to boot.
The session logic can easily be replicated in a more lightweight form, so can the routing and that is what this tutorial is about. We are creating an MVC setup where M is Doctrine, V is Smarty and C is our own stuff we do here. The Zend Framework has been reduced to just another component library for me now, I will pick goodies when I need them.
Why Smarty? Because I have to use it, and believe it or not I’ve come to like Smarty, to the point where I think the normal way of doing things looks messy. Call me uncool, call me crazy, heck you can even call me a bad PHP programmer, I’ll still like Smarty.
The goal here is to replicate the functionality demonstrated in the Merb registration tutorial. We don’t want the framework to stifle us, but at the same time we want to feel empowered.
Some of the stuff we do here will be a rehash of what has been done earlier in the Smarty/Zend series linked to above, if something is unclear please check it out.
Let’s start with the bootstrap index.php:
set_include_path('.' . PATH_SEPARATOR . './models/' . PATH_SEPARATOR . '../lib/' . PATH_SEPARATOR . get_include_path());
require_once('classes/Common.php');
require_once('SmartyDoctrine.php');
require_once('doctrine/lib/Doctrine.php');
spl_autoload_register(array('Doctrine','autoload'));
$GLOBALS['doctrine_db'] = Doctrine_Manager::connection('mysql://root:@localhost/doctrine_test');
$GLOBALS['doctrine_manager'] = Doctrine_Manager::getInstance();
$GLOBALS['doctrine_manager']->setAttribute(Doctrine::ATTR_VALIDATE, Doctrine::VALIDATE_ALL);
require_once('classes/Ctrl.php');
require_once('classes/Mdl.php');
require_once('helpers.php');
$GLOBALS['doctrine_manager']->setAttribute('model_loading', 'conservative');
Doctrine::loadModels('models');
$_SESSION['lang'] = empty($_SESSION['lang']) ? 'en' : $_SESSION['lang'];
Common::index();
SmartyDoctrine sounds cool but it’s just a normal Smarty extension setup. Common is our only “namespace” (class with static methods). We turn on validation. with the setAttribute() call. Ctrl is our base controller class that is replacing Zend Controller. Mdl is an extension class we extend Doctrine Record with, it doesn’t do anything yet. Could be handy in the future though. The helpers.php file contains Smarty insert functions. We set the language to be used to en. Finally we call the index() function in Common to load our templates:
static function loadCtrl($ctrl){
$name = ucfirst($ctrl)."Ctrl";
if(!class_exists($name)){
$file_name = "controllers/$name.php";
if(!is_file($file_name)){
echo "The controller does not exist";
exit;
}
include_once($file_name);
}
$obj = new $name();
$obj->init(ucfirst($ctrl));
return $obj;
}
static function index(){
$req = array_slice(explode('/', $_SERVER['REQUEST_URI']), 1);
$GLOBALS['base_dir'] = $req[0];
if(empty($req[1]) || empty($req[2])){ echo "No ctrl or method"; exit; }
$args = self::array_trim(array_slice($req, 3));
if(count($args) % 2 != 0){ echo "key/missing value"; exit; }
$smarty = new SmartyDoctrine();
$obj = self::loadCtrl($req[1]);
$method = $req[2];
$smarty->assign('base_dir', $req[0]);
for($i = 0; $i < count($args) - 1; $i += 2)
$obj->g[ $args[$i] ] = $args[$i + 1];
$smarty->assign('content', $obj->$method());
$smarty->display('index.tpl');
}
The above is combined with this .htaccess file:
RewriteEngine on
RewriteRule !\.(js|ico|gif|jpg|png|css|html|htm|xml|php|swf)$ index.php
The request URI will look like this later in this tutorial: http://localhost/doctrine_test/member/form/id/1. All URIs looking like this will therefore be rerouted to our index.php file when mod_rewrite is turned on. We begin with splitting it by ‘/’ and saving doctrine_test as the global ‘base_dir’. The next value in this case is member which is of course our requested controller in the form of MemberCtrl.
We loop through the rest and create an associative array with them in the controller, in this case it will be array(‘id’ => 1). Finally we call the form method and save the result in ‘content’ which will be drawn in the middle of the page. Other controllers can be inserted with the help of Smarty insert, the function is located in helpers.php and should be familiar if you’ve followed the old Smarty/ZF CMS/Community tutorial:
function insert_item($p){
$method = empty($p['method']) ? 'render' : $p['method'];
return Common::loadCtrl($p['ctrl'])->$method($p);
}
A call could look like this: {insert name=item ctrl=menu method=coolMethod}.
In our case we will call the member controller and the form method in the Common::index() call. At the moment there is not much in MemberCtrl.php, almost everything is located in Ctrl.php which MemberCtrl extends:
function init($name){
$this->name = $name;
$this->base_dir = "/{$GLOBALS['base_dir']}/{$this->name}/";
$this->lang = $_SESSION['lang'];
$this->smarty = new SmartyDoctrine();
$this->q = Doctrine_Query::create();
$this->t = $this->getT($this->name);
$this->p = $_POST;
$this->template = $this->name.".tpl";
$this->config_file = $this->lang.$this->name.".conf";
$this->assign('config_file', $this->config_file);
$this->regObj("tObj", $this->t);
$this->regObj("obj", $this);
}
function find($id = NULL){
if(empty($id))
return empty($this->g['id']) ? $this->t->find($this->p['id']) : $this->t->find($this->g['id']);
else
return $this->t->find($id);
}
function form(){
return empty($this->g['id']) ? $this->iForm() : $this->uForm();
}
function iForm(){ return $this->formCommon('save', 'insert'); }
function uForm(){
$this->setS('pop', $this->hasPop() ? $this->getS('pop') : $this->find()->toArray());
return $this->formCommon('save/id/'.$this->g['id'], 'update');
}
Simple logic to decide what to do, if an argument with the key ‘id’ is in the g (from $_GET) array it must mean we want to update the entry with that id. If not we should insert a new item. Note that we have to prepopulate the form in case of update on first load, $this->find()->toArray() will take care of that via the Doctrine table object.
function formCommon($f_head, $type, $tpl = "ui_form.tpl"){
$this->assign('form_head', $this->getFormHead($f_head));
$this->assign('type', $type);
return $this->fetch($tpl);
}
The getFormHead function is responsible for creating a proper form attribute list which we will use in the ui_form template. The type assignments can be used in the template to make it look different. It’s often the case that the number of fields are different when updating and inserting.
ui_form.tpl:
{config_load file=$config_file}
<form {$form_head} >
<table>
<tr>
<td>{#username#}: </td>
<td>{obj->textBox idname=username}</td>
</tr>
<tr>
<td>{#password#}: </td>
<td>{obj->textBox pwd=true idname=password}</td>
</tr>
<tr>
<td>{#password2#}: </td>
<td>{obj->textBox pwd=true idname=password2}</td>
</tr>
<tr>
<td>{#email#}: </td>
<td>{obj->textBox idname=email}</td>
</tr>
<tr>
<td>{#city#}: </td>
<td>{obj->selBox lbl=name mdl=City idname=city}</td>
</tr>
{if $type eq "insert"}
<tr>
<td>{#termscond#}: </td>
<td>{obj->checkBox idname=tcond}</td>
</tr>
{/if}
</table>
<input type="submit" value="{#submit#}">
</form>
The obj object is the current controller we registered in the init function above ($this->regObj(“obj”, $this);). Let’s take a look at textBox(), the others follow a similar pattern so we leave them out. Back to Ctrl.php then:
function textBox($p, &$s){
$args = array('type' => empty($p['pwd']) ? 'text' : 'password', 'idname' => $p['idname']);
return $this->commonBox($args, 'text_box.tpl', $p['popfrom']);
}
function commonBox($p, $tpl, $pop = false){
$s = new SmartyDoctrine();
$s->assign('error', $this->error[$p['idname']]);
$s->assign('prepop', !$pop ? $this->getPop($p['idname']) : $this->getPop($pop));
$s->assign($p);
return $s->fetch($tpl, false);
}
Currently we only display one error at a time, however the above logic should work in a multi error scenario. To get multi-errors to work you have to change some stuff we will get to in a minute if you want to display all of them at the top of the form or something. Do we populate explicitly? If not (the usual case) we fetch the value from a session variable I realize now that I’m writing this. Seems unnecessary, might as well be stored in a member variable since persistence is not an issue, todo…
text_box.tpl:
<div class="error">{$error}</div>
<input type="{$type}" id="{$idname}" name="{$idname}" value="{$prepop}" />
Upon hitting save in our update scenario we will call: http://localhost/doctrine_test/member/save/id/1.
Ctrl::save():
function save(){
$custom_validation = $this->validation();
if($custom_validation !== true){
list($key, $error) = $custom_validation;
return $this->setErrQuit($key, $error);
}
$obj = empty($this->g['id']) ? $this->myMdl() : $this->find();
try{
$obj->fromArray($this->p);
$obj->save();
return $this->onSave();
}catch(Doctrine_Validator_Exception $e){
foreach($obj->getErrorStack()->toArray() as $key => $value)
return $this->setErrQuit($key, $value[0]);
}
}
function setErrQuit($key, $error){
$this->setS('pop', $this->p);
$this->error = array($key => $this->getConfig('errors', $error));
return $this->form();
}
First we call custom validation with $this->validation(), that is currently the only function that MemberCtrl overrides. In that function we check if the passwords match and if the terms and conditions checkbox has been checked (if we are dealing with an insert form, otherwise not). Next it’s time to create the Doctrine_Record object. If we are updating we need to find it, otherwise we create a new one.
Finally we try to save, if any Doctrine validator fails here we will run the catch block instead of calling onSave(). In that case we get the first key and value from the array, this is the place you need to change if you want multi-error handling. The error stack might look like this for instance:
Array ( [username] => Array ( [0] => unique ) )
Hence $value[0] above.
function onSave(){
if(empty($this->g['id'])) return $this->onInsert();
else return $this->onUpdate();
}
function onInsert(){ return "The object has been saved."; }
function onUpdate(){ return "The object has been updated."; }
This is the same check we perform in the form function, as you can see onInsert() and onUpdate() are meant to be overridden in the extension controler, in our case MemberCtrl but we haven’t done that yet.
Below is a source download, be sure to create a database called doctrine_test and run the populate_and_test.php script described in the prior tutorial before you try this stuff out.
Final Words
If you ask me this is pretty darn close to the Merb with Datamapper example that I liked so much. Seems like the PHP world is slowly catching up with the Ruby world and I have to say that Doctrine is probably one of the main engines driving this development.
The above enables create and update functionality with validations out of the box with hardly any coding needing to be added. Only the validators in the Doctrine models, some HTML and the translation files are needed for each table. I can do that in no time, it’s trivial stuff, it doesn’t have to get any better or cleverer than this, I’m fine with this, it works. Zend DB et al didn’t quite work, I always found myself thinking; is this as good as PHP gets, is this the end of the line?
Sure, the above Doctrine to Smarty glue is certainly not the end of the line but my own itch has at least stopped. I’m home now, at peace with PHP. It’s not the most beautiful of programming languages but I think projects like Doctrine shows that it doesn’t have to be unbearable as many people seem to think. Far from it.
Related Posts
Tags: Doctrine, orm, PHP, Smarty, templating, validating