Programming and Technology
RSS icon Home icon
  • Domain Model Programming With the Zend Framework

    Posted on April 3rd, 2009 brandon 10 comments

    Each application I write tends to be an evolutionary successor to the previous and I suspect this is the case for most developers. With long term projects one tends to find things that work better than others, and methods that end up being a real pain in the ass and not as scalable as hoped once the code line count breaks a couple thousand or so. When I first got hired on at eROI I’ll admit my php was a bit rusty. I was fairly comfortable with it as I had developed a grab bag of small sites using procedural php and mysql, and I had a good understanding of the language as a language junky. It was fortunate that prior to working at eROI that I had delved head first into the world of Ruby on Rails and various python frameworks all of which allowed me to bask in the goodness that is MVC (Model-View-Controller architecture). As a result when I came on board with the charter of deploying large scale applications in php, my first goal was to figure out how to make php less painful and how to deploy the design patterns I had grown accustomed to.

    The Zend Framework was the perfect toolkit for these ends as it provided all the abstracted functionality I could want. The first site I built using the framework was pretty cool but I’ll be the first to admit I’d sooner have a foot amputated than have someone look at the code. While I had experience with MVC, I was still pretty green and my controllers were embarrassingly bloated with logic and manual manipulation of model objects. Controllers had an unmaintainable knowledge of model objects and even minor edits proved to be disastrous as a fix in one block of code would break another somewhere else. Learning is fun. Fortunately, that website never saw the light of day. When the next major project landed on my desk, I was chomping at the bit to apply all of the lessons I had learned from the previous abortion of code. Like a good little programmer I took the logic out of the controllers and pushed it into the models resulting in a much more maintainable codebase. That website did get deployed and has been extended multiple times without much ado and has proved to be a delight to work with.

    The following few projects, naturally, were built on evolutions of that same codebase, each one more abstracted and generalized than the one before. I’m a big fan of reusable code and have this vision of doubling my productivity every six months or so by applying the right technologies and re-purposing black box code whenever possible. I can’t say for certain if I’ve stayed true to the road map, but I have noticed that the projects get larger and more complex but development time is remaining the same so I must be doing something right. (I know I’m rambling on but bare with me, I’ll get to the topic in a moment). The last project I did was enormous. I started to see my coding methods strain under the pressure of the demands of the application and maintainability started to slip a bit. It was time for a major evolution.

    Enter: Domain Model Programming. I’m a total newb to this design pattern so I won’t comment much on it and instead refer you to wikipedia’s description. In a nutshell the idea is to separate out the true business logic from the persistance layer implementation. I first encountered the concept here on the blog of a huge Zend Framework contributor, during a search for some annoying abstraction problem I was having and when I read Matthew’s article a light bulb went on. Introducing this method to my coding has had the effect of allowing me to break the sound barrier and accomplish the true abstraction that I have needed for real re-usable objects. So now I am going to introduce how I have implemented it using the Zend Framework.

    So recently I decided to try out the Doctrine ORM library in place of the included Zend_Db module and in short I haven’t looked back. This made for a perfect opportunity to show how the database implementation can be made to be completely interchangable without having to recode the models or change any of the application specific logic. The point is the models, controllers and views have no concept of the persistance layer so it should be a breeze to swap out Zend_Db for Doctrine, and should that approach go south, it’s no effort to switch back, or replace both with something else entirely.

    So first things first. Let me introduce everyone to a series of base classes that can be re-used in any project. These classes provide the base functionality that this design pattern needs leaving the models themselves free to only concern themselves with the application itself.

    1.  
    2. <?php
    3.  
    4. abstract class FP_Model {
    5.   protected $_data = array();
    6.   protected $_gateway = null;
    7.  
    8.   public function __construct(array $pData, $pGateway){
    9.     $this->setGateway($pGateway);
    10.     $this->import($pData);
    11.   }
    12.  
    13.   public function __set($pAtt, $pVal){
    14.     if (array_key_exists($pAtt, $this->_data))
    15.       $this->_data[$pAtt] = $pVal;
    16.   }
    17.  
    18.   public function __get($pAtt){
    19.     switch ($pAtt){
    20.     case ‘gateway’:
    21.       return $this->getGateway();
    22.     default:
    23.       if (array_key_exists($pAtt, $this->_data))
    24.         return $this->_data[$pAtt];
    25.       else
    26.         throw new Exception("Invalid attribute – $pAtt");
    27.     }
    28.   }
    29.  
    30.   public function __isset($pAtt){
    31.     if (array_key_exists($pAtt, $this->_data))
    32.       return isset($this->_data[$pAtt]);
    33.     else
    34.       return false;
    35.   }
    36.  
    37.   public function __unset($pAtt){
    38.     if (array_key_exists($pAtt, $this->_data))
    39.       unset($this->_data[$pAtt]);
    40.     else
    41.       return null;
    42.   }  
    43.  
    44.   public function validate($pData){
    45.     $pData = $this->_convert($pData);
    46.  
    47.     return true;
    48.   }
    49.  
    50.   public function import($pData){
    51.     foreach ($pData as $key => $val)
    52.       $this->__set($key, $val);
    53.  
    54.     return $this;
    55.   }
    56.  
    57.   public function setGateway($pGateway){
    58.     $this->_gateway = $pGateway;
    59.     return $this;
    60.   }
    61.  
    62.   public function getGateway(){
    63.     return $this->_gateway;
    64.   }
    65.  
    66.   public function toArray(){
    67.     return $this->_data;
    68.   }
    69.  
    70.   protected function _convert($pData){
    71.     if (is_array($pData))
    72.       return $pData;
    73.     else if (is_object($pData))
    74.       return (array) $pData;           
    75.     else
    76.       throw new Exception("Data must be an datasource, array or object");
    77.   }
    78. }
    79. ?>
    80.  

    This is the class that all models extend. It’s purpose is to provide general CRUD functionality. The idea is that a model represents an object that has attributes and an interface to save itself. With that in mind, the base class manages an attribute array and an object that we refer to as a gateway. The gateway is the object that talks to the persistence layer so it acts as a broker for the model whenever the model needs to be saved or needs additional data from outside of itself.

    1.  
    2. <?php
    3.  
    4. class FP_Model_Collection implements Iterator, Countable
    5. {
    6.   protected $_model_class = ‘FP_Model’;
    7.   protected $_gateway = null;
    8.   protected $_collection = null;
    9.   protected $_count = null;
    10.  
    11.   public function __construct($pItems,` $pGateway){    
    12.     $this->setGateway($pGateway);
    13.     $this->setCollection($pItems);
    14.   }
    15.  
    16.   public function setGateway($pGateway){
    17.     $this->_gateway = $pGateway;
    18.     return $this;
    19.   }
    20.  
    21.   public function getGateway(){
    22.     return $this->_gateway;
    23.   }
    24.  
    25.   public function setCollection($pItems){
    26.     $this->_collection = $pItems;
    27.   }
    28.  
    29.   public function getCollection(){
    30.     return $this->_collection;
    31.   }
    32.  
    33.   public function count(){
    34.     if ($this->_count === null)
    35.       $this->_count = count($this->_collection);
    36.     return $this->_count;
    37.   }
    38.  
    39.   public function current(){
    40.     if ($this->_collection instanceof Iterator)
    41.       $key = $this->_collection->key();
    42.     else
    43.       $key = key($this->_collection);
    44.  
    45.     if ($key === null)
    46.       return false;
    47.  
    48.     $item = $this->_collection[$key];
    49.  
    50.     if ((get_class($item) != $this->_model_class) && !is_subclass_of($item, $this->_model_class)){
    51.       $ModelClass = $this->_model_class;
    52.       $item = new $ModelClass($item, $this->_gateway);
    53.       $this->_collection[$key] = $item;
    54.     }
    55.  
    56.     return $item;
    57.   }
    58.  
    59.   public function key(){
    60.     return key($this->_collection);
    61.   }
    62.  
    63.   public function next(){
    64.     return next($this->_collection);
    65.   }
    66.  
    67.   public function rewind(){
    68.     return reset($this->_collection);
    69.   }
    70.  
    71.   public function valid(){
    72.     return $this->current() !== false;
    73.   }
    74.  
    75.   public function append($pItem){
    76.     $this->_collection[] = $pItem;
    77.   }
    78.  
    79.   public function _toArray($pCollection){
    80.     $collection = array();
    81.     foreach ($pCollection as $item) {
    82.       if ($item instanceof FP_Model || $item instanceof FP_Model_Collection)
    83.         $collection[] = $item->toArray();
    84.       else
    85.         $collection[] = $item;      
    86.     }
    87.     return $collection;
    88.    
    89.   }
    90.  
    91.   public function toArray(){
    92.     return $this->_toArray($this->_collection);
    93.   }
    94. }
    95. ?>
    96.  

    This class exists solely for the purpose of acting as a collection of models. It is designed in a way that allows for lazy loading of classes. As you can see, the collection class implements some interfaces that allow it to be iterated through so we can use a standard foreach loop to step through the collection. The constructor of this class can take an array of models that have already been instantiated or an array of associative arrays describing a particular object. For example if we have a model representing a user that looks something like this:

    1.  
    2. <?php
    3.  
    4. class User extends FP_Model {
    5.   protected $_data = array(
    6.                                      ‘id’ => null,
    7.                                      ‘username’ => null,
    8.                                      ‘password’ => null
    9.                                     );
    10. }
    11.  
    12. ?>
    13.  

    We would be able to loop through a user collection like any other:

    1.  
    2.  
    3. $users = $usergateway->fetch();
    4. foreach ($users as $user){
    5.   //do something
    6. }
    7.  
    8.  

    You may have noticed the $_model_class property which contains the name of a class (specifically a model class). This is how the collection knows which class to use to instantiate models lazily as it steps through its internal array and finds descriptive arrays. When we define our Users collection class:

    1.  
    2. <?php
    3.  
    4. class Users extends FP_Model_Collection {
    5.    protected $_model_class = ‘User’;
    6. }
    7.  
    8. ?>
    9.  

    We override that property with the name of our own model class allowing the magic to happen internally. Some people may prefer to not do it that way. Another alternative is to always provide a common interface on the gateway to return the proper type of object when passed the descriptive array. In that situation, the current() function would instead look something like this:

    1.  
    2.  
    3.   public function current(){
    4.     if ($this->_collection instanceof Iterator)
    5.       $key = $this->_collection->key();
    6.     else
    7.       $key = key($this->_collection);
    8.  
    9.     if ($key === null)
    10.       return false;
    11.  
    12.     $item = $this->_collection[$key];
    13.  
    14.     if (!is_object($item)){
    15.       $item = $this->_gateway->create($item);
    16.       $this->_collection[$key] = $item;
    17.     }
    18.  
    19.     return $item;
    20.   }
    21.  
    22.  

    This method is probably better but either way works. So by now you are probably wondering what the hell is a gateway? Well as I said before it is the object that talks to the persistance layer (ie database, webservice, file, etc). The gateway has intimate knowledge about how to create models, how to manipulate them, and how to persist them. They can be thought of almost as drivers for web applications. It is ideal to have these adhere to an interface for a number of reasons which I will go into. So my interface looks like this:

    1.  
    2. <?php
    3.  
    4. interface FP_Model_Gateway_Interface {
    5.  
    6.   public function fetch($pCriteria=null, $pOffset=null, $pLimit=null);
    7.   public function count($pCriteria=null);
    8. }
    9.  
    10. ?>
    11.  

    Generally the application will talk to the gateway and ask for what it needs. The gateway will instantiate models and collections on demand and return them to the application which can be used for views. Additionally, because fetch() optionally takes an offset and limit, we can utilize the gateway in a paginator with the help of an Zend_Paginator adapter:

    1.  
    2. <?php
    3.  
    4. class FP_Model_Paginator_Adapter implements Zend_Paginator_Adapter_Interface {
    5.  
    6.   protected $_criteria = null;
    7.   protected $_gateway = null;
    8.   protected $_count = null;
    9.  
    10.   public function __construct($pCriteria, FP_Model_Gateway_Interface $pGateway){
    11.     $this->_criteria = $pCriteria;
    12.     $this->_gateway = $pGateway;
    13.   }
    14.  
    15.   public function getItems($pOffset, $pLimit){
    16.     return $this->_gateway->fetch($this->_criteria, $pOffset, $pLimit);
    17.   }
    18.  
    19.   public function count(){
    20.     return $this->_gateway->count($this->_criteria);
    21.   }
    22. }
    23.  
    24. ?>
    25.  

    Application logic not dependent on persistence can be processed inside of the models and when the application asks the model object to save itself, the model will in turn ask the gateway to make it happen. As a result we have all implementation specifics housed inside of the gateway allowing for any major backend changes to be implemented by swapping out that component alone. So lets take our User example further by demonstrating what a User gatway might look like:

    1.  
    2. <?php
    3.  
    4. class Doctrine_Users extends FP_Model_Gateway {
    5.  
    6.   public function create(array $pData=array()){
    7.     return new User($pData, $this);
    8.   }
    9.  
    10.   //Update existing user
    11.   public function replace($pId, array $pData){
    12.     $user = $this->fetch($pId);  
    13.     if ($user){
    14.       $user->import($pData);
    15.       return $user;
    16.     } else
    17.       throw new Exception(‘Invalid user id’);
    18.   }
    19.  
    20.   public function save(array $pData){
    21.     if (!is_null($pData[‘id’])){
    22.       $_user = Doctrine_Query::create()
    23.         ->select(‘u.*’)
    24.         ->from(‘Doctrine_Dao_User u’)
    25.         ->where(‘u.id = ?’, $pData[‘id’])
    26.         ->fetchOne();
    27.     } else
    28.       $_user = new Doctrine_Dao_User();
    29.      
    30.     if ($_user){    
    31.       //If password was changed, encrypt it
    32.       if ($_user->password != $pData[‘password’])
    33.         $pData[‘password’] = md5($pData[‘password’]);
    34.  
    35.       $_user->fromArray($pData);
    36.       $_user->save();
    37.     } else
    38.       throw new Exception(‘Invalid user id’);
    39.   }
    40.  
    41.   public function delete($pId){
    42.     return Doctrine_Query::create()
    43.       ->delete(‘Doctrine_Dao_User u’)
    44.       ->where("u.id = $pId")
    45.       ->execute();  
    46.   }
    47.  
    48.   public function count($pCriteria=null){
    49.     if (is_array($pCriteria)){
    50.       $q = Doctrine_Query::create()
    51.         ->select(‘COUNT(u.id)’)
    52.         ->from(‘Doctrine_Dao_User u’);
    53.      
    54.       $first = true;
    55.       foreach ($pCriteria as $key => $val){
    56.         if ($first){
    57.           $q = $q->where("$key = ?", $val);
    58.           $first = false;
    59.         } else
    60.           $q = $q->andWhere("$key = ?", $val);
    61.       }
    62.       return $q->execute(array(), Doctrine::HYDRATE_SINGLE_SCALAR);
    63.     } else {
    64.       $q = Doctrine_Query::create()
    65.         ->select(‘COUNT(u.id)’)
    66.         ->from(‘Doctrine_Dao_User u’);
    67.       return $q->execute(array(), Doctrine::HYDRATE_SINGLE_SCALAR);
    68.     }
    69.   }
    70.  
    71.   public function fetch($pCriteria=null, $pOffset=null, $pLimit=null){
    72.     if (is_numeric($pCriteria)){
    73.       $_user = Doctrine_Query::create()
    74.         ->select(‘u.*’)
    75.         ->from(‘Doctrine_Dao_User u’)
    76.         ->where("u.id = $pCriteria")
    77.         ->fetchOne();
    78.       if ($_user)
    79.         return new User($_user->toArray(), $this);
    80.     } else if (is_array($pCriteria)){
    81.       $q = Doctrine_Query::create()
    82.         ->select(‘u.*’)
    83.         ->from(‘Doctrine_Dao_User u’);
    84.      
    85.       $first = true;
    86.       foreach ($pCriteria as $key => $val){
    87.         if ($first){
    88.           $q = $q->where("$key = ?", $val);
    89.           $first = false;
    90.         } else
    91.           $q = $q->andWhere("$key = ?", $val);
    92.       }
    93.  
    94.       if ($pOffset)
    95.         $q = $q->offset($pOffset);
    96.  
    97.       if ($pLimit)
    98.         $q = $q->limit($pLimit);
    99.  
    100.       $_users = $q->execute();
    101.       return new Users($_users->toArray(), $this);
    102.     } else {
    103.       $q = Doctrine_Query::create()
    104.         ->select(‘u.*’)
    105.         ->from(‘Doctrine_Dao_User u’)
    106.  
    107.       if ($pOffset)
    108.         $q = $q->offset($pOffset);
    109.  
    110.       if ($pLimit)
    111.         $q = $q->limit($pLimit);
    112.  
    113.       $_users = $q->execute();
    114.       return new Users($_users->toArray(), $this);
    115.     }      
    116.   }
    117. }
    118. ?>
    119.  

    As you can see, the gateway implements CRUD operations intelligently. It takes data in the form of associative arrays and happily serializes the information into databases, files, etc. This particular gateway is able to determine whether an object requires an INSERT operation or an UPDATE operation by inspecting the data for the presence of an ‘id’ attribute. It is here where we implement functionality provided by the Doctrine library. So for those of you who are observant, you may be wondering, where are Doctrine’s table abstraction classes? At some point we have to extend Doctrine_Record in order to manipulate those tables. Those are stored in a subfolder tied to the persistence implementation. For this example the folder structure of the models looks something like this:

    • models/
      • Doctrine/
        • Dao/
          • User.php
        • Users.php
      • User.php
      • Users.php

    Here I have created a folder named ‘Doctrine’ since the implementation uses a database for persistence. This folder houses the gateway classes as well as another folder named ‘Dao’ which houses the Doctrine table models. The Gateway classes rely on the database models in the Dao folder in order to manipulate the database. Here is the doctrine User model

    1.  
    2. <?php
    3.  
    4. class Doctrine_Dao_User extends Doctrine_Record
    5. {
    6.     public function setTableDefinition()
    7.     {
    8.         $this->setTableName(‘users’);
    9.         $this->hasColumn(‘id’, ‘integer’, 4, array(‘type’ => ‘integer’, ‘length’ => 4, ‘primary’ => true, ‘autoincrement’ => true));
    10.         $this->hasColumn(‘username’, ’string’, 255, array(‘type’ => ’string’, ‘length’ => 255, ‘default’ => , ‘notnull’ => true));
    11.         $this->hasColumn(‘password’, ’string’, 255, array(‘type’ => ’string’, ‘length’ => 255, ‘default’ => , ‘notnull’ => true));
    12.     }
    13. }
    14.  

    This class implements all of the functionality needed to manipulate the user table in the database by extending the Doctrine_Record base class. Details on how these are implemented can be found here in the Doctrine guide.

    Alternately, we could also have a ZendDb folder containing the gateway classes that work with Zend_Db instead of doctrine. In this scenario our folders would look like this:

    • models/
      • Doctrine/
        • Dao/
          • User.php
        • Users.php
      • ZendDb/
        • Dao/
          • User.php
        • Users.php
    • User.php
    • Users.php

    And inside the Dao folder for that implementation we would have our Zend_Db table model.

    1.  
    2. <?php
    3.  
    4. class ZendDb_Dao_User extends Zend_Db_Table {
    5.   protected $_name = ‘users’;
    6. }
    7. ?>
    8.  

    With a little imagination we can make switching between implemenations dynamic. For those who read my earlier article on application skeletons, this can be accomplished by extending our application modules with getters for gateway objects. We can implement our module as such:

    1.  
    2. <?php
    3.  
    4. class AppModule implements FP_Application_Module {
    5.  
    6.   /* …..Snipped ….*/
    7.  
    8.  public function getUserGateway(){
    9.    return new Doctrine_Users();
    10.  }
    11.  
    12. }
    13.  
    14. ?>
    15.  

    Controllers and helpers can then grab an instance of this module and retrieve the intended gateway to be used and not have to be concerned about which persistence implementation they should be using. When making the switch we can simply alter our application module to return a different gateway object and be assured it will propogate through the entire application. Now that’s easy.

    So now we have a pair of classes User and Users which are 100% portable to any other application with little or no modification. Chances are the Gateway classes will be portable too most of the time. Whether we are storing them in databases, xml files, or serializing them over a REST call, we can be assured that our class will behave consistently in each scenario. This architecture also lends itself heavily to optimal unit testing which is a boon in itself.

    • Share/Bookmark
     

    10 responses to “Domain Model Programming With the Zend Framework”

    1. [...] The only major deviation I have made from the past versions of the skeleton is the migration from Zend_Db to Doctrine and the use of domain model programming which only affects the module model folders. For more on this, see my recent article about it. [...]

    2. Thx for this great article!

      But I’ve one question: your class Doctrine_Users extends the class FP_Model_Gateway. But you didn’t post that class here, or did I only overlook it?

    3. The FP_Model_Gateway class is nothing more than an abstract class so it actually implements nothing. All it provides is a stub for fetch() in order to adhere to the FP_Model_Gateway_Interface. It’s purpose is to provide an interface for FP_Model_Paginator_Adapter

    4. nice post Brandon, I was reading the same article by Matthew but I could not figure out if the User model had a domain logic method like…

      $userGateway = new UserGateway();
      $user = $userGateway->find(4);
      $user->updatePayrollDeduction(5);
      $userGateway->save($user);

      if the updatePayrollDeduction method needed access to the db, would it just call it’s own gateway when it needed DB info? I’d imagine no DB references would be in the model itself?

      thanks

    5. That’s correct. The model has no concept of the persistence layer at all. It’s best to treat all CRUD operations as atomic, meaning queue up the data that needs to be saved or altered in the model class and pass it all in to the gateway when save is called and let the gateway sort everything out. The exception is when you need to pull in related records (One to One, One to Many, etc), you can lazy load a collection of related records by requesting them from the gateway which knows how to retrieve them using the model’s id.

    6. [...] and one of the entries that piqued my interest was a post by brandon from realm of zod titled: Domain Model Programming With the Zend Framework which talks about using Doctrine ORM instead of Zend DB Table’s Table Data Gateway [...]

    7. [...] Some time ago I wrote an article on how to implement domain model programming with the Zend Framework while using Doctrine for your Object Relational Mapper (ORM), you read it here: [...]

    8. Hi, Just wanted to stop by to tell you that I decided to try Doctrine out as per your recommendation on my blog post: http://blog.rvdavid.net/zend-framework-model/

      Puts the final piece of the puzzle for me. I was planning on implementing my own DataMapper / Model layer, Doctrine actually does all this for me.

      Thank you for your recommendation.
      Keep in touch!

    9. [...] sajnos sosem jártam igazi sikerrel. Kipróbáltam mások elképzeléseit is (Model Infrastructure, Domain Model Programming With the Zend Framework) de hosszabb távon, az igények növekedésével ezek a megvalósítások mindig zsákutcának [...]

    10. [...] will be building upon the foundation I have laid in past articles so you may want to catch up: Domain Model Programming with the Zend Framework Doctrine, Complex SQL Queries, and [...]

    Leave a reply

Get Adobe Flash playerPlugin by wpburn.com wordpress themes