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 Bundle Laravel Package

msgphp/eav-bundle

A Symfony bundle for entity-attribute-value (EAV) modeling, letting you attach flexible, dynamic attributes and typed values to entities without schema changes. Integrates with Doctrine and the msgphp domain layer for extensible, metadata-driven data storage.

View on GitHub
Deep Wiki
Context7

Getting Started

The package is a Symfony bundle (MsgPhp\EavBundle\MsgPhpEavBundle), not a Laravel package. It ships the EAV domain (Attribute, AttributeValue, AttributeMetadata) plus Doctrine XML mappings and a DI extension. In a Laravel app you have two realistic entry points:

  1. Use msgphp/eav only (the plain domain library) and wire your own Doctrine mappings / repositories — this is the path of least resistance under Laravel.
  2. Port the bundle's config by reading DependencyInjection/Configuration.php and Resources/config/*.xml and re-declaring the services in a ServiceProvider.

If you are on Symfony, it's a normal three-line install:

composer require msgphp/eav-bundle
# config/bundles.php
MsgPhp\EavBundle\MsgPhpEavBundle::class => ['all' => true],

Where to look first

  1. src/ of msgphp/eav — Attribute, AttributeValue, AttributeMetadata (short classes, read them end to end before writing anything).
  2. msgphp/eav-bundle/Resources/config/doctrine/*.orm.xml — the mapped-superclass definitions. This is the contract you inherit; if a column/association surprises you, it's defined here.
  3. msgphp/eav-bundle/DependencyInjection/Configuration.php — the class: map that lets you swap in your own entity classes.
  4. msgphp/domain — DomainId, DomainIdInterface, the Identifiable/Entity base. The IDs (AttributeId, AttributeValueId) are UUID-backed by default and rely on DBAL types registered by msgphp/domain-bundle.

First use case

The canonical one: a Product catalogue where each product needs arbitrary, admin-defined properties (colour, weight, screen size, "is BPA free"). You don't want a migration per property.

// one-off, in a seeder or an admin controller
$attribute = new Attribute(AttributeId::create(), 'screen_size');
$attribute->setMetadata(new AttributeMetadata('float', multiple: false));
$em->persist($attribute);
$em->flush();

// later, per product
$product->addAttribute(new AttributeValue(
    AttributeValueId::create(),
    $attribute->getId(),
    '6.1'
));

Two things to do before that first persist: (a) create your own App\Entity\Attribute extends MsgPhp\Eav\Attribute and register it under msgphp_eav.class.attribute (or your own service config), because the library classes are mapped-superclass and Doctrine will not manage them directly; (b) run doctrine:schema:validate and diff a migration.

Implementation Patterns

Mapped-superclass extension (the core idiom)

Every msgphp bundle uses the same pattern: the library defines the domain entity, the bundle defines the mapping, you define the concrete class. Your entity must be the one Doctrine manages.

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use MsgPhp\Eav\Attribute as BaseAttribute;

/**
 * @ORM\Entity(repositoryClass="App\Repository\AttributeRepository")
 * @ORM\Table(name="eav_attribute")
 * @ORM\Index(columns={"name"})
 */
class Attribute extends BaseAttribute
{
}
/**
 * @ORM\Entity()
 * @ORM\Table(name="eav_attribute_value")
 * @ORM\Index(columns={"attribute_id"})
 */
class AttributeValue extends \MsgPhp\Eav\AttributeValue
{
}

Then point the bundle at them:

# config/packages/msgphp_eav.yaml
msgphp_eav:
    class:
        attribute: App\Entity\Attribute
        attribute_value: App\Entity\AttributeValue

Under Laravel, reproduce the same mapped-superclass XML by hand or convert it to attributes on your subclass (a subclass can't redefine the parent's fields, so you generally copy the mapping onto your class and skip the XML entirely).

Attaching values to your own aggregate

Give your Product the EAV side of the relation. The library exposes a HasAttributes-style interface/trait for exactly this — check src/ for the exact name in your version, since it moved between releases — and implement it on the aggregate:

class Product implements HasAttributes
{
    /** @ORM\OneToMany(targetEntity=AttributeValue::class, mappedBy="product", cascade={"persist"}, orphanRemoval=true) */
    private $attributes;

    public function addAttribute(AttributeValueInterface $value): void
    {
        $this->attributes->add($value);
    }

    public function getAttribute(string $name): ?AttributeValueInterface
    {
        foreach ($this->attributes as $value) {
            if ($value->getAttribute()->getName() === $name) {
                return $value;
            }
        }
        return null;
    }
}

Keep the "get by name" helper on your entity, not in a repository — you'll call it in Blade templates constantly.

Typed accessors

AttributeValue::$value is stored as a string. Cast at the boundary so the rest of the app never sees strings:

final class ProductAttributeReader
{
    public function __construct(private AttributeRepository $attributes) {}

    public function float(Product $product, string $name): ?float
    {
        $value = $product->getAttribute($name);
        return $value?->getValue() === null ? null : (float) $value->getValue();
    }
}

Drive the cast from AttributeMetadata::$dataType (or whatever your version calls it) rather than from per-call (float) casts, so admin-defined types actually mean something.

Querying without loading the world

EAV means joins. Use fetch-join on the values you actually need, and filter by attribute.name in DQL rather than in PHP:

$qb->select('p', 'v', 'a')
   ->leftJoin('p.attributes', 'v')
   ->leftJoin('v.attribute', 'a', 'WITH', 'a.name IN (:names)')
   ->setParameter('names', ['colour', 'screen_size']);

A Product grid showing 20 products × 6 attributes will otherwise be 120 queries.

Admin-side workflows

Three recurring flows worth building once:

  • Attribute catalogue CRUD — create/rename attributes, edit AttributeMetadata, mark deprecated rather than deleting (deleting an Attribute orphans values).
  • Import/mapping — when ingesting a supplier feed, map unknown column headers to Attribute rows on the fly, then attach values.
  • Validation — a rule per dataType before persist. The library does not validate value/type consistency for you.

Gotchas and Tips

  • This is not a Laravel package. There is no service provider, no facade, no config/eav.php. MsgPhpEavBundle extends Symfony's Bundle and will not boot under Laravel. Plan for msgphp/eav + your own Doctrine layer, or accept a day of porting.
  • Do not @ORM\Entity the library classes. They're mapped superclasses; Doctrine will throw on duplicate/undefined mappings. Always subclass and register the subclass in the class map.
  • The class map is mandatory, not optional. If you forget msgphp_eav.class.attribute, the bundle resolves to the library class and you get a mapping error at cache warmup, not at runtime — easy to miss in CI.
  • UUID DBAL types. IDs come from msgphp/domain's DomainId; the DBAL type registration lives in msgphp/domain-bundle. In Laravel you must register DomainIdType / the concrete id types yourself in your Doctrine config, or IDs silently persist as strings and queries miss.
  • $value is a string column. Comparisons (WHERE v.value > 10) will sort lexicographically. Cast in SQL or store numeric metadata separately.
  • Delete semantics. orphanRemoval + cascade={"persist"} on the inverse side is what you want; the library gives you no cascade policy, so decide it per aggregate.
  • AttributeValue holds both attributeId and an attribute association. Keep them in sync — constructing with the id but never setting the association breaks getAttribute() and your WITH a.name = ... joins.
  • Maturity check before committing. Zero stars, zero dependents, no recent activity, and constraints pinned to older PHP/Doctrine majors. Verify composer why-not against your PHP and doctrine/orm versions first; a transitive conflict here is a multi-hour rabbit hole and there is no upstream to patch it.
  • Debugging: bin/console doctrine:schema:validate (or php artisan doctrine:schema:validate) catches superclass mistakes fastest; doctrine:migrations:diff then --dry-run before touching a populated EAV table (a dropped attribute_id column loses data irreversibly).
  • Extension points that actually exist: the class: map (swap entities), custom repositories on your subclass, and decorating the bundle's factories in the container. There is no event system, no plugin API — don't go looking.
  • Honest recommendation: for a greenfield Laravel app, a hand-rolled attributes / attribute_values pair (or spatie/laravel-schemaless-attributes, or Postgres jsonb) gives you 90% of this package with a tenth of the integration cost. Use msgphp/eav when you specifically want its Attribute/AttributeValue/AttributeMetadata model and already run Doctrine.
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.
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment