What is Single Table Inheritance?
I saw a post by Kapil Verma where he shared his STI base class. I cleaned it up a bit and it seems to be running solidly. There may be omissions. But, I'll update it as I use it.
abstract class SingleTableInheritanceEntity extends Eloquent
{
// the field that stores the subclass
protected $subclassField = null;
// must be overridden and set to true in subclasses
protected $isSubclass = false;
public function isSubclass()
{
return $this->isSubclass;
}
// if no subclass is defined, function as normal
public function mapData(array $attributes)
{
if ( ! $this->subclassField) {
return $this->newInstance();
}
return new $attributes[$this->subclassField];
}
// instead of using $this->newInstance(), call
// newInstance() on the object from mapData
public function newFromBuilder($attributes = array())
{
$instance = $this->mapData((array) $attributes)->newInstance(array(), true);
$instance->setRawAttributes((array) $attributes, true);
return $instance;
}
public function newQuery($excludeDeleted = true)
{
// If using Laravel 4.0.x then use the following commented version of this command
// $builder = new Builder($this->newBaseQueryBuilder());
// newEloquentBuilder() was added in 4.1
$builder = $this->newEloquentBuilder($this->newBaseQueryBuilder());
// Once we have the query builders, we will set the model instances so the
// builder can easily access any information it may need from the model
// while it is constructing and executing various queries against it.
$builder->setModel($this)->with($this->with);
if ($excludeDeleted && $this->softDelete) {
$builder->whereNull($this->getQualifiedDeletedAtColumn());
}
if ($this->subclassField && $this->isSubclass()) {
$builder->where($this->subclassField, '=', get_class($this));
}
return $builder;
}
// ensure that the subclass field is assigned on save
public function save(array $options = array())
{
if ($this->subclassField) {
$this->attributes[$this->subclassField] = get_class($this);
}
return parent::save($options);
}
}
Then, let's imagine that you have a notifications
database table. It has a few fields, id
, user_id
, class
, subject_type
, subject_id
. Subject is a polymorphic relation to the item that the notification is for. Then, we have a class ForumTag
so that if a user is tagged no the forum, it creates this sort of notification.
class Notification extends SingleTableInheritanceEntity
{
protected $table = 'notifications';
protected $subclassField = 'class';
public function setSubject(Model $object)
{
$this->attributes['subject_type'] = get_class($object);
$this->attributes['subject_id'] = $object->getKey();
}
}
class ForumTag extends Notification
{
protected $isSubclass = true;
}
$notification = new ForumTag;
$notification->setSubject($forumThread);
$notification->save();
The resulting collection here will include ForumTag classes.
$notifications = Notification::where('user_id', '=', $userId)->get();
ForumTag
notificatinos$notifications = ForumTag::where('user_id', '=', $userId)->get();
Single Table Inheritance should only be used when different types have the exact same fields, but need different behavior. You never want to end up adding new nullable fields just for the needs of a specific type.
More Information on When and Why to use Single Table Inheritance
very interesting, I'm going to implement it as well
I've changed something
public function save(array $options = array())
{
if ($this->subclassField) {
$this->attributes[$this->subclassField] = get_class($this);
}
return parent::save($options); // return boolean
}
In my project I check if the model is saved buy doing something like
$image = $this->image->newInstance();
$image->name = 'test image';
return $image->save() ? $image : null;
By returning parent::save($options)
I can check if the save went ok
I agree that the parent::save() should be returned. This is something that I've since implemented as well. I've updated the original post to reflect the change.
This is a really neat idea. With a side project I'm working on I might look at using this approach. I'll have a whole bunch of incidents, such as fire, car accident, flood, etc. Single Table Inheritance should make this a whole lot easier.
I'm looking into using STI for my users table. I'm using Sentry and have 9 different user groups, and my User model is becoming obscene with all logic required for different types of users. This way I can have a base User class, and then either have a class for each user group, or a few different classes to represent similar user groups.
Regarding adding nullable columns: I completely agree. Although, if your data starts to change over time (i.e. in the case of different user groups, maybe new fields are required only for a subset of the groups) you can split it up into another table (i.e. admin_user_profile, member_user_profile).
Seems to be working well. I just wanted to share one change I made:
public function newQuery($excludeDeleted = true)
{
$builder = parent::newQuery($excludeDeleted);
// if this is a subclass, add the subclass field to the query
if ($this->subclassField && $this->isSubclass()) {
$builder->where($this->subclassField, '=', get_class($this));
}
return $builder;
}
It looked like the only change to the newQuery function was adding in the where clause for the subclassField, so I just have it call the parent method, and then customize the returned builder and return it. This also helps remove the L4.0/L4.1 check mentioned in the comments.
Please let me know if anyone sees any issues with this.
Sorry to resurrect an old thread; I wanted to mention that I found this very helpful, but in t removed the whole $isSubclass approach, and replaced the function with the following:
public function isSubclass()
{
return (get_parent_class($this) != self::class && is_subclass_of($this,self::class));
}
In my testing so far, this seems to work well regardless of what I name the abstract class, and doesn't require a specific variable in each of my subclass models.
That said, let me know if there are issues with this approach that I may not have encountered yet.
ryanabrams change works just fine, in PHP <= 5.4 you have to use CLASS instead of self::class though.
public function isSubclass()
{
return (
get_parent_class($this) != __CLASS__
&& is_subclass_of($this, __CLASS__)
);
}
Sign in to participate in this thread!
The Laravel portal for problem solving, knowledge sharing and community building.
The community