masterfri/laravel-smart-relations
Laravel Smart Relations streamlines Eloquent relationship management in Laravel with smarter helpers and conventions for defining, querying, and working with model relations, reducing boilerplate and keeping model code cleaner and easier to maintain.
composer require <vendor>/<package>, then php artisan vendor:publish --provider="<Vendor>\<Package>\<Package>ServiceProvider" (config + migrations). Run php artisan migrate before touching the API.config/<package>.php — every behavior toggle lives here; skim keys and defaults before writing code.// app/Models/Post.php and app/Models/Video.php
use Vendor\Package\Concerns\HasAttachments;
class Post extends Model
{
use HasAttachments;
}
// Tinker / first smoke test
$post = Post::factory()->create();
$video = Video::factory()->create();
$post->attach($file); // or whatever the package's write API is
$video->attach($file);
Post::with('attachments')->first(); // must not lazy-load per row
Enforce the morph map immediately. Polymorphic support is only stable if you register aliases before any rows exist. Do this in a service provider, not in a model:
use Illuminate\Database\Eloquent\Relations\Relation;
public function boot(): void
{
Relation::enforceMorphMap([
'post' => \App\Models\Post::class,
'video' => \App\Models\Video::class,
]);
}
With enforceMorphMap, a missing alias throws instead of silently writing a FQCN into the database.
Migrations — pick the right morph helper. v1.2 relations require the composite (type, id) pair to be indexed; Laravel's helpers do this for you, and the choice matters:
$table->morphs('attachable'); // unsignedBigInteger + index (default)
$table->nullableMorphs('attachable'); // when the child can be orphaned
$table->uuidMorphs('attachable'); // UUID parents
$table->ulidMorphs('attachable'); // ULID parents
If your parent keys are uuid/ulid, the package's default migration will produce a type mismatch — swap the helper before running migrations in any environment you care about.
Eager-load the polymorphic side explicitly. morphTo cannot be solved in one query; it needs one query per distinct type. Batch it at the controller level:
$items = Attachment::query()
->with('attachable')
->latest()
->paginate(50);
Constrain on the type, not the class, when filtering:
Attachment::query()
->whereHasMorph('attachable', [Post::class], fn ($q) => $q->published())
->get();
Passing a narrowed array of types to whereHasMorph is the difference between two targeted queries and a full scan.
Wrap writes in a transaction when a parent and its polymorphic children are created together. Morph rows have no FK constraint, so a failed child insert leaves a half-populated parent with no database-level complaint.
Policy/authorization pattern: resolve authorization through the parent, not the child. A AttachmentPolicy@view that delegates to $attachment->attachable keeps rules in one place and survives new parent types being added.
Testing: one factory per morph type plus a shared trait test that asserts the relation round-trips. A polymorphic feature that works for Post and breaks for Video is the most common regression class in packages with this shape.
post → App\Models\Post is a one-way door; renaming the alias orphans every existing row. Treat the map in the provider as a schema.constrained(). Deleting a parent silently leaves orphan children. Add a scheduled prune job or model deleting hooks — the package won't do it for you.morphTo is always N+1 when lazy loaded, and Laravel won't warn you. Model::preventLazyLoading() in AppServiceProvider::boot() (non-production) catches this early; treat the resulting exception as a real defect.$morphClass / getMorphClass() overrides are the escape hatch when you must rename a class without rewriting rows — but they interact badly with enforceMorphMap if you forget one. Keep the alias identical to the map key.whereHasMorph on a nullable relation excludes rows where the morph pair is null; use orWhereDoesntHaveMorph (or explicit orWhereNull) to keep them.$child->attachable unless you handle it; withTrashed()/withoutGlobalScopes() on the relation is usually needed.(type, id). If you mostly query "all children of this parent", add a composite index on (attachable_type, attachable_id, created_at) for paginated listings.Relation::morphMap(), and log the pair with $child->getAttributes()['attachable_type']. A stale config cache (php artisan config:clear) is a frequent cause of "the map isn't taking effect" — the morph map is registered in a provider, but a cached config bootstraps the provider before your map is applied in some setups.How can I help you explore Laravel packages today?