From:
http://jasonswett.net/blog/inheritance-with-symfony-and-doctrine-orm/
The Problem
Doctrine ORM claims to support some kind of inheritance but I have yet to see a good example. The inheritance documentation on the Doctrine ORM site could be worse but it certainly has a lot of room for improvement. How about an example with some actual data? The symfony documentation for Doctrine inheritance lays things out a little more clearly than the Doctrine docs but it still wants for good examples.
The Solution
Here I intend to perform a hearty exploration of Doctrine inheritance with the hope that I can show, by example, what it can and can’t do. The inheritance model I will use will be simple: the base class will be called
If you’d like to follow along, I’ve put together some setup instructions for this project.
Doctrine offers three styles of inheritance: Simple, Concrete and Column Aggregation. The symfony docs give the following definitions:
Simple Inheritance
The Doctrine documentation shows an example where we have a
Let’s try something similar to their example but go a little further with it.
Since we’re using enums, we’ll have to update
Now let’s build the model, build the SQL and insert the SQL:
What does this give us in the database?
As you can clearly see, all the inheriting classes’ properties have been rolled into one table along with the base class’s properties. This is exactly what the Doctrine doc tells us, but it doesn’t totally spell it out with an example.
The table above is a bad model, both from a purist and pragmatic perspective. A purist might say, “We have three distinctly different types of things but only one data structure! Are we expected to deduce, based on which values are
If you look at the PHP classes created by symfony, they’re meaningless. Take a look at
There’s nothing unique to Chicken there. All the properties of all the
I’ve seen enough of “simple inheritance” to know that I would never use it. Let’s see what “concrete inheritance” is all about.
Concrete Inheritance
Apparently all that’s needed to switch from simple inheritance to concrete inheritance is to replace all instances of type: simple with type: concrete:
Let’s run our symfony commands again:
What’s in the database now?
That’s encouraging: each class has its own separate table.
That looks like what I want. Each entity is represented by a table that has a column for each of its properties, nothing more and nothing less. The only way this might be improved, in my opinion, is if we could tell Doctrine that
If we look at the symfony-generated classes, they look how we’d expect:
Doctrine’s “concrete inheritance” looks pretty solid. Let’s take a look at the third and final style, “column aggregation.”
Column Aggregation
To be honest, I’m pretty baffled as to what this one is supposed to do for us. Hopefully this example will make things clearer.
One table again, just like simple inheritance. Since I already rejected this structure, I see no reason to continue with column aggregation.
Conclusion
In my opinion, Doctrine’s simple inheritance and column aggregation are invalid and concrete is the only way to go. I hope these examples cleared up some confusion for anyone who had as much trouble with these concepts as I did.
The Problem
Doctrine ORM claims to support some kind of inheritance but I have yet to see a good example. The inheritance documentation on the Doctrine ORM site could be worse but it certainly has a lot of room for improvement. How about an example with some actual data? The symfony documentation for Doctrine inheritance lays things out a little more clearly than the Doctrine docs but it still wants for good examples.
The Solution
Here I intend to perform a hearty exploration of Doctrine inheritance with the hope that I can show, by example, what it can and can’t do. The inheritance model I will use will be simple: the base class will be called
FarmAnimal
and the three inheriting classes will be
Cow
,
Dog
and
Chicken
. All
FarmAnimal
s will have at least a name, a sound and a number of legs. Each type of
FarmAnimal
will have the following unique properties:
Cow
will have a use (beef or dairy), a
Dog
will have a breed and a
Chicken
will have an egg color.
If you’d like to follow along, I’ve put together some setup instructions for this project.
Doctrine offers three styles of inheritance: Simple, Concrete and Column Aggregation. The symfony docs give the following definitions:
Name | Description |
---|---|
Concrete | Each child class has a separate table has all the columns of its parents |
Simple | Each child class shares the same table and columns as its parents |
Column Aggregation | All columns must be defined in the parent and each child class is determined by a type column |
The Doctrine documentation shows an example where we have a
User
class and a
Group
class. Both of these classes inherit from
Entity
and neither one has any unique properties. When the example schema is evaluated, Doctrine only spits out one table:
entity
. If that’s all that “simple inheritance” is good for, I don’t really see how that’s inheritance. Any object you might instantiate, whether it be a
User
,
Group
or plain
Entity
, is an
Entity
, nothing more and nothing less. What’s gained there?
Let’s try something similar to their example but go a little further with it.
config/doctrine/schema.yml
FarmAnimal: columns: name: string(20) sound: string(20) leg_count: integer created_at: timestamp updated_at: timestamp Cow: inheritance: extends: FarmAnimal type: simple columns: purpose: type: enum values: [beef, dairy] Dog: inheritance: extends: FarmAnimal type: simple columns: breed: string(20) Chicken: inheritance: extends: FarmAnimal type: simple columns: egg_color: type: enum values: [brown, white]
config/databases.yml
and add the
use_native_enum
directive:
all: doctrine: class: sfDoctrineDatabase param: dsn: 'mysql:host=localhost;dbname=inheritance' username: inheritance password: pass123 attributes: use_native_enum: true
$ symfony doctrine:build-model $ symfony doctrine:build-sql $ symfony doctrine:insert-sql
mysql> SHOW TABLES;
+-----------------------+
| Tables_in_inheritance |
+-----------------------+
| farm_animal |
+-----------------------+
1 ROW IN SET (0.00 sec)
mysql> DESCRIBE farm_animal;
+------------+-----------------------+------+-----+---------+----------------+
| FIELD | TYPE | NULL | KEY | DEFAULT | Extra |
+------------+-----------------------+------+-----+---------+----------------+
| id | BIGINT(20) | NO | PRI | NULL | AUTO_INCREMENT |
| name | VARCHAR(20) | YES | | NULL | |
| sound | VARCHAR(20) | YES | | NULL | |
| leg_count | BIGINT(20) | YES | | NULL | |
| created_at | datetime | YES | | NULL | |
| updated_at | datetime | YES | | NULL | |
| purpose | enum('beef','dairy') | YES | | NULL | |
| breed | VARCHAR(20) | YES | | NULL | |
| egg_color | enum('brown','white') | YES | | NULL | |
+------------+-----------------------+------+-----+---------+----------------+
9 ROWS IN SET (0.01 sec)
The table above is a bad model, both from a purist and pragmatic perspective. A purist might say, “We have three distinctly different types of things but only one data structure! Are we expected to deduce, based on which values are
NULL
and which are not, which type of
FarmAnimal
we’re dealing with? What if we add the idea of a
Cat
with a breed? How do we tell it apart from a
Dog
?” And a pragmatist might say, “What if we have 20 different types of
FarmAnimal
s, each having 5 different properties? That’s at least 100 columns in the
FarmAnimal
table!” I would agree with both the pragmatist and the purist. Imagine looking at a table with 100 columns, most of the rows having
NULL
in most of the columns most of the time. That’s an extreme example, but the principle is the same on any scale.
If you look at the PHP classes created by symfony, they’re meaningless. Take a look at
lib/model/doctrine/base/BaseChicken.class.php
, for example:
/**
* BaseChicken
*
* This class has been auto-generated by the Doctrine ORM Framework
*
*
* @package INHERITANCE
* @subpackage model
* @author Your name here
* @version SVN: $Id: Builder.php 6820 2009-11-30 17:27:49Z jwage $
*/
abstract class BaseChicken extends FarmAnimal
{
public function setUp()
{
parent::setUp();
}
}
FarmAnimal
s are kept in
lib/model/doctrine/base/BaseFarmAnimal.class.php
:
abstract class BaseFarmAnimal extends sfDoctrineRecord
{
public function setTableDefinition()
{
$this->setTableName('farm_animal');
$this->hasColumn('name', 'string', 20, array(
'type' => 'string',
'length' => '20',
));
$this->hasColumn('sound', 'string', 20, array(
'type' => 'string',
'length' => '20',
));
$this->hasColumn('leg_count', 'integer', null, array(
'type' => 'integer',
));
$this->hasColumn('created_at', 'timestamp', null, array(
'type' => 'timestamp',
));
$this->hasColumn('updated_at', 'timestamp', null, array(
'type' => 'timestamp',
));
$this->hasColumn('purpose', 'enum', null, array(
'type' => 'enum',
'values' =>
array(
0 => 'beef',
1 => 'dairy',
),
));
$this->hasColumn('breed', 'string', 20, array(
'type' => 'string',
'length' => '20',
));
$this->hasColumn('egg_color', 'enum', null, array(
'type' => 'enum',
'values' =>
array(
0 => 'brown',
1 => 'white',
),
));
}
public function setUp()
{
parent::setUp();
}
}
Concrete Inheritance
Apparently all that’s needed to switch from simple inheritance to concrete inheritance is to replace all instances of type: simple with type: concrete:
FarmAnimal: columns: name: string(20) sound: string(20) leg_count: integer created_at: timestamp updated_at: timestamp Cow: inheritance: extends: FarmAnimal type: concrete columns: purpose: type: enum values: [beef, dairy] Dog: inheritance: extends: FarmAnimal type: concrete columns: breed: string(20) Chicken: inheritance: extends: FarmAnimal type: concrete columns: egg_color: type: enum values: [brown, white]
$ symfony doctrine:build-model $ symfony doctrine:build-sql $ symfony doctrine:insert-sql
mysql> SHOW TABLES;
+-----------------------+
| Tables_in_inheritance |
+-----------------------+
| chicken |
| cow |
| dog |
| farm_animal |
+-----------------------+
4 ROWS IN SET (0.00 sec)
mysql> DESCRIBE farm_animal;
+------------+-------------+------+-----+---------+----------------+
| FIELD | TYPE | NULL | KEY | DEFAULT | Extra |
+------------+-------------+------+-----+---------+----------------+
| id | BIGINT(20) | NO | PRI | NULL | AUTO_INCREMENT |
| name | VARCHAR(20) | YES | | NULL | |
| sound | VARCHAR(20) | YES | | NULL | |
| leg_count | BIGINT(20) | YES | | NULL | |
| created_at | datetime | YES | | NULL | |
| updated_at | datetime | YES | | NULL | |
+------------+-------------+------+-----+---------+----------------+
6 ROWS IN SET (0.00 sec)
mysql> DESCRIBE cow;
+------------+----------------------+------+-----+---------+----------------+
| FIELD | TYPE | NULL | KEY | DEFAULT | Extra |
+------------+----------------------+------+-----+---------+----------------+
| id | BIGINT(20) | NO | PRI | NULL | AUTO_INCREMENT |
| name | VARCHAR(20) | YES | | NULL | |
| sound | VARCHAR(20) | YES | | NULL | |
| leg_count | BIGINT(20) | YES | | NULL | |
| created_at | datetime | YES | | NULL | |
| updated_at | datetime | YES | | NULL | |
| purpose | enum('beef','dairy') | YES | | NULL | |
+------------+----------------------+------+-----+---------+----------------+
7 ROWS IN SET (0.00 sec)
mysql> DESCRIBE dog;
+------------+-------------+------+-----+---------+----------------+
| FIELD | TYPE | NULL | KEY | DEFAULT | Extra |
+------------+-------------+------+-----+---------+----------------+
| id | BIGINT(20) | NO | PRI | NULL | AUTO_INCREMENT |
| name | VARCHAR(20) | YES | | NULL | |
| sound | VARCHAR(20) | YES | | NULL | |
| leg_count | BIGINT(20) | YES | | NULL | |
| created_at | datetime | YES | | NULL | |
| updated_at | datetime | YES | | NULL | |
| breed | VARCHAR(20) | YES | | NULL | |
+------------+-------------+------+-----+---------+----------------+
7 ROWS IN SET (0.01 sec)
mysql> DESCRIBE chicken;
+------------+-----------------------+------+-----+---------+----------------+
| FIELD | TYPE | NULL | KEY | DEFAULT | Extra |
+------------+-----------------------+------+-----+---------+----------------+
| id | BIGINT(20) | NO | PRI | NULL | AUTO_INCREMENT |
| name | VARCHAR(20) | YES | | NULL | |
| sound | VARCHAR(20) | YES | | NULL | |
| leg_count | BIGINT(20) | YES | | NULL | |
| created_at | datetime | YES | | NULL | |
| updated_at | datetime | YES | | NULL | |
| egg_color | enum('brown','white') | YES | | NULL | |
+------------+-----------------------+------+-----+---------+----------------+
7 ROWS IN SET (0.00 sec)
FarmAnimal
is an abstract class and we don’t want a table for it.
If we look at the symfony-generated classes, they look how we’d expect:
abstract class BaseChicken extends FarmAnimal
{
public function setTableDefinition()
{
parent::setTableDefinition();
$this->setTableName('chicken');
$this->hasColumn('egg_color', 'enum', null, array(
'type' => 'enum',
'values' =>
array(
0 => 'brown',
1 => 'white',
),
));
}
public function setUp()
{
parent::setUp();
}
}
abstract class BaseFarmAnimal extends sfDoctrineRecord
{
public function setTableDefinition()
{
$this->setTableName('farm_animal');
$this->hasColumn('name', 'string', 20, array(
'type' => 'string',
'length' => '20',
));
$this->hasColumn('sound', 'string', 20, array(
'type' => 'string',
'length' => '20',
));
$this->hasColumn('leg_count', 'integer', null, array(
'type' => 'integer',
));
$this->hasColumn('created_at', 'timestamp', null, array(
'type' => 'timestamp',
));
$this->hasColumn('updated_at', 'timestamp', null, array(
'type' => 'timestamp',
));
}
public function setUp()
{
parent::setUp();
}
}
Column Aggregation
To be honest, I’m pretty baffled as to what this one is supposed to do for us. Hopefully this example will make things clearer.
FarmAnimal: columns: name: string(20) sound: string(20) leg_count: integer created_at: timestamp updated_at: timestamp Cow: inheritance: extends: FarmAnimal type: column_aggregation keyField: type keyValue: 1 columns: purpose: type: enum values: [beef, dairy] Dog: inheritance: extends: FarmAnimal type: column_aggregation keyField: type keyValue: 2 columns: breed: string(20) Chicken: inheritance: extends: FarmAnimal type: column_aggregation keyField: type keyValue: 3 columns: egg_color: type: enum values: [brown, white]
$ symfony doctrine:build-model $ symfony doctrine:build-sql $ symfony doctrine:insert-sql
mysql> SHOW TABLES;
+-----------------------+
| Tables_in_inheritance |
+-----------------------+
| farm_animal |
+-----------------------+
1 ROW IN SET (0.00 sec)
mysql> DESCRIBE farm_animal;
+------------+-----------------------+------+-----+---------+----------------+
| FIELD | TYPE | NULL | KEY | DEFAULT | Extra |
+------------+-----------------------+------+-----+---------+----------------+
| id | BIGINT(20) | NO | PRI | NULL | AUTO_INCREMENT |
| name | VARCHAR(20) | YES | | NULL | |
| sound | VARCHAR(20) | YES | | NULL | |
| leg_count | BIGINT(20) | YES | | NULL | |
| created_at | datetime | YES | | NULL | |
| updated_at | datetime | YES | | NULL | |
| TYPE | VARCHAR(255) | YES | | NULL | |
| purpose | enum('beef','dairy') | YES | | NULL | |
| breed | VARCHAR(20) | YES | | NULL | |
| egg_color | enum('brown','white') | YES | | NULL | |
+------------+-----------------------+------+-----+---------+----------------+
10 ROWS IN SET (0.01 sec)
Conclusion
In my opinion, Doctrine’s simple inheritance and column aggregation are invalid and concrete is the only way to go. I hope these examples cleared up some confusion for anyone who had as much trouble with these concepts as I did.