Single Table Inheritance In Laravel 4

Today I needed to implement single table inheritance with Laravel 4.

Single table inheritance, in a nutshell, is where you have a hierarchy of classes stored in a single table. It’s a great pattern in a situation where most of the fields of the classes are the same but only a couple are different in the child classes.

In my case I have a base Contact class with two descendant classes, Customer and Vendor.

Contact contains the company name, contact name, addresses, phone numbers, etc. Customer extends Contact with fields like sales_tax_id where Vendor extends Contact with fields like terms_id.

With single table inheritance this all ends up in the contacts table like so:

id class_name …(common fields omitted)… sales_tax_id terms_id
1 Customer 1 NULL
2 Vendor NULL 1

So how to automatically persist the Eloquent models with class_name getting set properly? And how to automatically retrieve them in a manner that they are restricted to the proper class? For example, a call to Customer::find(2) should return NULL, not the Vendor record.

There are 4 things that need to be addressed:

  1. The table name for all descendant classes has to be the same as the table name for the base class.
  2. The class_name attribute for all classes has to be properly set.
  3. When retrieving records, the query must restrict them to records matching the class name of the class being retrieved with one exception: when the class being retrieved is the base class. Then we want them all.
  4. When retrieving records using the base class the object returned must be the class in the object’s class_name attribute.

1. The same table name for all classes in the heirarchy.

This is dead simple. All we have to do is add:

class Contact extends Eloquent {
  protected $table = 'contacts';
}

to the base class.

2. The class_name attribute must be properly set.

Also pretty simple. Override __construct in the base class Contact.

class Contact extends Eloquent {

  protected $table = 'contacts';

  public function __construct($attributes = array())
  {
    parent::__construct($attributes);
    $this->setAttribute('class_name',get_class($this));
  }
}

Now the class_name field is set when the class is constructed.

3. Retrieved records need to be restricted to class_name

A little trickier, this. I found a great gist by Maxime Lafontaine that helped me figure this one out.

It goes like this. In the base Illuminate\Database\Eloquent\Model class, the function newQuery() returns the query builder object when a new query is being built. Here’s how we use that to restrict the records to class_name.

class Contact extends Eloquent {

  protected $table = 'contacts';

  public function __construct($attributes = array())
  {
    parent::__construct($attributes);
    $this->setAttribute('class_name',get_class($this));
  }

  public function newQuery($excludeDeleted = true)
  {
    $builder = parent::newQuery($excludeDeleted);
    // If I am using STI, and I am not the base class,
    // then filter on the class name.
    if ('Contact' !== get_class($this)) {
        $builder->where('class_name',"=",get_class($this));
    }
    return $builder;
  }
}

Now, using the above data as an example, Customer::find(1) will return a Customer model, and Customer::find(2) will return NULL.

Ensuring an object of the proper class is returned.

Right now, if we call Contact::find(1) it will return a Contact object. What we really need is a Customer.

So… the solution is found in the newFromBuilder() function in Illuminate\Database\Eloquent\Model.

That function needs to be overridden with custom logic to return the proper class.

class Contact extends Eloquent {

  protected $table = 'contacts';

  public function __construct($attributes = array())
  {
    parent::__construct($attributes);
    $this->setAttribute('class_name',get_class($this));
  }

  public function newQuery($excludeDeleted = true)
  {
    $builder = parent::newQuery($excludeDeleted);
    // If I am using STI, and I am not the base class, 
    // then filter on the class name.
    if ('Contact' !== get_class($this)) {
        $builder->where("class_name","=",get_class($this));
    }
    return $builder;
  }

  public function newFromBuilder($attributes = array())
  {
    if ($attributes->class_name) {
      $class = $attributes->class_name;
      $instance = new $class;
      $instance->exists = true;
      $instance->setRawAttributes((array) $attributes, true);
      return $instance;
    } else {
      return parent::newFromBuilder($attributes);
    }
  }
}

Now calling Contact::find(1) will return an instance of Customer.

Abstracting this pattern into your whole application

OK, with me so far? This is great for my Contact - Customer - Vendor situation, but what if I wanted to use this anywhere in my app? Maybe I also have a Product - Shoe - Jacket thing coming up?

Here’s how to do it. The trick is to create a base model for the whole app with just a bit of additional logic.

class BaseModel extends Eloquent {

  public function __construct($attributes = array())
  {
    parent::__construct($attributes);
    if ($this->useSti()) {
      $this->setAttribute($this->stiClassField,get_class($this));
    }
  }

  private function useSti() {
    return ($this->stiClassField && $this->stiBaseClass);
  }

  public function newQuery($excludeDeleted = true)
  {
    $builder = parent::newQuery($excludeDeleted);
    // If I am using STI, and I am not the base class, 
    // then filter on the class name.    
    if ($this->useSti() && get_class(new $this->stiBaseClass) !== get_class($this)) {
        $builder->where($this->stiClassField,"=",get_class($this));
    }
    return $builder;
  }

  public function newFromBuilder($attributes = array())
  {
    if ($this->useSti() && $attributes->{$this->stiClassField}) {
      $class = $attributes->{$this->stiClassField};
      $instance = new $class;
      $instance->exists = true;
      $instance->setRawAttributes((array) $attributes, true);
      return $instance;
    } else {
      return parent::newFromBuilder($attributes);
    }
  }
}

And that’s it. I swear, it’s almost too easy.

Now if I want to do the Product - Shoe - Jacket thing, I do the following. First, add class_name to the products table.

$table->string('class_name')->index(); // Do index this field!

Then, the model classes are as follows:

class Product extends BaseModel {

  protected $table = 'products';

  protected $stiClassField = 'class_name';
  protected $stiBaseClass = 'Product';

  ...

}
class Shoe extends Product {

}
class Jacket extends Product {

}

Now the product table will automagically have the class_name field populated with Shoe or Jacket depending on the class, and retrieval will be scoped. Retrieving a Product directly, however, does not scope and returns the proper class (Shoe or Jacket, depending.) This happens because of the existence of the $stiClassField and $stiBaseClass properties, and if they are not declared on a class, the STI logic is ignored (see the useSti() function).

Wow! Gotta love Laravel 4. Thanks Taylor and team!


Comments

Single Table Inheritance In Laravel 4 — 2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *