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

Eav Laravel Package

sunel/eav

Sunel/EAV brings flexible Entity-Attribute-Value modeling to Laravel. Attach dynamic attributes to Eloquent models without constant migrations, store typed values, and query entities by custom attributes—ideal for catalogs, CMS data, and configurable products.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish (5 minutes):

composer require sunel/eav
php artisan vendor:publish --provider="Sunel\Eav\EavServiceProvider"
php artisan migrate

Read three things before writing a line of code, in this order:

  1. config/eav.php — table names and model bindings. This is your override surface, not just configuration.
  2. src/Models/ — Entity, Attribute, Value, Option. These are ordinary Eloquent models; anything you know about Eloquent applies directly.
  3. src/Traits/ — the trait you apply to your own model. This is where the __get/__set/save hooks live if the package provides magic attribute access.

First use case — a product catalog with merchandiser-defined attributes:

// 1. Define the attribute once (seeder, admin screen, or migration)
$color = Attribute::create([
    'code'    => 'color',
    'name'    => 'Color',
    'type'    => 'string',        // string|text|int|decimal|boolean|date|select
    'options' => ['red', 'green', 'blue'],
]);

// 2. Apply the trait to your model
use Sunel\Eav\EavTrait;

class Product extends Model
{
    use EavTrait;
}

// 3. Read/write through the model
$product->color = 'red';
$product->save();

$product->color;                    // 'red', resolved from eav_values
$product->eavValues;                // raw Value models, if the relation is exposed

Confirm the exact trait namespace and helper method names against src/ — the README is from 2018 and forks differ. The Value model is always the escape hatch when magic accessors don't fit:

Value::create([
    'entity_id'    => $product->id,
    'attribute_id' => $color->id,
    'value'        => 'red',
]);

Implementation Patterns

Treat attribute definitions as seed data, not runtime config. Put them in a seeder or a data migration so dev, staging, and production match. The package will happily let you create attributes in a controller; don't.

One attribute set per model type. If the package exposes entity types/sets, bind Product to a single set and attach attributes to that set. This is what keeps Attribute::where('code', 'color') unambiguous when several models need a "size" attribute.

Eager load values, always.

$products = Product::with('eavValues.attribute')->paginate(50);

Lazy access on a 50-row listing with 10 attributes is 500+ queries. This is the single biggest day-to-day performance decision.

Querying by attribute. For a single filter, whereHas is fine:

$colorId = Attribute::where('code', 'color')->value('id');

Product::whereHas('eavValues', function ($q) use ($colorId) {
    $q->where('attribute_id', $colorId)->where('value', 'red');
})->get();

For multi-attribute filters or sorting, join eav_values once per attribute, or — better — maintain a flat projection table (product_flat) rebuilt on save. EAV is a storage model, not a reporting model.

Serialization for APIs. Append a code→value map so your JSON doesn't expose the normalized shape:

protected $appends = ['attributes'];

public function getAttributesAttribute()
{
    return $this->eavValues->mapWithKeys(
        fn ($v) => [$v->attribute->code => $v->value]
    );
}

Validation from metadata. The attribute rows are the schema; generate rules from them instead of hand-writing:

$rules = $attributes->mapWithKeys(function ($a) {
    return ["eav.{$a->code}" => [
        $a->required ? 'required' : 'nullable',
        match ($a->type) {
            'int'     => 'integer',
            'decimal' => 'numeric',
            'boolean' => 'boolean',
            'date'    => 'date',
            'select'  => Rule::in($a->options),
            default   => 'string',
        },
    ]];
});

Write in a transaction. One entity plus N values is multi-row; a half-saved product is a real failure mode.

Cache attribute definitions. They change rarely and are read on every request that renders a form. Cache the collection keyed by entity type and bust it in the Attribute model's saved/deleted events.

Testing. Seed a small attribute fixture in setUp(), use RefreshDatabase, and assert at the storage layer (assertDatabaseHas('eav_values', [...])) as well as through the model — the magic accessors can mask write failures.

Gotchas and Tips

  • Age is the headline risk. Last release 2018-09-03; composer constraints will pin you to Laravel 5.5–5.8 and PHP 7.1/7.2. On Laravel 9+ expect to patch: helper calls (str_* → Str::*), $dates, service provider defer behavior, and getAttribute signatures. Budget for a fork in your vendor namespace rather than fighting --ignore-platform-reqs in production.
  • getAttribute collision. If the trait overrides Eloquent's getAttribute, real DB columns still win, but an EAV attribute named status can shadow a model accessor or relation of the same name. Namespace codes (spec_status) or keep a reserved-name list and validate against it when attributes are created.
  • Everything is stored as a string. boolean false comes back as "0" or "" depending on the write path; decimals drop trailing zeros. Cast on read using a per-attribute cast map derived from type — never trust the raw value column in business logic.
  • No database-level required/uniqueness. That's your validation layer's job. Keep a unique index on (entity_id, attribute_id) for single-valued attributes; multi-valued attributes need a different design (a separate repeatable group or a JSON column) — don't try to bolt it on.
  • Orphaned values. Check the published migrations for onDelete('cascade') on entity_id and attribute_id. If they aren't there, add them in your own migration; deleting an attribute otherwise leaves rows that silently break filters.
  • Indexes are minimal by default. Add composites on (entity_id, attribute_id) and (attribute_id, value). Without the second one, every attribute filter is a full scan.
  • Save semantics. Confirm whether setting an attribute writes immediately or queues until save(). The pattern $product->save(); $product->color = 'red'; losing the value is a classic. Wrap writes in a helper that always saves.
  • Sorting is the pain point. "Sort products by weight" via EAV is a join plus an index you probably don't have. Denormalize anything the admin UI sorts or filters at scale.
  • Debugging. DB::listen() or Telescope, then inspect eav_values for the entity id directly. toArray() will not include EAV values unless you append them, which makes "it works in the model but not the API" a common confusion.
  • Extension points. Model bindings in config/eav.php let you subclass Attribute/Value (add casts, scopes, custom types) without forking. That's the cleanest place to add behavior the package doesn't ship.
  • When not to use it. Under ~10 attributes, or heavy reporting, Laravel's json column plus generated columns beats EAV on simplicity, query cost, and validation. Reach for this package when the attribute list is genuinely open-ended and admin-defined.
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.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core