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 FarmAnimal and the three inheriting classes will be Cow, Dog and Chicken. All FarmAnimals 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

Simple Inheritance

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]

Since we’re using enums, we’ll have to update 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

Now let’s build the model, build the SQL and insert the SQL:

$ symfony doctrine:build-model
$ symfony doctrine:build-sql
$ symfony doctrine:insert-sql

What does this give us in the database?

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)

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 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 FarmAnimals, 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();
 
    }
}

There’s nothing unique to Chicken there. All the properties of all the FarmAnimals 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();
 
    }
}

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:

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]

Let’s run our symfony commands again:

$ symfony doctrine:build-model
$ symfony doctrine:build-sql
$ symfony doctrine:insert-sql

What’s in the database now?

mysql> SHOW TABLES;
+-----------------------+
| Tables_in_inheritance |
+-----------------------+
| chicken               |
| cow                   |
| dog                   |
| farm_animal           |
+-----------------------+
4 rows IN SET (0.00 sec)

That’s encouraging: each class has its own separate table.

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)

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 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();
 
    }
}

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.

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)

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.

6 Responses to “Inheritance with symfony and Doctrine ORM”

  1. nini says:

    Concrete inheritance doesn’t work well with relations between models because of strict foreign keys checks (Mysql/Innodb). So if you have such schema:


    detect_relations: true
    User:
    columns:
    username: string

    Admin:
    inheritance:
    extends: User
    type: concrete

    Moderator:
    inheritance:
    extends: User
    type: concrete

    Document
    columns:
    description: string
    user_id: integer

    then it will be not possible to save a Document, say, for an Administrator, because mysql will try to check if such user exists in User(!) table, not in administrator table. So it why i’m forced to use column aggregation inheritance.

  2. jason says:

    Nini, if there’s a one-to-one relationship between User and Admin, what’s wrong with having Document’s foreign key point to User?

  3. Raphael says:

    Good post!

    I agree with the conclusion that concrete inheritance mode is the best one among the three. But the mileage may vary depending on the specific need and circumstances.

    I wish to add the issue with concrete inheritance which nini has just raised. Even the concrete inheritance mode is not complete enough for the needs of true object-oriented logic: although there exists a top-level table farm_animal, this table will always be empty. The dogs, cows and chicken are exclusively stored in their respective tables, and none of them at all in the base class table. This arrangement is unlucky, as it inhibits browsing and quering through _all_ FarmAnimals, no matter which their (sub-)class is.
    So e.g. it is not possible to search for an animal called “Rosa”, without knowing whether Rosa is a dog or a cow.
    And more importantly, as nini is stating, you can’t setup an association between FarmAnimal and e.g. Farm (“a farm has zero, one or more animals”).

    Doctrine 2.0 may sometimes offer true full inheritance functionality by introducing “table inheritance”. That mode should avoid any redundancy (no same field exists in multiple tables) and then have the base class’ table store its part of each object, now allowing queries to be made on FarmAnimal level.

  4. Kev says:

    The key advantage I found with using the column aggregation is that you can use dql to query the parent table and then get back the child objects. This will hopefully come in real handy on a project I’m working on where there’s a public timeline of actions and when I query get the last 20 actions I only need to query one table and every entry in the timeline can have it’s own individual ID

  5. Prasad says:

    Agree with Kev.
    Concrete Inheritance is used primarily for different entities of the same parent class

    example 1: types of ‘people’: user, agent, admin, supplier contact, customer contact, employee

    example 2: options (name, value): gender, phone-type, account types, dept codes

  6. David says:

    What I’d like to find is a method of having the objects so that the common values are in the parent table and only the sub-class specific columns are in a separate table (that is handled auto-magically, as that is what I am doing by hand currently).

    cow (purpose) -> farm_animal (name, sound, leg_count)
    milk_cow (quantity) -> cow (purpose) -> farm_animal

Leave a Reply