28

Currently I have a model class named Post.

class Post extends Eloquent {

    protected $table = 'posts';
    protected $fillable = array('user_id', 'title', 'description', 'views');

    /*
     * Relationships
     */

    public function user()
    {
        return $this->belongsTo('User');
    }

    public function tags()
    {
        return $this->belongsToMany('Tag', 'post_tags');
    }

    public function reactions()
    {
        return $this->hasMany('Reaction');
    }

    public function votes()
    {
        return $this->hasMany('PostVote');
    }

    //Scopes and functions...
}

I would like to divide posts into two different types; articles and questions. I thought the best way to do this is by inheritance, so Article and Question would extend Post. What is the best way to do this and where to start?

Carrie Kendall
  • 11,124
  • 5
  • 61
  • 81
JasonK
  • 5,214
  • 9
  • 33
  • 61
  • 1
    In the database it still would be one table? (and for example a column that defines the type) – lukasgeiter Nov 01 '14 at 18:34
  • Sorry, I was so confused! Adding a `type` column in the posts table is enough since there are no differences between articles and questions. The only thing is that articles have a thumbnail image, but I could leave that column `NULL` for questions (right?). – JasonK Nov 01 '14 at 18:41
  • Yes, if it's only one column that differs, I'd say its totally ok to just leave it null. However if this changes you might want to split it up into multiple tables to reduce unused columns. So now, do you still need help with your question or are you going to work with the Post model alone? – lukasgeiter Nov 01 '14 at 18:56
  • Yes I would love to hear your answer as the only difference might not be the thumbnail column. I need this for further development. – JasonK Nov 01 '14 at 22:04

2 Answers2

74

Before I dig into multi table inheritance I want to lose a few words about single table inheritance. Single table inheritance is the more easy way when it comes to inheritance with db models.
You have multiple models bound to the same table and a type column to distinguish between the different model classes. However the reason you normally want to implement inheritance is because the models have shared properties but also some that are unique to the model.
When using single table inheritance your table looks similar to that at some point:

id   shared_column   question_column   article_column   question_column2   article_column2 etc...
1    Lorem           62                NULL             test               NULL
2    Ipsum           NULL              x                NULL               true

You end up having a lot of NULL values because there are columns not needed for a certain type of model. And with a lot of records this can influence the size of database.

However for some cases it might still be the best solution. Here's a well written Tutorial that shows how to implement it in Laravel in a pretty elegant way.

Multi Table Inheritance

Now lets look at multi table inheritance. With this method you split your single table into multiple ones (well I guess the name gave that kind of away already ;)) We are going to use a technique called Polymorphism

Our schema from the example above would look like this:

posts table:

id   shared_column  postable_id  postable_type
1    Lorem          1            Question
2    Ipsum          1            Article


questions table:

id   question_column   question_column2
1    62                test


articles table:

id   article_column   article_column2
1    x                true

A lot cleaner if you ask me...

The interesting columns here are postable_id and postable_type. The type tells us on which table we will find our "rest" of the model and the id specifies the primary key of the record that belongs to it. Note that the column names could be whatever you want, but it is convention to call it "-able".

Lets take a look at the Eloquent models now.

Post

class Post extends Eloquent {
    // all your existing code
    
    public function postable(){
        return $this->morphTo();
    }
}

Question / Article / every other postable type

class Question extends Post {
    public function post(){
        return $this->morphOne('Post', 'postable');
    }
}

Note that you actually don't have to extend from Post but you can if you maybe have methods that you want to use too. Anyways, the polymorphic relationship will work with or without it.

Ok that's the basic setup. Here's how you can use your new models:

Retrieve all posts

$posts = Post::all();

Retrieve all questions

$questions = Question::all();

Get question columns from a post

$post = Post::find(1);
$question_column2 = $post->postable->question_column2;

Get post properties from question

$question = Question::find(1);
$shared_column = $question->post->shared_column;

Check which type a post is

$post = Post::find(1);
echo 'type: '.get_class($post->postable);
if($post->postable instanceof Question){
    // Looks like we got a question here
}

Create new question

Now bear with me, creating a model is a bit more complicated. If you have to do it at multiple places in your application I suggest you write a reusable function for it.

// create a record in the questions and posts table

$question = new Question();
$question->question_column = 'test';
$question->save();

$post = new Post();
$post->shared_column = 'New Question Post';
$post->save();

// link them together

$question->post()->save($post);

So as you can see, a clean database comes with it's price. It will be a bit more complex to handle your model. However you can put all these extra logic (e.g. that's required to create a model) into a function in your model class and don't worry about it too much.

Also, there's a nice Tutorial for multi table inheritance with laravel too. Maybe it helps ;)

Community
  • 1
  • 1
lukasgeiter
  • 147,337
  • 26
  • 332
  • 270
  • 2
    This is brilliant, clear and more then I could have asked for. Thank you very much! – JasonK Nov 02 '14 at 21:38
  • 11
    You're very welcome. I hope this answer also helps other people in the future :) – lukasgeiter Nov 02 '14 at 21:43
  • I've been looking all over for a multi-table inheritance example. Was so stuck in the symfony style of thinking I didn't realize you could leverage polymorphic relationships to emulate the behavior. Thanks! – Dylan Pierce Nov 17 '14 at 17:51
  • This response inspired me on coding a way to automate and simplify the process of writing an object through many tables, implementing model inheritance by laravel polymorphic relations. I would be glad to hear your opinion about it: https://stackoverflow.com/questions/48445727/laravel-model-inheritance-through-polymorphic-relation – g4b0 Jan 26 '18 at 10:54
  • @lukasgeiter This is nice until you need to delete a Post. Because the relationship is inversed, when you delete a Question, of course the Post will be deleted. But what if we delete the Post? When we delete the Post, whatever associated to it, whether it's Question or Article should be deleted. How do you handle it? – Edward Anthony Oct 18 '18 at 20:48
  • The NULL values issue is possibly less significant nowadays compared to when this answer was written because you can potentially create a JSON field to store attributes specific to content types and support for JSON fields is pretty common now in relational databases. – Matthew Daly Apr 28 '21 at 11:53
8

from Laravel 5.2, Global Scope is available:

class Article extends Post
{
    protected $table = 'posts';

    protected static function boot()
    {
        parent::boot();

        static::addGlobalScope('article', function (Builder $builder) {
            $builder->where('type', 'article');
        });

        static::creating(function ($article) {
            $article->type = 'article' 
        }); 
    }
}

where type = 'article' is added to all query of Article just like SoftDeletes.

>>> App\Article::where('id', 1)->toSql()
=> "select * from `posts` where `id` = ? and `type` = ?"

Actually laravel provide SoftDeletes trait using this feature.

Kazuya Gosho
  • 996
  • 13
  • 14