Class Table Inheritance


The Class Table Inheritance feature of Doctrine allows you to have a number of different entities similar to Single Table Inheritance. The difference with Class Table Inheritance is it allows you to split the data specific to an entity in to a separate table. This allows each entity to have extra data specific to its needs, without having a table with a large number of null-able columns.

When creating a new entity you simply instantiate the class for the specific type required. When you persist the entity Doctrine creates an entry in the base table with the discriminator of your chosen class, Doctrine will also create an entry in the class specific table with a foreign key linking back to the base table.

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

If I were implement an Order Item system using Class Table Inheritance, I would create an abstract class and base table that could handle all of the generic Order Item functionality and data. Creating a concrete implementations for each specific Order Items requirements.

In the example configuration I have only created two concrete implementations one for products and one for vouchers.

# Model.Order.OrderItem.yml
Model\Order\OrderItem:
  type: entity
  inheritanceType: JOINED
  discriminatorColumn:
    name: type
    type: string
  discriminatorMap:
    product_order_item: ProductOrderItem
    voucher_order_item: VoucherOrderItem
  table: order_item
  id:
    id:
      type: integer
      generator:
        strategy: TODO
  fields:
    createdAt:
        type: datetime
  manyToOne:
    order:
      targetEntity: Model\Order\Order
      inversedBy:   orderItems
      joinColumn:
        name:                 order_id
        referencedColumnName: id
        
# Model.Order.ProductOrderItem.yml
Model\Order\ProductOrderItem:
  type: entity
  fields:
    sku:
      type:     string
      nullable: false
    quantity:
      type:     integer
      nullable: false
      
# Model.Order.VoucherOrderItem.yml
Model\Order\VoucherOrderItem:
  type: entity
  fields:
    voucherCode:
      type:     string
      nullable: false
    discountAmount:
      type:     float
      nullable: false

With the above configuration I would then create the classes below.

An abstract OrderItem that handles the link between OrderItems and their Orders and also maintains the time that an OrderItem was created.

class OrderItem
{

    /**
     * @var integer
     */
    private $id;

    /**
     * @var Order
     */
    private $order;

    /**
     * @var \DateTime
     */
    private $createdAt;

    public function __construct(Order $order)
    {
        $this->order     = $order;
        $this->createdAt = new \DateTime();
    }
}

The concrete ProductOrderItem extends the OrderItem with functionality specific to Products within an Order. Storing and validating the products Sku and required quantity.

class ProductOrderItem extends OrderItem
{

    /**
     * @var string
     */
    private $sku;

    /**
     * @var integer
     */
    private $quantity;

    public function setSku($sku) { ... }
    public function getSku() { ... }

    public function setQuantity($quantity) { ... }
    public function getQunatity() { ... }
}

The concrete VoucherOrderItem extends the OrderItem with functionality specific to Vouchers within an Order. storing and validating the voucher code and discount amount.

class VoucherOrderItem extends OrderItem
{

    /**
     * @var string
     */
    private $voucherCode;

    /**
     * @var float
     */
    private $discountAmount

    public function setVoucherCode($voucherCode) { ... }
    public function getVOcuherCode() { ... }


    public function setDiscountAmount($discountAmount) { ... }
    public function getDiscountAmount() { ... }
}

This implementation of the OrderItems maintains a clean database without any unecessary null-able columns. The implementation also conforms to the Single Responsibility Principle as a concrete OrderItem class only needs to be edited if the requirements of that specific type of OrderItem change. If I needed to create a new OrderItem for employee discount, I would create and configure a new EmployeeDiscountOrderItem entity in Doctrine without having to touch any existing OrderItem classes.