Single Table Inheritance


The Doctrine single table inheritance feature maps a number of different entity classes to a single table in your database.

When creating an entity you simple construct the desired class. When you persist the new entity to your data store, Doctrine stores the discriminator (class type identifier) with the rest of the entity’s data.

When retrieving a specific entity from the database Doctrine uses a discriminator to decide which class to construct with the data.

I have recently used the single table inheritance feature to refactor a Price class that used to use a boolean field to determine if the price contained within the entity was inclusive or exclusive of tax.

The previous code that made use of the Price entity looked something like this

$price = $priceRepository->findById(1);

$priceWithTax = null;

if ($price->isTaxIncluded()) {
    $priceWithTax = $price->getPrice();
} else {
    $priceWithTax = $price->getPrice() + $tax;
}

This code is difficult to init test this calling code given the number of routes through it.

However if we use single table inheritance we can refactor out the conditionals for Polymorphism.

Before I started to refactor the functionality the entity’s configuration YAML looked something like this

Slapi\Model\Product\Price\Price:
  type: entity
  table: price
  id:
    id:
      type: integer
      generator:
        strategy: AUTO
  fields:
    price:
      type: float
    taxIncluded:
      type: boolean

I then changed the YAML set up to something similar to this

# Model.Price.Price.yml
Model\Price\Price:
  type: entity
  table: price
  inheritanceType: SINGLE_TABLE
  discriminatorColumn:
    name: type
    type: integer
  discriminatorMap:
    including_tax: IncludingTax
    excluding_tax: ExcludingTax
  id:
    id:
      type: integer
      generator:
        strategy: AUTO
  fields:
    price:
      type: float

# Model.Price.IncludingTax.yml
Model\Price\IncludingTax:
  type: entity

# Model.Price.ExcludingTax.yml
Model\Price\ExcludingTax:
  type: entity

The new configuration gives us the following classes.

class price
{
    /**
     * @var float
     */
    private $price;

    /**
     * @return float
     */
    abstract public function getPrice();
}
class ExcludingTax extends Price
{
    /**
     * @return float
     */
    public function getPrice()
    {
        return $this->price + $tax;
    }
}
class IncludingTax extends Price
{
    /**
     * @return float
     */
    public function getPrice()
    {
         return $this->price;
    }
}

After the refactoring the entity to use single table inheritance to create a PriceIncludingTax and PriceExcludingTax class the code makes use of the price entities now looks something like this

$price = $priceRepository->findById(1);

$priceWithTax =  $price->getPrice();

The code in the second example is much easier to read and has no knowledge of the fact that a price entity return from the price repository could be inclusive or exclusive of tax. The calling code is also a lot easier to unit test as there is now only a single route through it.