Fat Models are Good!
Submitted by Matthew Turland on Wed, 01/07/2009 - 12:27If there is one thing I admire about Matthew Weier O'Phinney, it's the amount of passion he puts into his work. He perceived concerns about best practices for developing models when using Zend Framework and took the time to write up three blog posts that detail his own experiences and recommendations. When I began actively using ZF on a daily basis in early 2008, I had questions similar to those that the aforementioned blog posts are intended to address. As such, I thought take a blog post to share some of what I'd learned.
A relatively simple example that showcases some of what Matthew discusses is, appropriately, a model for a blog. Since a relational database is the most common form of storage these days, let's start with a MySQL table to contain blog posts. In the interest of expediency, I've borrowed the table schema from the Habari blogging software.
CREATE TABLE posts (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
slug VARCHAR(255) NOT NULL,
content_type SMALLINT UNSIGNED NOT NULL,
title VARCHAR(255) NOT NULL,
guid VARCHAR(255) NOT NULL,
content LONGTEXT NOT NULL,
cached_content LONGTEXT NOT NULL,
user_id SMALLINT UNSIGNED NOT NULL,
status SMALLINT UNSIGNED NOT NULL,
pubdate INT UNSIGNED NOT NULL,
updated INT UNSIGNED NOT NULL,
modified INT UNSIGNED NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY slug (slug(80))
) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci;
To perform low-level data handling from this table, we'll use a class based on Zend_Db_Table. I won't show all the logic that would be needed here, but just enough to give you an idea of what might be required.
class PostTable extends Zend_Db_Table_Abstract
{
protected $_name = 'posts';
protected $_primary = 'id';
public function insert(array $data)
{
if ($data['status'] == 2) {
$data['pubdate'] = time();
}
return parent::insert($data);
}
public function update(array $data, $where)
{
$data['modified'] = time();
$select = $this->select()->where($where);
$current = $this->fetchRow($select)->toArray();
$diff = array_diff_assoc($data, $current);
if (isset($diff['status'])
&& $data['status'] == 2
&& empty($current['pubdate'])) {
$data['pubdate'] = time();
}
return parent::update($data, $where);
}
public function save(array $data)
{
if (!empty($data['id'])) {
$where = $this->getAdapter()->quoteInto('id = ?', $data['id']);
$this->update($data, $where);
return $data['id'];
}
return $this->insert($data);
}
}
The biggest misconception floating around is that this is the end of model development. There are two major reasons that this is incorrect.
First, Zend_Db_Table offers a lot of functionality out of the box. This is a good thing, but only if used properly. There may be many aspects of that functionality that you don't want to expose in your model. Leaving them available significantly raises the likelihood of a security hole being created when modifications are made to the application codebase over time.
Second, there may come a time when you want to migrate to a data source that isn't a relational database, and using Zend_Db_Table directly means that there are now model references interspersed in the rest of your application. In this instance, you'd either have to update all model references in your application to conform to a new API or implement the adapter design pattern so that the API for your new data source conforms to the existing Zend_Db_Table API. Neither is a particularly appealing option.
Yet most people stop here. This is why Bill Karwin emphasizes that creating the model is the job of the developer and why Pádraic Brady goes on to say that models are very misunderstood: it's assumed that the framework does or should do this for you, which isn't the case.
So to save time and frustration, we're going to head this potential eventuality off at the pass by wrapping our Zend_Db_Table class in another class where we decide what the API looks like. By doing this, we encapsulate the low-level logic involved in the data access capabilities we need. So long as the conceptual operations don't change, the underlying logic can be changed to implement caching or use a different data source entirely. Again, the model below doesn't include every bell and whistle you might want to include, but gives you a rough idea of what some of the model would look like.
class PostModel
{
protected $_table;
protected $_form;
public function __construct()
{
$this->_table = new PostTable();
}
public function getForm()
{
if (empty($this->_form)) {
require_once 'PostForm.php';
$this->_form = new PostForm();
}
return $this->_form;
}
public function getRecentList($number)
{
$select = $this->_table
->select()
->order('pubdate desc')
->limit($number);
return $this->_table->fetchAll($select);
}
public function getPost($id)
{
return $this->find($id);
}
public function deletePost($id)
{
$where = $this->_table->getAdapter()->quoteInto('id = ?', $id);
return $this->_table->delete($where);
}
public function savePost(array $data)
{
$form = $this->getForm();
if (!$form->isValid($data)) {
return false;
}
$data = $form->getValues();
return $this->_table->save($data);
}
}
Note how a form is used for data filtering and validation prior to saving a post. Since the introduction of invalid data is something you don't want to introduce unintentionally, it makes sense to put this here to ensure that all data that goes into the model ends up being validated before it reaches the underlying data source.
Below is an example what a form might look like. This can be used to display form elements corresponding to the database table fields or validate and obtain filtered user input. In either case, only the required components will be loaded, so any overhead involved is negligible.
class PostForm extends Zend_Form
{
public function init()
{
$this->addElements(array(
'id' => array(
'type' => 'hidden',
'validators' => array('Int')
),
'slug' => array(
'type' => 'text',
'validators' => array(
array('StringLength', false, array(1, 255))
)
),
'content' => array(
'type' => 'textarea'
),
// ...
));
}
}
At this point you might be thinking, "OK, that's all well and good, but what does using this model look like? See the example controller below. It probably looks a bit small - and that's the idea. Thin controllers, fat models. Side note: what's shown in the init() method below is likely logic that you'd want to place into an action helper for reuse among several controllers, but I've included it in init() in this example for the sake of keeping it short and simple.
class PostController extends Zend_Controller_Action
{
protected $_model;
public function init()
{
$this->_model = new PostModel();
$auth = Zend_Auth::getInstance();
if (!$auth->hasIdentity()) {
switch ($auth->authenticate()->getCode()) {
case Zend_Auth_Result::SUCCESS:
// get this user's role from the authentication data source
$this->_model->setIdentity($role);
default:
// display an appropriate error message
}
}
}
public function indexAction()
{
$this->view->posts = $this->_model->getRecentList();
}
public function viewAction()
{
if (!$id = $this->_getParam('id')) {
// display an appropriate error message
}
$this->view->assign($this->_model->getPost($id));
}
public function addAction()
{
$this->view->form = $this->_model->getForm();
}
public function editAction()
{
if (!$id = $this->_getParam('id')) {
// display an appropriate error message
}
$form = $this->_model->getForm();
$form->populate($this->_model->getPost($id));
$this->view->form = $form;
}
public function saveAction()
{
$this->_model->savePost($this->_getAllParams());
$this->_helper->redirector->setGotoSimple('index');
}
public function deleteAction()
{
if (!$id = $this->_getParam('id')) {
// display an appropriate error message
}
$this->_model->deletePost($id);
$this->_helper->redirector->setGotoSimple('index');
}
}
With ZF in particular, you may notice that I'm returning and using Zend_Db_Table_Row objects directly rather than trying to typecast them. If you don't want to use (or expose) the functionality offered by these objects, calling toArray() on them prior to returning them (and casting that to stdClass if you prefer objects) is a way to accomplish this. I simply didn't do so in this examples to keep them simple and to make the point that, so long as the objects are only ever used as generic objects would be, models could be changed to return generic objects and that change would be transparent to existing code using the models.
There's also the matter of coupling between the model and the form. Matthew's approach couples the form to the model, while Jani Hartikainen suggests an approach that couples the model to the form. It is also possible to remove this coupling by establishing a service layer that composes the form and model and only accessing either via that service layer. Some people prefer to resort to coupling rather than adding another layer of complexity to the application. Additionally, changes to the model or form that won't affect the other are fairly few, so in that respect the coupling is not especially harmful. In the end, it comes down to a matter of preference.
Hopefully this post has provided a little insight into the logic behind this approach to models. I welcome further questions, suggestions, and discussion on the topic in the comments of this post.


Also read...
At the risk of over self-promotion, I recommend that everyone also peruses http://akrabat.com/2008/12/13/on-models-in-a-zend-framework-application/ which follows the same line of thinking. Regards, Rob...