Laravel Eloquent parent with multiple polymorphic children

  Kiến thức lập trình

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?

3

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);

}

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’

class ShapeContainer
{
    public function shapes()
    {
        return $this->hasMany(Shape::class);
    }
}
//table fields
//  - shape_container_id

class Shape extends Model
{
    public function newFromBuilder($attributes = [], $connection = null): static
    {
        //NamespaceCircle
        $class = $attributes->type;
        $model = new $class();

        $model = $model->newInstance([], true);

        $model->setRawAttributes((array) $attributes, true);

        $model->fireModelEvent('retrieved', false);

        return $model;
    }

    public function newQuery(): Builder
    {
        
       // Circle::query will retrieved only circles
        return parent::newQuery()->where('type', static::class);  //NamespaceCircle
    }

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

    public function getArea()
    {
        //
    }
}
class Circle extends Shape
{
    public function getArea()
    {
        //
    }
}

no need to use polymorphic relations if all shapes is a same table

2

Theme wordpress giá rẻ Theme wordpress giá rẻ Thiết kế website Kho Theme wordpress Kho Theme WP Theme WP

LEAVE A COMMENT