Extending your Doctrine Model: Template Classes
Symfony | Technical | Development | Doctrine | January 20, 2011
A lot of times in the past, working with Symfony 1.X and Doctrine 1.X, I got into a problem. According with Model View Controller design pattern, you have to carefully choose what to put in each layer. For example if it is some business logic you have to put that in the Model, if it is some HTML as is in the web case you have to put that in the View and finally if it is some code to manage the view and model, you have to put that in the Controller.
It is easy to read that and say: "Hey I'm going to follow this cool pattern". However, when programming, against the clock it is not so simple. Maybe you put some business logic in the controller or perhaps you put some view related code into the model. The last case used to happen to me a lot of times and I'll show you why.
The Problem
Let's say you have this schema:
Employee:
tableName: t_employee
actAs:
Sluggable: { fields: [ name ] }
columns:
id: { type: integer , length: 20 , primary: true, autoincrement: true }
name: { type: string , length: 50 , notnull: true }
web: { type: string , length: 255 }
photograph: { type: string , length: 255 }
Here we got an Employee entity with three attributes: name, web and photograph.
In your templates you are going to use surely these two statements:
link_to($employee->getName(), $employee->getWeb(), array('title' => $employee->getName()))
This shows the name of the employee as a link to his web page.
link_to(image_tag($employee->getPhotograph()), $employee->getWeb(), array('title' => $employee->getName()))
This shows the photograph of the employee as a link to his web page.
Because you are going to use this two statements a lot let's put them in a place where its reuse is guarantee:
class Employee extends BaseEmployee { public function getLinkToWeb() { return link_to($employee->getName(), $employee->getWeb(), array('title' => $employee->getName())); } public function getLinkToWebWithPhotograph() { return link_to(image_tag($employee->getPhotograph()), $employee->getWeb(), array('title' => $employee->getName())); } }
Now you can reuse this code for all your templates ... Wrong!!!!!
We were talking about separating of concerns with MVC pattern and you mix view code with the model. The idea behind the model is to have just the logic related to the data and no more. Imagine you are going to use your model with a different markup than HTML, then these functions will make no sense at all.
Now you know the problem I had.
The Solution: Template classes
It would be great if all these methods could continue in the model classes, but remember: that's wrong. Then let's create an extension of our model to work just with our HTML view.
The How
1.- Create in your project/lib/model the template directory.
2.- Add to the previous directory the EmployeeTemplate.class.php file:
class EmployeeTemplate extends sfDoctrineTemplateExt { public function getLinkToWeb() { return link_to($employee->getName(), $employee->getWeb(), array('title' => $employee->getName())); } public function getLinkToWebWithPhotograph() { return link_to(image_tag($employee->getPhotograph()), $employee->getWeb(), array('title' => $employee->getName())); } }
3.- Now, to properly connect your model with the template classes you need: sfDoctrineTemplateExt class
/** * sfDoctrineTemplateExt * * @package symfext * @subpackage doctrine * @author Jonathan Olger Nieto Lajo [johan.nieto.lajo@gmail.com] */ abstract class sfDoctrineTemplateExt { protected $object = null; public function __construct(DoctrineRecord $object) { $this->object = $object; } public function __call($method, $arguments) { return call_user_func_array(array($this->object, $method), $arguments); } }
4.- Update your sfDoctrineRecordExt file. As you may know Doctrine gives the advantage of changing the inheritance hierarchy of your model in one level. That is, you can put a class (or many) in the middle of the inheritance hierarchy. In my case, the whole hierarchy would be:
Doctrine_Record_Abstract > Doctrine_Record > sfDoctrineRecord > sfDoctrineRecordExt > BaseEmployee > Employee
/** * sfDoctrineRecordExt * * @package symfext * @subpackage doctrine * @author Jonathan Olger Nieto Lajo [johan.nieto.lajo@gmail.com] */ abstract class sfDoctrineRecordExt extends sfDoctrineRecord { /** * Returns the corresponding template instance of this model. * * @param boolean $force Whether or not the method should force the conversion * * @throws RuntimeException If force and the corresponding template class does not exist * * @return DoctrineRecord|DoctrineTemplate The result of the conversion */ public function toTemplate($force = true) { $template_class = $this->getTable()->getComponentName().'Template'; if (class_exists($template_class)) { $template = new $template_class($this); return $template; } if ($force) { throw new RuntimeException(sprintf('The "%s" class does not exist. You can\'t convert a sfDoctrineRecordExt to a template without the template class', $template_class)); } return $this; } }
5.- Try your brand new functionality in the showEmployeeSuccess.php file (it is assumed an employee object is passed from the action to the view):
?php echo $employee->toTemplate()->getLinkToWeb() ?php>
Or
?php echo $employee->toTemplate()->getLinkToWebWithPhotograph() ?php>
Finally, the model is free from view related code and there is the possibility to reuse methods and functionality on an entity basis.
To solve the above problem in Yii I would probably create a custom helper class.
"Helper classes: in views we often need some code snippets to do tiny tasks such as formatting data or generating HTML tags. Rather than placing this code directly into the view files, a better approach is to place all of these code snippets in a view helper class. Then, just use the helper class in your view files. Yii provides an example of this approach. Yii has a powerful CHtml helper class that can produce commonly used HTML code. Helper classes may be put in an autoloadable directory so that they can be used without explicit class inclusion."
- http://www.yiiframework.com/doc/guide/1.1/en/basics.best-practices#view