Blog: 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('[email protected]'); $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.