php

EAV in Laravel: what I built and why

Why I built an EAV package for Laravel, what Magento taught me about the pattern, and how I wanted the API to feel cleaner in Eloquent.

Most developers meet EAV through a painful system first.

For me, that system was Magento — six years of thinking inside a codebase where product catalog flexibility comes at the cost of joins across catalog_product_entity_varchar, catalog_product_entity_int, catalog_product_entity_decimal, and attribute metadata tables. I have watched Magento EAV make experienced developers genuinely frustrated. I have also watched it solve real problems that rigid flat tables cannot solve comfortably.

Both observations matter. Magento taught me two things that can look contradictory until you sit with them long enough: EAV solves a genuine problem, and EAV can make a system substantially harder to query and reason about. Holding both at the same time is what eventually pushed me toward building an EAV package for Laravel — not to import Magento’s complexity into a cleaner framework, but to keep the useful part of the pattern and discard as much accidental pain as possible.

Why EAV exists and when it deserves to exist

The pattern is appropriate when the shape of an entity is unstable in ways that matter. Attributes are dynamic, different instances need different fields, or the business defines new attributes over time without engineering involvement. In those cases, a conventional table with a long list of nullable columns becomes genuinely awkward. Adding a new attribute type means a schema migration. Differing attribute sets across product families mean conditionally nullable columns or a mess of related tables.

EAV addresses this by separating three things: the entity itself, attribute definitions describing what fields exist, and values storing typed data against entity-attribute pairs. The schema is flexible because attributes are rows rather than columns, and the same entity table can participate in many different attribute configurations without schema changes.

That is the right tradeoff in some domains. Commerce is one of them. Content management is another. User-configurable product features are a third. The mistake is not using EAV in those domains — the mistake is treating it as a general-purpose data modeling strategy.

Magento got the central insight right: product catalogs in multi-category retail genuinely need more flexibility than a flat table offers. The tradeoff it made is defensible given when the decision was made. Where it became painful is the developer experience layered on top — a mental model that forces engineers to think constantly about attribute tables, scope values, type tables, and join structure rather than about the business problem they are trying to solve. That is not inherent to EAV. That is a design choice that a cleaner implementation does not have to repeat.

What I wanted the Eloquent API to feel like

The design goal for the Laravel eav package was expressed in terms of what it should not require of developers.

Developers using the package should not need to know that dynamic attributes are stored differently from normal model fields. They should not need to write explicit join logic to read attribute values. They should not need to maintain a separate mental model for EAV models versus regular Eloquent models. The package should speak the language Laravel developers already know — models, relationships, scopes, eager loading — and fit that language rather than layering a foreign system on top.

That is the gap Magento never closed, and it is why Magento EAV has such a bad reputation among developers familiar with more ergonomic ORMs. The storage model is not the problem. The API surface is.

For the entity model, I wanted something that declares EAV capability through a trait or interface addition rather than through inheritance from a different base class. The existing model stays recognizable as an Eloquent model. Attributes are first-class definitions with metadata — type, label, default values — accessible through the model’s interface. Values load and present themselves as ordinary model properties, with the EAV storage abstracted below the surface.

The explicit concepts stay accessible when you need them. Entities, attributes, and values are not hidden. If you want to query directly against the attribute layer, you can. But the common case — reading and writing dynamic attributes on a model — should not require thinking about storage internals at all.

Loading strategy and the N+1 problem

The most important performance decision in any EAV implementation is the loading strategy for attribute values. If every entity loads its dynamic attributes lazily in separate queries, the system becomes unusable at any scale. N+1 in a conventional Eloquent model is annoying. N+1 in an EAV model multiplies across attribute tables, and the result is query counts that are difficult to believe without a query log open.

Laravel’s eager loading patterns are the right tool here. If attribute values can load in batches — one query per attribute type for a set of entities rather than one query per entity — the performance model becomes comprehensible. The cost is proportionate to the number of entities being loaded and the number of attribute types, not to their product. That is a meaningful difference between a workable implementation and one that breaks down under ordinary application loads.

EAV will never be as cheap as a flat table for ordinary reads. The join structure that enables flexibility is not free. What matters is making the cost predictable and understandable — something Magento’s EAV implementation never quite achieved at the developer experience level.

The package design leans on Eloquent relationships for value loading. Values attach to entities through relationship-like mechanisms that support eager loading. The query structure is visible and understandable, not buried in magic that makes performance investigation difficult.

When I would and wouldn’t reach for this

Building the package sharpened my thinking about when EAV actually earns its implementation cost.

It makes sense when attributes are genuinely dynamic rather than just numerous, when the business domain legitimately needs to define new fields over time without schema migrations, and when different entity instances meaningfully need different attribute sets. Commerce catalogs, configurable product types, CMS with flexible content schemas — real cases.

Where I would not use it: stable schemas where the rigidity of flat tables is actually a feature rather than a limitation, reporting-heavy workloads where query simplicity is paramount, and applications where the attribute set is effectively fixed at design time and the dynamic-field flexibility never gets used. EAV chosen for those cases adds implementation complexity without delivering the flexibility that justifies it.

Magento made me understand this line the hard way — watching EAV applied uniformly to a system where parts of the catalog would have been simpler and faster with conventional relational modeling. The pattern is not good or bad in the abstract. It is appropriate or inappropriate for a specific problem.

What the project is really about

The package is the tangible output of an idea I have been working toward since my Magento years: that the EAV pattern is worth preserving but the developer experience around it can be substantially better than what the frameworks we inherited showed us.

Building it in the Eloquent context has been clarifying. Laravel’s approach to expressive model APIs is not accidental — it reflects strong opinions about what everyday data access code should feel like. An EAV layer that respects those opinions rather than overriding them is a fundamentally different thing from one that adds a foreign mental model alongside the framework’s native one.

That distinction is what I care about most from this project. Not that an EAV package now exists for Laravel — there are others. But that a version of the pattern exists that does not ask developers to abandon what they already know in order to work with dynamic attributes. Bringing an architectural pattern into a new context without carrying along its accumulated bad habits — that is the question I find genuinely interesting, and this package is my current best answer to it.