May 15, 2008 7:23:57 PM

Tags: PHP Mysql Zend Framework Zend_Db

I've spent the last couple of days musing to myself about Zend Framework's extensibility. One particular feature that's always intrigued and impressed me is the way that Zend_Db handles rowsets and rows fetched by the Zend_Db_Table class: using inheritance it's possible define custom behaviour on both types of objects. As a demonstration of this, I thought I'd explore the concept of database object inheritance.

Inheritance is a mechanism very familiar to anyone who's worked with OO code, but it does not extend (excuse the pun) very comfortably to relational databases. As an example, consider the following three tables:

CREATE TABLE cars (
	`id` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
	`brand` VARCHAR(255) NOT NULL
);

CREATE TABLE trucks (
	`parent_id` INT(11) NOT NULL,
	`number_of_wheels` TINYINT NOT NULL
);

CREATE TABLE sports_cars (
	`parent_id` INT(11) NOT NULL,
	`top_speed` TINYINT NOT NULL
);

In a sense, it could be said that `trucks` inherits from `cars`, although the table `trucks` does not have the attributes, or columns, that it should be inheriting from its supertable. The inheritance, therefore, would have to reside in code and be supported by the relational architecture. There are several ways to achieve this, but before we jump in, let's set up three classes to represent the tables above.

<?php

class Car extends Zend_Db_Table
{
	protected $_name = 'cars';
}

class Truck extends Car
{
	protected $_name = 'trucks';
	protected $_rowClass = 'App_Db_Table_Row_Truck';

	protected $_referenceMap = array(
		'Car' => aray(
			'columns' => 'parent_id',
			'refTableClass' => 'Car',
			'refColumns' => 'id'
		)
	);
}

class SportsCar extends Car
{
	protected $_name = 'sports_cars';
	protected $_rowClass = 'App_Db_Table_Row_SportsCar';

	protected $_referenceMap = array(
		'Car' => aray(
			'columns' => 'parent_id',
			'refTableClass' => 'Car',
			'refColumns' => 'id'
		)
	);
}

Fetching a row using Truck or SportsCar will only return columns in their respective tables. Ideally we want a situation where fetching a row from either table also returns the brand, because by inheritance in this context it should have its supertable's columns as well. I'll suggest two ways to achieve this, and you'll notice that which method you choose is a tradeoff between efficiency and simplicity.

On-demand approach

Using this approach, the supertable's columns are fetched the first time a reference to its columns are made. Let's code up App_Db_Table_Row_Truck as an example:

<?php

class App_Db_Table_Row_Truck extends Zend_Db_Table_Row
{
	protected $_parent = NULL;

	public function __get ($columnName)
	{
		if (array_key_exists($columnName, $this->_data)
		{
			return $this->_data[$columnName];
		}

		if ($this->_parent === NULL)
		{
			$this->_parent = $this->findParentRow('Car');
		}

		return isset($this->_parent->$columnName) ? $this->_parent->$columnName : NULL;
	}
}

You can now reference $truck->brand, assuming $truck is an instance of App_Db_Table_Row_Truck, as well as $truck->number_of_wheels. There's nothing fancy going on here – it's just a display of how extensible the framework is – but it is making a second query to fetch the truck's inherited columns. If efficiency is not your key concern, then I recommend this approach. And although the Car supertable is hardcoded in this example, but it would be easy to generalize this code by creating an $_inheritsFrom attribute in the model.

Fetch-interception method

(Yes, I had some fun inventing these names)

This approach works quite simply by subclassing Zend_Db_Table and cunningly overriding the _fetch() method. In case you aren't familiar with it, _fetch() is the final port of call for fetchRow() and fetchAll(), which are used to retrieve rows and rowsets respectively. Out of the box, _fetch() looks like this:

<?php

protected function _fetch(Zend_Db_Table_Select $select)	
{
	$stmt = $this->_db->query($select);
	$data = $stmt->fetchAll(Zend_Db::FETCH_ASSOC);
	return $data;
}

The trick here is to add to $select a join to the supertable. So let's build App_Db_Table_Inheritable:

<?php

class App_Db_Table_Inheritable extends Zend_Db_Table
{
	protected $_inheritsFrom = NULL; // will be 'Car' later

	protected function _fetch (Zend_Db_Table_Select $select)
	{
		if ($this->_inheritsFrom !== NULL)
		{
			$parent = new $this->_inheritsFrom;
			$parentTable = $parent->getTable();

			$select->setIntegrityCheck(false); // needed to allow the join

			$select->join(
				array('parent' => $parentTable),
				$parentTable.'.id = '.$this->getTable().'.parent_id
			);
		}

		return parent::_fetch($select);
	}
}

To complete this example, let's modify Car and SportsCar a little:

<?php

// Car.php

class Car extends App_Db_Table_Inheritable
{
	protected $_name = 'cars';
} 

// SportsCar.php

class SportsCar extends Car
{
	protected $_name = 'sports_cars';
	
	protected $_referenceMap = array(...);

	protected $_inheritsFrom = 'Car';
}

What happens now is that when SportsCar fetch*() functions are called, the database query is automatically extended to include a join to the supertable. All row objects fetched this way will then have the data fetched from the supertable's columns. I quite like this technique because it combines sub- and superclass fetches into one query, but clearly it's a little more complex than the on-demand method.

If you can think of any other methods, please let me know. I've found so many uses for these techniques, and although the denormalization introduces possible inefficiencies, the table structure is much simpler and cleaner.

Recent Posts

Discussion

Subscribe to an RSS feed of these comments

Jani

May 16, 2008 12:12:53 PM

Here's a good article related to the topic of db table inheritance:
http://www.ibm.com/developerworks/library/ws-mapping-to-rdb/

Avi

May 18, 2008 11:29:43 PM

The downside with the second approach is that you will not be able to update the rowset that comes back (at least the out-of-the box) rowset...
This was an American two cents...not sure how much that's worth in South Africa...

Avi

May 18, 2008 11:34:25 PM

Also, I forgot to mention, a better practice, OOP wise, would be to end you're overwritten _fetch function with parent::_fetch($select), instead of copying the code verbatim. This is a good example of code reuse (and why we all like OOP so much).

Avi

May 18, 2008 11:54:29 PM

Sorry to keep bothering you...but three more comments...
1) Another downside of the first method is that toArray() would not work as expected since it would only return the child columns...
2) I don't think there is a getTable() method on Zend_Db_Table_Abstract (unless you implemented it yourself)
3) $select->join() would require a from() method to be called as well, otherwise it drops the child table from the query

Neil Garb

May 19, 2008 8:48:44 AM

Hi Avi -- thanks for all your comments!

I'm pretty sure that a few more tweaks would be necessary to get this method to work as naturally in the framework as OO inheritance would.

I only intend my posts to be discussion points, rather than tutorials, which is why I never post whole code or code that has been thoroughly scrutinized -- I just don't have the time :)

I appreciate your feedback and will make what updates I can to the post.

dave

Oct 29, 2009 2:33:01 PM

interesting

and a simple sql view to make jointure wouldn't be a simple solution ?

for example a table view_cars_trucks
after we juste need a normal class App_Db_Table_Car_Truck
with id car it give truck and car

of course to update and insert you have to use the class table car and truck
but so the logic is more in sql model

what do you think ?

Your comment