Custom Zend DB Table Base Class with Static Finder Methods

I’m putting it out there that I’m a fan of the Active Record Software Design Pattern.  Zend Framework uses a different design pattern for it’s database modelling called the Table Data Gateway Pattern.  In reality both are very good at doing what they’re meant to do, ie, model the tables in a database and return a collection of records for the user to manipulate.  As the proverb says there is always more than one way to skin a cat.

The main difference between the two is that most Active Record classes provide one class with static methods to find various types of results.  You would always invoke methods on the one class and it would be named in a singular fashion.  For examples this would be a “User” class.  However the Zend Framework uses an implementation of the Table Data Gateway which necessitates the use of two classes.  One named in a plural way to represent the table (a “Users” class) and one named in a singular way to represent each of the rows that is returned (a “User” class).  These classes are not called statically but rather they are instantiated first before use.

In the past I’ve used various incarnations of PHP frameworks that use Active Record and I like the easy static method invocation syntax to get a collection of data results.  I’m a lazy programmer and I wanted to use what the Zend Framework offered as it’s stable and well tested.  So instead of trying to hack something up (I actually did but we won’t go there), I decided to extend the functionality of Zend_Db_Table and Zend_Db_Row to give me a a Table Data Gateway Pattern with a taste of Active Record!  Think of it as a fusion to help my RAD-ness.

The result is a Zend_Db_Table class that has the basic tools that allow me to find collections of results statically.  In association with this base class, I’m using a naming convention that allows me to clearly distinguish what search criteria it is that I’m using to find records.  This has especially helps when I’m working with other developers as it provides structure. In the past I’ve found that these two things have made my development even more DRY and RAD.

Since PHP 5.3.x we’ve had Late Static Binding available to us.  This has brought about a whole new range of possibilities for objects in PHP and has brought along with it a new standard of object oriented programming in the PHP world.  In the following examples let’s pretend that we’re implementing a basic “Users” model class that would extend Zend_Db_Table, or in this case our custom parent class, say Swink_Db_Table.  

The following is a basic Object::getInstance method that allows us to bypass the whole $users = new Users() style of notation for the Table Gateway Class. It’s a similar method signature to what many singletons use but that’s not our purpose here.

/**
* Create an instance of the model statically
* @since As of 5.3
*
* @param array|Zend_Config $config
* @return Swink_Db_Table
*/
public static function getInstance($config = array()) {
   $class = get_called_class();
   return new $class($config);      
}

Note that in the days prior to PHP 5.3 we would need to put this method on the child class (ie, the actual “Users” class) and use “return new self()”. This would lead to a lot of repeated code doing the exact same thing. However the get_called_class function is able to detect which original class down the inhertence chain was called and thus return a new instance of that class. This enables us to push the instantiation method to the base class without any problems.

So now we can invoke our class statically. This is handy but in this particular context it’s really mostly used by other methods within the custom DB_Table class itself. The magic comes out when we start to use the naming convention in association with a dynamic way to call “finder” methods.

A “finder” method is a method that allows you to find a collection of results using some sort of criteria. For example the most common examples would be a “findAll()” or “findById()” method. You would then extend that so that you can use more refined search terms such “findAllByStatus()” or “findByUsername()”. Essentially we’re using a naming convention here of:

  • findBy<field>            # allows us to find one record by a field
  • findAllBy<field>         # allows us to find multiple records by a field

So putting this convention together with the magic __call function we’re able to create a whole set of “virtual” methods that allow us to search on any of the fields in the model/database table. This looks like the following:

/**
* Handles 'magic' finder method calls, like findBy<field> and findAllBy<field>
*
* @param string $method name of method to call.
* @param array $params parameters for the method.
* @return mixed Whatever is returned by called method
* @access protected
*/
public function __call($method, $params) {
   // see if its our magic findBy<field> option
   if (1 === preg_match('/^(findAllBy|findBy)(\w+)$/', $method, $matches)) {
      // set the function and field
      $function = $matches[1];
      $field = lcfirst(trim($matches[2]));
      $value = $params[0];
      // make sure the field is in the model
      if (in_array($field, $this->_getCols())) {
         // return a collection of instances
         if ('findAllBy' == $function) {
            return $this->fetchAll(array($field => $value));
         }
         else if ('findBy' == $function) {
            // return one instance 
            return $this->fetchRow(array($field => $value));
         }
      }
   }
   // catch all action exceptions
   throw new Zend_Db_Table_Exception("'$method' does not exist");
}

As you can see what this method is doing is taking the field from the end of the virtual method signature and using that to search upon in the model. From a syntax perspective this gets us to the point where we can make dynamic method calls without actually having defined any of these methods. Going back to our “Users” example class again:

  • Users::getInstance()->findById(7);
  • Users::getInstance()->findAllByName(“Johnny”);

Now this is all well and good but It’s still not complete. The icing on the cake is PHP 5.3′s new __callStatic() method. This allows us to go make dynamic calls statically just like the magic member method __call(). So adding the following method makes the base class complete:

/**
* Facilitates static calls such as findById() etc for non static 
* functions. Acts as a proxy to the non static class
*
* @param string $method
* @param mixed $params
* @return Swink_Db_Table
*/
public static function __callStatic($method, $params) {
   // see if we have a valid config
   $config = array();
   if ($params[0] instanceof Zend_Config || is_array($params[0])) {
      // set the config
      $config = $params[0];
   }   
   // pass the method call through to the magic call function     
   return self::getInstance($config)->__call($method, $params);
}

Now we can obtain collections of records using an Active Record finder style of notation. For example:

  • Users::findById(7);
  • Users::findAllByName(“Johnny”);

How easy is that? You have to admit that with the simple addition of these methods the class facilitates a very dynamic and powerful interface using virtual methods. Now we can use this notation in loops or inline statements. The Users::FindById(int) is especially handy.

 
// loop directly on the finder statements
foreach (Users::findAllByName("ted") as $user) {
   // do stuff with each user!
   if ($user->isActive()) {
      echo $user->email;
   }
}
 
// use the Zend_Db_Table toArray method to dump the records
print_r(Users::findAll()->toArray());
 
// inline statements - findById returns a "User" object and sets to inactive
Users::findById(7)->setActive(false);

Some of you may be scratching your head right now as you’ve realized that the __call method is being used twice and you’re thinking about the performance implications. You’re completely right in doing so as this method does incur an overhead. However RAD applications these days are all about scaffolding and getting something up and running. This dynamic style of invocation allows you to instantly have a suave of virtual finder methods available to you.

What I’ll usually do is later down the track after the first prototype is figure out which finder methods I actually need and implement concrete versions of those use cases directly in my model. Using the “Users” example I know that I always have a Users::findAllByName() method. Therefore I’ll manually code up that method and the __call will never be invoked. Remember that __call and __callStatic are only ever called as a last resort by an object when the method can’t be found. So for this example I would implement the findAllByName() method in the Users model class like so:

/**
* Statically find a collection of users by their name
* 
* @param mixed $where
* @param mixed $order
* @param mixed $count
* @param mixed $offset
* @return Zend_Db_Table_Rowset
*/
public static function findAllByName($name) {       
	return self::findAll(array('name' => $name));
}

Now the performance problem is gone and we have some cool syntax. I can even use these calls directly in loops or wherever I like. for example:

foreach (Users::findAllByName("ted") as $user) {
    // do stuff with each user!
}

Invariably there are two other examples of static methods that I ALWAYS use in the base class as they are like candy to the object orientated programmer. These are the findById() and findAll() methods. These can be implemented in the base table class without any difficulty like so:

/**
* Statically find a rowset
* 
* @param mixed $where
* @param mixed $order
* @param mixed $count
* @param mixed $offset
* @return Zend_Db_Table_Rowset
*/
public static function findAll($where = null, $order = null, $count = null, $offset = null) {       
   // the where (if an object) must be an instance of Zend_Db_Table_Select. Note the *Table*
   return self::getInstance()->fetchAll($where, $order, $count, $offset);
}
 
/**
* Statically find objects by their identifier. Simple example
* @param mixed $id
* @return Swink_Db_Table_Row
*/
public static function findById($id) {
   // fire up an instance
   $model = self::getInstance();   
   // primary keys can be an array in ZF
   $clause  = $model->info(Zend_Db_Table_Abstract::NAME) . '.';
   $clause .= current((array) $model->getPrimaryKey()) . ' = ?';                
   // return a populated instance
   return $model->fetchRow($model->getSelect()->where($clause, $id));
}

This now works a treat as the Users::findAll() method just wraps the fetchAll() functionality. I can build up a select object and pass it to the findAll() if I want to to refine my search or I can just call if without any parameters if I want all of the records as User objects (providing you’ve implemented a User class that extends Zend_Db_Table_Row of course).

There are a couple of other helper methods in there that you can see above, ie, getPrimaryKey() and getSelect(). These are public and very useful as you often want to obtain the primary key and the info() method doesn’t do it for me. The getSelect() method wraps the Zend_Db_Table select() function and disables integrity checks. This makes life easier, trust me. It will help your performance if you turn database metadata caching on, see this reference guide for more.

So putting it all together we have class that looks like (I cut and pasted some of this so no guarantees that it will work first go):

<?php
/**
* A base model object to extend the Zend_Db_Table 
* and provide some basic Active Record style flavor
*/
class Swink_Db_Table extends Zend_Db_Table_Abstract {
   /**
   * Setup some custom features
   * @param array|Zend_Config $config
   */
   public function __construct($config = array()) {
      // check out the config
      if ($config instanceof Zend_Config) {
         $config = $config->toArray();
      }
      // set the custom row class
      if (!isset($config['rowClass'])) {            
         //default to the basic custom row class
         $config['rowClass'] = 'Swink_Db_Table_Row';            
      }       
      // call the parent for the general setup
      parent::__construct($config);    
   }   
 
   /**
   * Public function for getting the primary key field
   * @return string|array
   */
   public function getPrimaryKey() {
      return $this->_primary;
   }
 
   /**
   * Override the default select to remove integrity checks
   * @param bool $withFromPart
   */
   public function getSelect($withFromPart = self::SELECT_WITHOUT_FROM_PART) {
      return parent::select($withFromPart)->setIntegrityCheck(false);
   }
 
   /**
   * Statically find a rowset
   * 
   * @param mixed $where
   * @param mixed $order
   * @param mixed $count
   * @param mixed $offset
   * @return Zend_Db_Table_Rowset
   */
   public static function findAll($where = null, $order = null, $count = null, $offset = null) {       
      // the where (if an object) must be an instance of Zend_Db_Table_Select. Note the *Table*
      return self::getInstance()->fetchAll($where, $order, $count, $offset);
   }
 
   /**
   * Statically find objects by their identifier. Simple example
   * @param mixed $id
   * @return Swink_Db_Table_Row
   */
   public static function findById($id) {
      // fire up an instance
      $model = self::getInstance();   
      // primary keys can be an array in ZF
      $clause  = $model->info(Zend_Db_Table_Abstract::NAME) . '.';
      $clause .= current((array) $model->getPrimaryKey()) . ' = ?';                
      // return a populated instance
      return $model->fetchRow($model->getSelect()->where($clause, $id));
   }
 
   /**
   * Create an instance of the model statically
   * @since As of 5.3
   *
   * @param array|Zend_Config $config
   * @return Swink_Db_Table
   */
   public static function getInstance($config = array()) {
      $class = get_called_class();
      return new $class($config);      
   }
 
   /**
   * Handles 'magic' finder method calls, like findBy<field> and findAllBy<field>
   *
   * @param string $method name of method to call.
   * @param array $params parameters for the method.
   * @return mixed Whatever is returned by called method
   * @access protected
   */
   public function __call($method, $params) {
      // see if its our magic findBy<field> option
      if (1 === preg_match('/^(findAllBy|findBy)(\w+)$/', $method, $matches)) {
         // set the function and field
         $function = $matches[1];
         $field = lcfirst(trim($matches[2]));
         $value = $params[0];
         // make sure the field is in the model
         if (in_array($field, $this->_getCols())) {
            // return a collection of instances
            if ('findAllBy' == $function) {
               return $this->fetchAll(array($field => $value));
            }
            else if ('findBy' == $function) {
               // return one instance 
               return $this->fetchRow(array($field => $value));
            }
         }
      }
      // catch all action exceptions
      throw new Zend_Db_Table_Exception("'$method' does not exist");
   }
 
   /**
   * Facilitates static calls such as findById() etc for non static 
   * functions. Acts as a proxy to the non static class
   *
   * @param string $method
   * @param mixed $params
   * @return Swink_Db_Table
   */
   public static function __callStatic($method, $params) {
      // see if we have a valid config
      $config = array();
      if ($params[0] instanceof Zend_Config || is_array($params[0])) {
         // set the config
         $config = $params[0];
      }   
      // pass the method call through to the magic call function     
      return self::getInstance($config)->__call($method, $params);
   }
}
 
/**
* A basic Users model class 
*/
class Users extends Swink_Db_Table {
   /**
   * The primary key
   * @var string|array
   */
   protected $_primary = 'id'; 
   /**
   * The table that this object relates to
   * @var string
   */
   protected $_name = 'users';
 
   /**
   * Add custom table related methods here
   */
 
   /**
   * Statically find a collection of users by their name
   * 
   * @param mixed $where
   * @param mixed $order
   * @param mixed $count
   * @param mixed $offset
   * @return Zend_Db_Table_Rowset
   */
   public static function findAllByName($name) {       
      return self::findAll(array('name' => $name));
   }
}
 
/**
* A basic subclassing of the row object 
*/
class Swink_Db_Table_Row extends Zend_Db_Table_Row_Abstract {
 
   /**
   * Implement validation and filtering via Zend_Input_Filter instance
   */
 
   /**
    * A convenience function to return the id
    * @return mixed
    */
   public function id() {     
      return $this->_data[current((array)$this->getTable()->getPrimaryKey())];
   }
 
   /**
   * Wrapper for the setFromArray method
   * @param array $data
   */
   public function setData(array $data) {
      $this->setFromArray($data);
      return $this;
   }
 
   /**
   * Shorthand method to set data and then save
   * @param array $data
   * @return Swink_Db_Table_Row
   */
   public function save(array $data = array()) {
      // if there is data then set it
      if (!empty($data)) {
         $this->setData($data);  
      }       
      return parent::save();
   }
}
 
/**
* A basic User model class 
*/
class User extends Swink_Db_Table_Row {
 
   /**
   * Add custom row related methods here
   */  
 
   /**
   * Check the see if this user is active. Checks the
   * bit field to see if they are active.
   * @return bool
   */
   public function isActive() {
      return (bool) $this->isActive;
   }
}

Now seriously, how easy does that make it to create models with static finder functions that rock! This is a bit of a simplified implementation but you can see the direction that it starts to take you in. It’s a very powerful way to start your day.

The code from this example can be found here.

Comments are closed.