davidwesdijk/laravel-polymorphic-eav
Add flexible, polymorphic EAV attributes to any Laravel model. Store custom fields as attribute-value pairs without schema changes, attach them to multiple models, and query them with familiar Eloquent syntax. Great for dynamic product or user metadata.
Install the package with Composer and publish its assets:
composer require davidwesdijk/laravel-polymorphic-eav
php artisan vendor:publish --provider="Davidwesdijk\LaravelPolymorphicEav\EavServiceProvider"
php artisan migrate
Publishing gives you the config file (table names, model bindings) and the migrations for the EAV tables — typically an attributes table holding the definitions and a values table holding the polymorphic valuable_type / valuable_id pair plus attribute_id and the actual value.
Where to look first:
config/eav.php — confirm table names and which Eloquent models are bound; override them here rather than editing vendor classes.(attribute_id, value) for filtering.use HasEavAttributes; on your model) and the Attribute / Value models.First use case — a Product that needs a different set of fields per category:
use Davidwesdijk\LaravelPolymorphicEav\Traits\HasEavAttributes;
class Product extends Model
{
use HasEavAttributes;
}
Then create definitions once (seeders, not runtime) and attach values per entity:
$color = Attribute::create(['name' => 'color', 'type' => 'string', 'slug' => 'color']);
$product->setEavAttribute('color', 'red');
$product->save();
$product->getEavAttribute('color'); // 'red'
Exact method names should be confirmed against the README for your installed version; the mental model — definitions are global, values are per-entity and polymorphic — is the part that matters.
Treat attribute definitions as schema, values as data. Create Attribute rows in migrations or seeders so they are versioned. Never let end users create definitions ad hoc; that turns your EAV store into an unvalidated free-for-all and makes reporting impossible.
Seed once, read everywhere. Load the attribute definitions once at boot (a cached collection keyed by slug) so your forms, validation rules and casts all derive from a single source:
$attributes = Cache::rememberForever('eav.attributes', fn () => Attribute::all()->keyBy('slug'));
Flush that cache in an Attribute model observer on save/delete.
Dynamic form generation. Loop the definitions for the entity's category, render the right input per type, then validate with a rule map built from the same definitions. This is the single biggest win the package gives you: one attributes table drives the admin UI for products, pages, users, whatever.
Eager load the relation, always. The values relation plus its attribute is a two-level eager load; skipping it is how EAV turns one query into two hundred:
$products = Product::with('eavValues.attribute')->paginate(25);
Filtering by attribute value is best done with whereHas against the polymorphic relation, or a join on the values table when you need to sort. For anything dashboard-like, write one named scope per hot filter rather than scattering whereHas through controllers.
Keep hot fields as real columns. Fields you sort, index, or aggregate on (price, status, published_at) belong in the main table. EAV is for the long tail of sparse, per-category fields. A hybrid — real column plus EAV overflow — is the only pattern that survives growth.
Polymorphic reuse. Because values are morphable, the same Attribute definitions can be shared across Product, Page and User. Use Relation::enforceMorphMap([...]) in a service provider so valuable_type stores stable aliases instead of FQCNs — this makes refactors safe.
app/ or a private package if you hit a dependency conflict. Budget for owning the code.$model->eavValues without with() will quietly explode. Add a debug assertion or Laravel's Model::preventLazyLoading() in local.Attribute. A boolean stored as "0"/"1" compared with === true will bite you.(valuable_type, valuable_id, attribute_id) in your own migration; without it, duplicate values appear the first time a form double-submits.deleted model event on each EAV-enabled model to purge values, or clean up in a scheduled command — otherwise the values table grows forever.enforceMorphMap after data exists, existing rows keep the old FQCN strings and silently stop resolving. Backfill in the same migration.Product::insert() or update() on a query builder bypasses the model layer entirely, so anything hooked into saved won't fire. Use save() per model, or handle EAV writes explicitly.->toSql() and ->getBindings() on a whereHas chain is the fastest way to see whether your attribute filter is actually constraining the outer query or leaking into a subquery.Value model for your own subclass to add casts, accessors or scopes, and register an observer on Attribute to manage caching. Do that instead of patching vendor files, so a future fork stays mergeable.$product->getEavAttribute('color') survive refactors to JSON columns or real columns later; assertions against raw values rows don't.How can I help you explore Laravel packages today?