skip to Main Content

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 foo(){
        do_abc();
    }
}
class ItemB extends Item implements SubItem {
    public function foo(){
        do_def();
    }
}
class ItemC extends Item implements SubItem {
    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 function. There is a type column which indicates what the model is.

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

2

Answers


  1. If you have a column indicating the type of model in it should be, try using the newFromBuilder method to create a new instance of the Item as the correct type. I am not sure which field specifies the model type in your case, but I will assume it is type:

    class Item extends Model {
        protected $table = 'items';
    
        protected static $typeMapping = [
            'item_a' => ItemA::class,
            'item_b' => ItemB::class,
            'item_c' => ItemC::class,
        ];
    
        // Override the newFromBuilder method
        public function newFromBuilder($attributes = [], $connection = null)
        {
            // Map the 'type' value to a specific class
            $class = static::$typeMapping[$attributes->type] ?? self::class;
    
            return (new $class)->newInstance((array) $attributes, true);
        }
    }
    
    Login or Signup to reply.
  2. i would try this quickly:

    class Owner {
        public function items($className) {
            return $this->hasMany($className);
        }
    }
    

    and then the client would call it like this:

    $shouldBeInstanceOfItemA= Owner::items(ItemA::class)->first();
    

    and now the correct foo method could be called on it, as an alternative as mentioned in the comment of the original post the state pattern should do it.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search