$ lexprog.com

// notes from an old coder -- php, databases, and the occasional rant

[September 03, 2024] Eloquent ORM

Eloquent Polymorphic Relations

Eloquent Polymorphic Relations: Tips & Tricks

────────────────────────────────────────────────────────

Eloquent Polymorphic Relations: Tips & Tricks

Tip: One Image Model for Multiple Parents

class Image extends Model
{
    public function imageable(): MorphTo
    {
        return $this->morphTo();
    }
}

class Post extends Model
{
    public function image(): MorphOne
    {
        return $this->morphOne(Image::class, 'imageable');
    }
}

Gotcha: No Foreign Key Constraints

Polymorphic relations can't use database foreign keys. If the parent is deleted, the child's reference becomes orphaned.

Tip: Morph Map for Cleaner Type Names

Relation::enforceMorphMap([
    'post' => Post::class,
    'video' => Video::class,
]);

Instead of storing full class names, stores 'post' and 'video'.

Gotcha: morphTo() Needs Both Columns

$table->morphs('imageable');
// Creates: imageable_id (unsigned big int) + imageable_type (string)

Tip: Polymorphic Many-to-Many

class Tag extends Model
{
    public function posts(): MorphToMany
    {
        return $this->morphedByMany(Post::class, 'taggable');
    }

    public function videos(): MorphToMany
    {
        return $this->morphedByMany(Video::class, 'taggable');
    }
}

Gotcha: Querying Polymorphic Relations

Image::where('imageable_type', Post::class)->get();

If you use a morph map, query with the map key instead.

Tip: Polymorphic Comments

class Comment extends Model
{
    public function commentable(): MorphTo
    {
        return $this->morphTo();
    }
}

// Works for posts, videos, pages, etc.

Tip: Use cursor() for Memory-Neutral Iteration

When exporting 100K rows, get() loads everything into memory. cursor() uses yield and keeps memory flat regardless of row count. Perfect for artisan commands.

Tip: whereHas() vs load() — Two Different Things

whereHas() filters the parent query by relationship existence. load() eager-loads relationships AFTER the query. Mixing them up is a common source of logic bugs.

Gotcha: withCount() Adds a Subquery

withCount('comments') runs a correlated subquery on every row. On large tables, this can be slower than a separate query. Profile before relying on it.

Senior Insight

Polymorphic relationships are Eloquent's most elegant footgun. They look clean in code but create databases with nullable foreign key columns and no referential integrity. I've debugged orphaned records because a parent model was deleted but the polymorphic children weren't cleaned up. If you use polymorphic relations, implement model events or observer cleanup, and always add database-level indexes on the type and id columns — queries without them are full table scans.

Source: Laravel Docs (https://laravel.com/docs/eloquent), Laravel News (https://laravel-news.com/), Freek.dev (https://freek.dev/tags/eloquent)

────────────────────────────────────────────────────────
<-- back to posts