Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Improved Polymorphic Eloquent Builder Laravel Package

rapidwebltd/improved-polymorphic-eloquent-builder

Improve polymorphic relationship querying in Laravel with a cleaner Eloquent builder API. This package helps you filter and chain queries across morph types more expressively, reducing boilerplate when working with polymorphic models.

View on GitHub
Deep Wiki
Context7

Getting Started

This package targets the era before Laravel 5.8 shipped whereHasMorph() / whereMorph() natively. It exists to make morphTo() relationships queryable — i.e. filtering a model by the type and attributes of its polymorphic parent without hand-writing UNION queries or type switches. Given the last release is from January 2018, treat it as a legacy-Laravel tool and confirm the installed Laravel version before adopting it.

Install and orient yourself

composer require rapidwebltd/improved-polymorphic-eloquent-builder

Look first at:

  1. The README examples — this is where the exact trait name, namespace, and method signatures live.
  2. src/ — usually a small Builder subclass plus a trait that swaps newEloquentBuilder().
  3. tests/ — the fastest way to see supported query shapes and the Laravel version the suite asserts against.

Minimal setup — add the provided trait to the model that owns the morphTo() relation (the "child" side), so its Eloquent builder is the improved one:

use RapidWeb\ImprovedPolymorphicEloquentBuilder\HasImprovedPolymorphicRelations;

class Comment extends Model
{
    use HasImprovedPolymorphicRelations;

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

First use case — filter polymorphic children by parent type and parent attributes in one query:

// Comments whose parent is a published Post
$comments = Comment::query()
    ->whereHasMorph('commentable', [Post::class], function ($q) {
        $q->where('published', true);
    })
    ->latest()
    ->paginate(20);

Verify in php artisan tinker that the generated SQL references the correct tables, then check DB::listen() to confirm no per-row queries are being issued.

Implementation Patterns

1. Constrain by morph type, then attributes. The core pattern is "type + closure": pass the list of concrete classes you want to match and constrain each. For heterogeneous lists (e.g. activity feed), pass every allowed type and use a per-type branch inside the closure if attribute names differ.

Activity::whereHasMorph('subjectable', [Post::class, Video::class], function ($q, $type) {
    $q->when($type === Post::class, fn ($q) => $q->where('status', 'published'));
    $q->when($type === Video::class, fn ($q) => $q->where('encoded', true));
})->get();

2. Eager load the polymorphic parent with constraints. Combine the query with with() to avoid N+1, keeping constraints aligned with the filter so you do not load rows you filtered out:

Comment::whereHasMorph('commentable', [Post::class], fn ($q) => $q->where('published', true))
    ->with(['commentable' => fn ($morphTo) => $morphTo->morphWith([
        Post::class => ['author'],
    ])])
    ->get();

3. Encapsulate as reusable scopes. Put type lists in a model method or a dedicated query object so callers do not repeat class strings:

public function scopeOnPublishedContent(Builder $query): Builder
{
    return $query->whereHasMorph(
        'commentable',
        [Post::class, Article::class],
        fn ($q) => $q->where('published', true)
    );
}

4. Layer with normal builder methods. The improved builder should return a standard Illuminate\Database\Eloquent\Builder, so where, orWhere, orderBy, paginate and withCount continue to work. Use withCount on relationship counts rather than loading collections when only the number is needed.

5. Admin / search screens. This is where the package pays off day-to-day: a single "type" dropdown feeding whereHasMorph filters, plus eager loading for the index grid. Build the type list from a whitelist — never from user input — because the morph map is resolved via class names.

6. Testing. Prefer RefreshDatabase plus SQLite in-memory for speed, but also run at least one integration test against MySQL/Postgres, since type/class resolution and column names behave differently across drivers.

7. Migration off the package. When you upgrade to Laravel 5.8+, replace package calls with the native whereHasMorph / whereMorph and drop the trait. Keep the scope methods — they become a one-line change for callers.

Gotchas and Tips

  • Laravel version drift is the biggest risk. whereHasMorph is native from 5.8 onward; on 5.8+ this package is redundant and may conflict. Pin the package version and document the Laravel constraint in composer.json. If you are on Laravel 5.5/5.6/5.7, this is the main reason to consider it.
  • Trait vs custom builder conflicts. The package works by overriding the model's newEloquentBuilder(). If another package (or your own base model) also overrides it, only one wins — last trait applied typically. Verify with get_class(Comment::query()) in tinker.
  • Class names are resolved literally. Passing a morph alias requires the morph map to be registered (Relation::morphMap([...])). Passing an unregistered class while a morph map is active will match nothing silently.
  • Global scopes are not automatically applied to morph types. Soft-deleted parents, tenant scopes, and published scopes must be applied inside the closure or via ->withoutGlobalScopes() deliberately. Always test with a soft-deleted parent present.
  • Ambiguous columns. If the parent and child tables share column names (id, created_at, status), qualify columns inside closures (->where('posts.status', ...)) or you will get "ambiguous column" errors on MySQL.
  • N+1 creep. The filter does not imply an eager load. Pair every filtered morph query used in a loop/response with with() or morphWith(), and assert query counts in tests using DB::enableQueryLog().
  • Debugging. Dump ->toSql() plus ->getBindings() first, then inspect with DB::listen() to catch silent extra queries. If results are wrong but SQL looks right, check the morph map and the concrete class's $table.
  • Extension points. If the builder API is missing something, subclass the package's builder and return your subclass from newEloquentBuilder(), or register a Builder::macro() — cheaper than forking. Keep a short internal README recording the macro so upgrades are tractable.
  • Maintenance posture. Low star count, last release 2018, LGPL-3.0. Vendor a fork or pin exact versions; do not depend on future upstream fixes. Budget for removal during the next Laravel upgrade — in most codebases the right long-term move is to use the native whereHasMorph and keep only the scopes you built around it.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core