Propel Gets Class Table Inheritance, With A Twist

Propel 1.6 already supports Single Table Inheritance and Concrete Table Inheritance, two powerful ways to map an object inheritance to a relational persistence. However, every once in a while, a Propel user pops in and asks for a Propel implementation of Class Table Inheritance. This type of inheritance uses one table per class in the inheritance structure ; each table stores only the columns it doesn't inherits from its parent.

For example, a sports news website displays statistics about various sports player. The Class Table Inheritance patterns translates that to a player table storing the identity, and two "children" tables, footballer and basketballer, with distinct statistics columns.

player
-------
first_name
last_name

footballer
------------
goals_scored
fouls_committed

basketballer
------------
points
field_goals

Implementing Class Table Inheritance via Joins

I have always thought that Class Table Inheritance isn't really inheritance. Actually, it is usually achieved using joins, by defining a foreign key in the children tables to the parent table, as follows:

player
-------
id
first_name
last_name

footballer
------------
id
goals_scored
fouls_committed
player_id       // foreign key to player.id

basketballer
------------
id
points
field_goals
three_points_field_goals
player_id       // foreign key to player.id

So to create a basketballer with an identity, relate a Basketballer to a Player the usual Propel way:

// create a Basketballer
basketballer = new Basketballer();
$basketballer->setPoints(101);
$basketballer->setFieldGoals(47);
$basketballer->setThreePointsFieldGoals(7);
// create a Player
$player = new Player();
$player->setFirstName('Michael');
$player->setLastName('Giordano');
// relate the two objects
$basketballer->setPlayer($player);
// save the two objects
$basketballer->save();

The Delegation Pattern

But this isn't inheritance. What the user expects, with the inheritance concept in mind, is to deal only with a Basketballer instance to manage both the identity and the stats, as follows:

$basketballer = new Basketballer();
$basketballer->setPoints(101);
$basketballer->setFieldGoals(47);
$basketballer->setThreePointsFieldGoals(7);
// use inheritance to hide join
$basketballer->setFirstName('Michael');
$basketballer->setLastName('Giordano');
// save basketballer and player
$basketballer->save();

Even if the two pieces of code would produce the same result (one basketballer record and one player record), the second one is more object-oriented.

But is it possible to achieve that using the PHP inheritance system? Not really, because the user wants the name information to be store in the player table, not in the basketballer table (otherwise Concrete Table Inheritance would be a better fit). As a matter of fact, the Basketballer object needs the Player object to handle the first name and last name for him. In object-oriented design, this is called "delegation". It's a very common design pattern, for example in Objective-C, where it is used extensively.

In PHP, a usual implementation of the delegation pattern is via the __call() magic method. So in order to make the previous code snippet work, all that's needed is the following code:

class Basketballer extends BaseBasketballer
{
  /**
   * Delegating not found methods to the related Player
   */
  public function __call($method, $params)
  {
    if (is_callable(array('Player', $method))) {
      if (!$delegate = $this->getPlayer()) {
        $delegate = new Player();
        $this->setPlayer($delegate);
      }
      return call_user_func_array(array($delegate, $method), $params);
    }
    return parent::__call($method, $params);
  }
}

And here you go, a Basketballer can reply to the Player method calls, and hide the join used to implement class table inheritance. For the end user, everything happens as if Basketballer actually extended Player, but the Player data is stored in a separate table.

Introducing the delegate behavior

Instead of providing yet another extension system in the Propel ActiveRecord classes, I implemented a behavior, called delegate, which allows to delegate method calls to another model. This behavior generates exactly the __call() code shown above, provided you set up your schema in the following way:

<table name="player">
  <column name="id" type="INTEGER" primaryKey="true" autoIncrement="true"/>
  <column name="first_name" type="VARCHAR" size="100"/>
  <column name="last_name" type="VARCHAR" size="100"/>
</table>
<table name="basketballer">
  <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
  <column name="points" type="INTEGER" />
  <column name="field_goals" type="INTEGER" />
  <column name="three_points_field_goals" type="INTEGER" />
  <column name="player_id" type="INTEGER" />
  <foreign-key foreignTable="player">
    <reference local="player_id" foreign="id" />
  </foreign-key>
  <behavior name="delegate">
    <parameter name="to" value="player" />
  </behavior>
</table>

Rebuild the model, and the Basketballer can now delegate all the method calls it can't manage on its own to his related Player, whether such a player already exists or not.

The delegate behavior, together with complete documentation and unit tests, has landed in the Propel master yesterday, and will be part of the upcoming 1.6.2 release.

You may think: Why should I be enthusiast about a behavior generating six lines of code in a __call() method? First of all, the delegate behavior has more features than than just simulating Class Table Inheritance. Second of all, it allows you to design your object model with delegation in mind, and that opens a lot of new possibilities.

Multiple Delegation

In PHP, an object can only inherit from one parent. However, delegation isn't restricted to a single class. So the Basketballer class can delegate to both a Player and an Employee class:

<table name="player">
  <column name="id" type="INTEGER" primaryKey="true" autoIncrement="true"/>
  <column name="first_name" type="VARCHAR" size="100"/>
  <column name="last_name" type="VARCHAR" size="100"/>
</table>
<table name="employee">
  <column name="id" type="INTEGER" primaryKey="true" autoIncrement="true"/>
  <column name="salary" type="INTEGER"/>
</table>
<table name="basketballer">
  <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
  <column name="points" type="INTEGER" />
  <column name="field_goals" type="INTEGER" />
  <column name="three_points_field_goals" type="INTEGER" />
  <column name="player_id" type="INTEGER" />
  <foreign-key foreignTable="player">
    <reference local="player_id" foreign="id" />
  </foreign-key>
  <column name="employee_id" type="INTEGER" />
  <foreign-key foreignTable="employee">
    <reference local="employee_id" foreign="id" />
  </foreign-key>
  <behavior name="delegate">
    <parameter name="to" value="player, employee" />
  </behavior>
</table>

Using only a Basketballer instance, a developer can now populate three records in three different tables:

$basketballer = new Basketballer();
$basketballer->setPoints(101);
$basketballer->setFieldGoals(47);
$basketballer->setThreePointsFieldGoals(7);
// delegate to player
$basketballer->setFirstName('Michael');
$basketballer->setLastName('Giordano');
// delegate to employee
$basketballer->setSalary(2000000);
// save basketballer and player and employee
$basketballer->save();

The liberty to use multiple inheritance might scare you, for it breaks one of the constraints that prevent many developers from designing horrible conceptual data models. However, it makes it possible to support Class Table Inheritance for several levels. For instance, if you modify the class hierarchy to have a ProBasketballer extend Basketballer extend Player, simple delegation doesn't work there. Even if ProBasketballer delegates to Basketballer, the generated ProBasketballer::__call() code won't be able to manage delegating all the way up to Player. The solution is to use multiple delegation to explicitly delegate to all ancestors, as follows:

<table name="player">
  <column name="id" type="INTEGER" primaryKey="true" autoIncrement="true"/>
  <column name="first_name" type="VARCHAR" size="100"/>
  <column name="last_name" type="VARCHAR" size="100"/>
</table>
<table name="basketballer">
  <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
  <column name="points" type="INTEGER" />
  <column name="field_goals" type="INTEGER" />
  <column name="three_points_field_goals" type="INTEGER" />
  <column name="player_id" type="INTEGER" />
  <foreign-key foreignTable="player">
    <reference local="player_id" foreign="id" />
  </foreign-key>
  <behavior name="delegate">
    <parameter name="to" value="player" />
  </behavior>
</table>
<table name="pro_basketballer">
  <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
  <column name="salary" type="INTEGER" />
  <column name="basketballer_id" type="INTEGER" />
  <foreign-key foreignTable="basketballer">
    <reference local="basketballer_id" foreign="id" />
  </foreign-key>
  <column name="player_id" type="INTEGER" />
  <foreign-key foreignTable="player">
    <reference local="player_id" foreign="id" />
  </foreign-key>
  <behavior name="delegate">
    <parameter name="to" value="basketballer, player" />
  </behavior>
</table>

Now a ProBasketballer can have a salary, while a simple Basketballer can't.

Delegating The Other Way Around

In all the examples shown previously, the foreign key supporting the delegation relation was located in the table that was actually delegating. This is because the main table (basketballer in the example) must have only one delegate in the other table (player in the example). The model must show a many-to-one relationship, and that places the foreign key in the delegating table.

player
-------
id
first_name
last_name

basketballer    // delegates to player
------------
id
points
field_goals
three_points_field_goals
player_id       // foreign key to player.id

But there is another way to have only one related record. Instead of using a many-to-one relationship, one could use a one-to-one relationship. In Propel, this is achieved by setting a foreign key which is also a primary key. So the player_id column can be removed, and the foreign key be placed on the basketballer primary key.

player
-------
id
first_name
last_name

basketballer   // delegates to player
------------
id             // foreign key to player.id
points
field_goals
three_points_field_goals

Since this kind of model is also suitable for delegation, the delegate behavior has been designed to supports one-to-one relationships as well.

One-to-one relationships are reversible. That means that the foreign key could be placed in the other table. For the player/basketballer model, that would mean:

player
-------
id             // foreign key to basketballer.id
first_name
last_name

basketballer   // delegates to player
------------
id
points
field_goals
three_points_field_goals

This is still supported by the behavior. But such a setup creates one constraint: a player can't have both basketballer and footballer stats anymore. In this case, it's not such a good idea. But think about this other use case:

user_profile
------------
id             // foreign key to user.id
first_name
last_name
email
telephone

user           // delegates to user_profile
-------
id
login
password

This schema may sound familiar to users of the sfGuardPlugin for the symfony framework. In this plugin, the User class handles only the basic identification data for a user. All the other information, like email address or full identity, is "delegated" to another class, the UserProfile. It is not a use case for Single Table Inheritance, but it's a great one for delegation.

Using the delegate behavior, Propel can now give access to the profile information directly from the user class:

<table name="user">
  <column name="id" type="INTEGER" primaryKey="true" autoIncrement="true"/>
  <column name="login" type="VARCHAR" size="100"/>
  <column name="password" type="VARCHAR" size="100"/>
  <behavior name="delegate">
    <parameter name="to" value="user_profile" />
  </behavior>
</table>
<table name="user_profile">
  <column name="id" type="INTEGER" primaryKey="true"/>
  <column name="first_name" type="VARCHAR" size="100"/>
  <column name="last_name" type="VARCHAR" size="100"/>
  <column name="email" type="VARCHAR" size="100"/>
  <column name="telephone" type="VARCHAR" size="100"/>
  <foreign-key foreignTable="user">
    <reference local="id" foreign="id" />
  </foreign-key>
</table>

In PHP, the developer can now write:

$user = new User();
$user->setLogin('francois');
$user->setPassword('S€cr3t');
// Fill the profile via delegation
$user->setEmail('francois@example.com');
$user->setTelephone('202-555-9355');
// save the user and its profile
$user->save();

This is why the concept of delegation is more powerful than Class Table Inheritance. There are a lot of use cases that delegation solves, without even being designed to do so. And this is why the introduction of the delegate behavior in Propel is such a great news.

Posted by Francois Zaninotto 

13 comments

Aug 22, 2011
Loïc said...
Great behavior and article, keep going on !
Aug 22, 2011
Guilherme Aiolfi said...
I was one of those users asking for the class table inheritance so this behavior will help me a lot. Thanks. Great work, Francois.
Nice to see you still committing code to propel and even better, in github. It makes it so much easier to interact with the project.
Aug 22, 2011
Gustavo said...
Wooo!!
This is huge. Thanks for this new behavior. By the way, why do you always use schema.xml instead schema.yml? Am I the only one still using YML?

Good luck!

Aug 22, 2011
@Gustavo: Propel alone doesn't support YAML schemas. It's via the sfPropelORMPlugin (and symfony) that Propel supports YAML.

Therefore, for those users who develop with Propel but without symfony, XML is the only schema format accepted.

Aug 22, 2011
Gustavo said...
@Francois Oh, that explains me everything. Thanks! (Anyway, it could be a nice new feature, isn't it?)
Aug 22, 2011
Nami said...
Nice. Even if IMHO, "_call" breaks the Propel philosophy.

IMHO (again), the model should foreach the delegates, try a setByName, and that setByName should try too to setByName on his delegate ... or something like that. But it becomes messy.
Rather, it can be done at buildtime ... Generating the elseif for all delegates, then doing the same for subdelegates, and so on.

Aug 22, 2011
@Nami: I thought about that, but first it's more work (which means it may happen in the future), and second it is not enough, since it doesn't work with methods added by the used to the ActiveRecord classes. So __call() is necessary anyway.

If you want to work on a patch that generates proxy methods for all columns, foreign keys, referrers, and cross joins for all delegates, together with unit tests, I'll consider it with great interest.

Aug 23, 2011
Nami said...
"since it doesn't work with methods added by the user to the ActiveRecord classes"
You're right. Here, __call may be used as fallback (didn't thought about that). But, having the ... "base" working would be nice too. But it may create headaches with too much nesting ... Add some kind or delegateLevel (as argument, if not, 0 (for all), default 0). I (may) am going to work on it as soon as I get back home.
Aug 23, 2011
Richtermeister said...
Maybe I don't understand this right, but to me the player -> footballer example doesn't call for composition and delegation. A footballer is an instance of a player and should just extend the player class. I do agree with divvying up the columns amongst the tables as you did, and joining them together to arrive at the complete dataset, but I don't get why a footballer needs to talk to a player when both could be the same object.

Daniel

Sep 01, 2011
Steve said...
I like this as it will be much cleaner IMHO than having data replicated across various tables.

What would be the best method to upgrade Basketballer to ProBasketballer? And what if he quits so he only wants to be a Basketballer in his retirement (assuming I have archived (woohoo!) his details for the Hall of Fame...?)

Sep 08, 2011
Amnes said...
Why BaseBasketballer class extend Player class?
Sep 08, 2011
Amnes said...
sry. once again.

Why BaseBasketballer class CAN'T extend Player class?

Sep 08, 2011
@Amnes: I'll let you find an implementation for that.

Leave a comment...