Issues

ZF-5906: Multi rowclass support in Zend_Db_Table_Abstract and Zend_Db_Table_Rowset_Abstract

Description

Rows from a database table should have the possibility to be instantiated as different types of objects depending on the values in the database row. Currently Zend_Db_Table_Abstract and Zend_Db_Table_Rowset_Abstract do not enable it.

I suggest that a protected method called getRowClass(array $data = array()) is added to both the _Table and Rowset abstract classes.

The methods to be modified are the following (changes made against ZF 1.7.2): -Zend_Db_Table_Abstract -- public function createRow(array $data = array(), $defaultSource = null) Replace lines


@Zend_Loader::loadClass($this->_rowClass);
$row = new $this->_rowClass($config);

With


$className = $this->_getRowClass($data);
$row = new $className($config);

-- public function fetchRow($where = null, $order = null) Replace lines


@Zend_Loader::loadClass($this->_rowClass);
return new $this->_rowClass($data);

With


$className = $this->_getRowClass($rows[0]);
return new $className($data);

-Zend_Db_Table_Rowset_Abstract -- public function current() Replace lines


$this->_rows[$this->_pointer] = new $this->_rowClass(
  array(
    'table'    => $this->_table,
    'data'     => $this->_data[$this->_pointer],
    'stored'   => $this->_stored,
    'readOnly' => $this->_readOnly
  )
);

With


$className = $this->_getRowClass($this->_data[$this->_pointer]); // Find the correct class name
$this->_rows[$this->_pointer] = new $className(
  array(
    'table'    => $this->_table,
    'data'     => $this->_data[$this->_pointer],
    'stored'   => $this->_stored,
    'readOnly' => $this->_readOnly
  )
);

Both Zend_Db_Table_Abstract and Zend_Db_Table_Rowset_Abstract should contain the following method, which then can be overridden in inheriting classes.


/**
 * Returns a string matching the class name for this data. Default implementation returns the classname
 * given in ROW_CLASS configuration option.
 * @param array $data
 * @return string Name of the class matching the given data.
*/
protected function _getRowClass(array $data = array())
{
  @Zend_Loader::loadClass($this->_rowClass);
  return $this->_rowClass;
}

Comments

Example usage:


class My_Entity_Rowset extends Zend_Db_Table_Rowset_Abstract
{
  protected $_myClassMap = array("default" => "My_Entity_Default", "nice" => "My_Entity_Nice");

  protected function _getRowClass(array $data = array())
  {
    $ret = $_this->_rowClass;
    if(isset($data["type"]) {
      $type = $data["type"];
      if(isset($this->_myClassMap[$type]) {
        $ret = $this->_myClassMap[$type];
      }
  }
  return $ret;
}

Imho this feature should be left for userland code only where it's easy to implement. This is useful just for a few special cases.

The trouble with implementing this feature in userland code is that we lose compatibility with Zend Framework. Because PHP doesn't allow casting of objects we cannot call for example the fetchRow or createRow methods of the Zend_Db_Table_Abstract. We need to directly instantiate the correct implementing class. Between ZF 1.5 and 1.7 the implementation of Zend_Db_Table_Abstract::createRow() and its dependencies were modified so much that it broke the compatibility between my createRow() implementation and the rest of Zend_Db_Table_Abstract.

I still think it easy to do in user code and keep "compatibility" with ZF, simply by overriding Zend_Db_Table_Abstract::createRow() like this:


public function createRow(array $data = array(), $defaultSource = null)
{
    $oldRowClass = $this->_rowClass;
    if ($data['attr'] == 'foo') {
        $this->_rowClass = 'My_New_Row_Class';
    }
    $row = parent::createRow($data, $defaultSource);
    $this->_rowClass = $oldRowClass;
    return $row;
}

Though the implementation of Zend_Db_Table_Abstract::createRow() changed, it still works.

You are right Martin that the code you showed enables us to keep Zend_Db_Table_Abstract's createRow() intact. But it doesn't work for fetchRow() because the data is retrieved inside the method the object is instantiated. Similar problems occur with the Zend_Db_Table_Rowset_Abstract::current() method.

I've the exact same need for this feature. It would be nice if it could be implement as suggested by Tommy!