Search code examples
laraveleloquenteloquent-relationship

Laravel relationships with extended models


I've got a requirement for a set of models with the same database table, but they need to implement certain methods in a different way. Given a set of classes like this:

class Item {
    protected $table = 'items';
}
interface SubItem {
    public function foo();
}
class ItemA extends Item implements SubItem {
    public function __construct($attributes) {
        $attributes['type'] = 'ItemA'; 
        parent::__construct($attributes);
    }
    public function foo(){
        do_abc();
    }
}
class ItemB extends Item implements SubItem {
    public function __construct($attributes) {
        $attributes['type'] = 'ItemB'; 
        parent::__construct($attributes);
    }
    public function foo(){
        do_def();
    }
}
class ItemC extends Item implements SubItem {
    public function __construct($attributes) {
        $attributes['type'] = 'ItemC'; 
        parent::__construct($attributes);
    }
    public function foo(){
        do_ghi();
    }
}
class Owner {
    public function items() {
        return $this->hasMany(Item::class);
    }
}

All works fine until I try to fetch the items() relationship. The resulting instances are cast to the Item class, and I'm unable to call the foo() function.

It seems like I should be able to declare Item as abstract and use the type column to have the relationship return a collection of ItemA, ItemB, and ItemC instances but this is clearly not the case. I may want to add an arbitrary number of additional child classes in the future. Does Laravel have a way to make this work?

Alternatives considered:

  • Polymorphic relationships, which don't seem to address this situation
  • Add the method to the Item class and run different code based on the instance type, which is very messy
  • Add relationships for every type and have a 'fake' relationship method collect them all behind the scenes, which is slightly less messy but doesn’t seem possible

Solution

  • One of the answers gave me the idea of overriding the model's methods; I came up with this, which is fairly minimal and works well:

    class Item extends Model
    {
        public function __construct($attributes = [])
        {
            $attributes['type'] = static::class;
            parent::__construct($attributes);
        }
        
        public function newFromBuilder($attributes = [], $connection = null): Model
        {
            $model = parent::newFromBuilder($attributes, $connection);
        
            return class_exists($model->type) && !$model instanceof $model->type
                ? (new $model->type)->newFromBuilder($model->getAttributes(), $connection)
                : $model;
        }
    }
    

    This allows any number of additional models to be added without having to touch the base class again.