skip to Main Content

I’ve been trying to model a relationship in Laravel between a ShapeContainer entity and different shapes with unique properties, but a common Shape interface (with getArea and getPerimeter methods). I’ve read up on the examples in the laravel documentation (posts, videos, comments), but in that example the child entity (Comment) can belong to different types of parents. In my case I want the parent entity (ShapeContainer) to be able to handle different types of children (Circle, Rectangle etc).

The end result I’m kind of looking for is something like this:

$shapeContainer = ShapeContainer::find(1);
$shapes = $shapeContainer->shapes()

foreach($shapes as $shape) {
    $area = $shape->getArea();
}

The way I managed to achieve with this right now is with adding an entity in between, something like a pivot

class Shape extends Model
{

    public function shapeable()
    {
        return $this->morphTo('shapeable');
    }

    public function shapeConatiner()
    {
        return $this->belongsTo(ShapeContainer::class);
    }
}

with the corresponding table fields

shape_container_id # Reference to the shape container
shapeable_type # square/circle
shapeble_id # Reference to the ID of the corresponding shape model.

so in code I can do

$shapeContainer = ShapeContainer::find(1);
$shapes = $shapeContainer->shapes()

foreach($shapes as $shape) {
    $area = $shape->shapable->getArea();
}

I’m wondering is this the way to go, since I’ve not recognized anything similar to my use case in the laravel documentation and it seems clunky to have a whole model inbetween just to explain the relationship. Is there a possibility to keep that pivot table, but simplify the interface so that developers using the model won’t have to worry about or see the shapeable() pivot?

2

Answers


  1. It sounds like you need to cast the model to its instant type when it is created, then each instance can have its own version of getArea

    try to implement something like this inside Shape:

    // Override the newFromBuilder method
    public function newFromBuilder($attributes = [], $connection = null) {
            
       $class = $attributes-> shapeable_type ?? self::class;
    
       if (class_exists($class)) {
           $model = (new $class)->newInstance((array) $attributes, true);
           $model->fireModelEvent('retrieved', false);
           $model->setRawAttributes((array) $attributes);
           return $model;
       } 
       // If the class doesn't exist, fallback to the parent
       return parent::newFromBuilder($attributes, $connection);
    
    }
    
    Login or Signup to reply.
  2. I think you can achieve what you need with a polymorphic many to many relation, and an inverse belongs to relation, since a container can have many shapes and one shape can belong to only one container, documented here: https://laravel.com/docs/11.x/eloquent-relationships#many-to-many-polymorphic-relations

    An example implementation for your case:

    class Circle extends Model
    {
        public function container(): BelongsTo
        {
            return $this->belongsTo(ShapeContainer::class);
        }
    }
    
    class Rectangle extends Model
    {
        public function container(): BelongsTo
        {
            return $this->belongsTo(ShapeContainer::class);
        }
    }
    
    class ShapeContainer extends Model
    {
        public function circles(): MorphToMany
        {
            return $this->morphedByMany(Circle::class, 'shapeable');
        }
    
        public function rectangles(): MorphToMany
        {
            return $this->morphedByMany(Rectangle::class, 'shapeable');
        }
    
        /**
         * @return Collection<Circle|Rectangle>
         */
        public function allShapes()
        {
            $shapes = collect();
    
            $shapeRelations = collect(
                (new ReflectionClass(static::class))->getMethods(ReflectionMethod::IS_PUBLIC)
            )->filter(function (ReflectionMethod $method) {
                return $method->getNumberOfParameters() == 0 &&
                    optional($method->getReturnType())->getName() == MorphToMany::class;
            })->map(fn (ReflectionMethod $method) => $method->getName());
    
            foreach ($shapeRelations as $relation) {
                if ($this->relationLoaded($relation)) { // get only loaded relations, but you can add logic to load missing ones
                    $shapes->put($relation, $this->{$relation});
                }
            }
    
            return $shapes;
        }
    }
    

    This uses the same pivot table you have, but it does not require a model for it; but for the inverse relation it uses a shape_container_id of each child shape, ‘circles’, ‘rectangles’

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