Creating Models in Zend Framework
Saturday, January 8, 2011 at 11:25PM by
Patrick Brown While working on various projects using the popular Model-View-Controller (MVC) Zend Framework, I had to rapidly build several models that had 1-to-1 mappings to underlying data sources (such as a database). While Zend Framework offers specialty Controllers and Views, its definition of Models is left very open. My approach is similar the approach that is now part of the Zend Framework Quickstart Guide. The guide details an example of using SQLite to create a guestbook. In this tutorial, I will show you how to abstract a generic model toolset so that creating new models takes only seconds. I will also be releasing a Creating Mappers in Zend Framework tutorial shortly (stay tuned).
We will be building a blog script for demonstration. Our blog script will require a model that we will use to interact with the blog post:
- Model_Blog_Post
- Model_Blog_Mapper
Before the juicy details of the mapper, we need to define our model. Models are mostly simple classes with a bunch of getters and setters for staging data into and out of model objects. They have a 1-to-1 relationship with the data they describe. Thus, each object instance of the Model_Blog_Post class contains exactly one real blog post. A simple blog post should contain a title, a body, an author, and a date. Thus, we define a basic model with these member variables and setters/getters to get/set their values:
class Model_Blog_Post extends App_Model_Model
{
protected $_mapper = "Model_Blog_Mapper";
protected $_postid;
protected $_title;
protected $_body;
protected $_author;
protected $_date;
public function getPostid()
{
return $this->_postid;
}
public function setPostid($postid)
{
$this->_postid = $postid;
return $this;
}
public function getTitle()
{
return $this->_title;
}
public function setTitle($title)
{
$this->_title = $title;
return $this;
}
public function getBody()
{
return $this->_body;
}
public function setBody($bodytext)
{
$this->_body = $bodytext;
return $this;
}
public function getAuthor()
{
return $this->_author;
}
public function setAuthor($author)
{
$this->_author = $author;
return $this;
}
public function getDate()
{
return $this->_date;
}
public function setDate($time)
{
$this->_date = $time;
return $this;
}
}
Above we see basic member variables defined with getter/setter methods for each. Each setter method also returns $this (the model object itself) in order to allow daisy-chaining such as:
$post->setTitle("Baseball Game")
->setBody("I went to a baseball game.")
->setAuthor("Peter Moylan")
->save();
If you read the Zend Framework article, you will notice that we are missing many functions such as a constructor and magic methods __get() and __set(). These functions are in fact very generic. They do not need to be redefined for every single model again and again. Instead, we will place these into a generic abstract model class App_Model_Model that we can then extend to create our post model as we did above:
/**
* Provides common methods that are universal to most models.
*
* @author Patrick Brown (patiek at ep-dev dot com)
*
*/
class App_Model_Model
{
/**
* @var $_mapper App_Model_Mapper
*/
protected $_mapper;
protected $_count=1;
/**
* Constructor
*
* @param array|null $options
* @return void
*/
public function __construct(array $options = null)
{
if (is_array($options))
$this->setOptions($options);
}
/**
* Overloading: allow property access
*
* @param string $name
* @param mixed $value
* @return void
*/
public function __set($name, $value)
{
$method = 'set' . $name;
if ('mapper' == $name || !method_exists($this, $method))
{
throw new Exception('Invalid property specified');
}
else
{
$this->$method($value);
}
return $this;
}
/**
* Overloading: allow property access
*
* @param string $name
* @return mixed
*/
public function __get($name)
{
$method = 'get' . $name;
if ('mapper' == $name || ! method_exists($this, $method))
{
throw new Exception('Invalid property specified');
}
else
{
return $this->$method();
}
}
/**
* Set object state
*
* @param array $options
* @return App_Model_Model
*/
public function setOptions(array $options)
{
$methods = get_class_methods($this);
foreach ($options as $key => $value)
{
$method = 'set' . ucfirst($key);
if (in_array($method, $methods))
{
$this->$method($value);
}
}
return $this;
}
/**
* Get total number returned in result set.
*/
public function getCount()
{
return $this->_count;
}
/**
* Set total number returned in result set.
*
* @param int $count
* @return App_Model_Model
*/
public function setCount($count)
{
$this->_count = (int)$count;
return $this;
}
/**
* Save this model to permanent space.
*
* @return App_Model_Model
*/
public function save()
{
$this->getMapper()->save($this);
return $this;
}
/**
* Delete this model from permanent space.
*
* @return App_Model_Model
*/
public function delete()
{
$this->getMapper()->delete($this);
return $this;
}
/**
* Find model by primary field
*
* @param mixed $primary
* @return App_Model_Model|null
*/
public function find($primary)
{
return $this->getMapper()->find($primary, $this);
}
/**
* Set data mapper
*
* @param mixed $mapper
* @return App_Model_Mapper
*/
public function setMapper($mapper)
{
$this->_mapper = $mapper;
return $this;
}
/**
* Get data mapper
*
* Lazy loads this model mapper instance if no mapper registered.
*
* @return App_Model_Mapper
*/
public function getMapper()
{
if (!is_object($this->_mapper))
{
$this->setMapper(new $this->_mapper());
}
return $this->_mapper;
}
}
Above we see the common methods __get and __set defined, as well as a constructor and a setOptions helper method. These are discussed in the Zend Framework article referenced at the top of this post. However, there are several additional methods defined here as well including get/set count, save, find, and get/set mapper. These are all helper methods that will allow us to easily interact with the mapper that we will be defining.
- getCount()/setCount()
- The count of a model will contain the total number of items in the set the model was returned from. Suppose the mapper determines that there are 13 posts that match some criteria but we told it to return only 10 at most. Each post model object will its count set to 13. Knowing the total number of items in the set this model belongs to allows for easy pagination.
- save()
- The save helper method calls this model's mapper to save itself. The mapper will then save this object into the database. This is a helper function that makes creating and updating models as easy as:
$post = new Model_Blog_Post(array( 'title' => "My Title", 'body' => "My Body", 'author' => "Patrick", 'date' => time()); $post->save(); - delete()
- The delete helper method calls mapper delete method to remove this object's data from underlying data source
- find()
- Another helper method to populate this post object without directly calling mapper. This assumes the post is identifiable by some primary key. If the post is found by mapper, the mapper populates the post data into this model. Otherwise, nothing happens. As an added feature, this method returns result of the mapper's find, which is either the model itself (the post was found) or null if the post wasn't found. Here is an example of populating a post object with the post that has postid = 5:
$post = new Model_Blog_Post(); if ($post->find(5) !== null) echo "Post id 5 has title " . $post->title; - getMapper()/setMapper()
- The mapper for any particular model can be dynamically set. This even opens the possibility of transferring data from one data source into another data source by changing loading the data model, changing its mapper to the new data source mapper, and then calling save(). The mapper can be either a string or an object. If a string is specified, the mapper is automatically instantiated when it is needed in the getMapper() call. Doing this allows us to also set a default mapper class (we simply set our $_mapper member variable to our model's default mapper classname "Model_Blog_Mapper").
A mapper class communicates information between our data source (our database) and instances of our data model class. I will be posting a follow-up to this article with a powerful abstracted generic mapper soon.
1 Reference