← Back to fiachehr.ir
Fiachehr · Package docs

Laravel EAV

A full EAV (Entity–Attribute–Value) layer for Laravel: domain-driven design, 20+ attribute types, validation rules, multilingual definitions, translation storage, fast typed columns, model scopes, and a fluent EavQueryBuilder for multi-condition filters. Targets Laravel 10, 11, and 12 with PHP 8.3+.

Overview

  • Layers: Domain entities/enums, application use cases & DTOs, infrastructure Eloquent models & repositories.
  • Storage: Values stored in typed columns (value_text, value_number, value_decimal, dates, boolean, JSON) for indexing and range queries.
  • API surface: Trait methods on your models, static scopes, Product::eavQuery() / findByEav(), repositories, and validators.

Requirements

  • PHP ≥ 8.3
  • Laravel 10.x, 11.x, or 12.x (see composer.json illuminate/*)
  • Composer

Installation

composer require fiachehr/laravel-eav

The service provider calls loadMigrationsFrom() — run:

php artisan migrate

Publish configuration

php artisan vendor:publish --tag=laravel-eav-config

Produces config/laravel-eav.php for table name overrides and cache-related options.

Database & migrations

Typical migration set (names may vary by version):

  • attributes — definitions (title, slug, type, validations, language, …)
  • attribute_groups — grouping metadata
  • attribute_group_attributes — pivot group ↔ attribute
  • attributable_attributes — actual EAV values (typed columns + pivot to entity)
  • attributable_attribute_groups — entity ↔ group assignment
  • eav_translations — optional field translations (title, description, …)

HasAttributes trait

use Fiachehr\LaravelEav\Domain\Shared\Traits\HasAttributes;
use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    use HasAttributes;
}

Provides relationships such as eavAttributes, eavAttributeValues, eavAttributeGroups, and all getEav* / setEav* / whereEav* helpers.

Reading & writing values

Read

$product->eavAttributes; // collection with pivot
$value = $product->getEavAttributeValue(1);          // by id
$value = $product->getEavAttributeValue('color');   // by slug
$values = $product->getEavAttributeValues('slug');  // ['color' => 'red', ...]
$values = $product->getEavAttributeValues('id');
$models = $product->eavAttributeValues;

Write / sync / clear

$product->setEavAttributeValues([
    1 => 'Value by id',
    'color' => 'red',
    'price' => 100.5,
    'is_active' => true,
]);

$product->setEavAttributeValue('color', 'blue');
$product->syncEavAttributeValues(['color' => 'red', 'size' => 'M']);
$product->removeEavAttributeValue('color');
$product->clearEavAttributeValues();

Attribute groups

$product->eavAttributeGroups;
$product->attachEavAttributeGroups([1, 2, 3]);
$product->syncEavAttributeGroups([1, 2]);
$product->detachEavAttributeGroups([1]);
$attrs = $product->getEavAttributesThroughGroups();

Validation

Use Fiachehr\LaravelEav\Domain\Enums\AttributeType and ValidationType when defining attributes (via admin UI, seeders, or use cases).

use Fiachehr\LaravelEav\Domain\Enums\AttributeType;
use Fiachehr\LaravelEav\Domain\Enums\ValidationType;

// Example metadata shape when persisting an attribute definition:
[
    'title' => 'Email',
    'type' => AttributeType::TEXT,
    'validations' => [
        ValidationType::REQUIRED->value,
        ValidationType::EMAIL->value,
    ],
];

// With parameters
[
    'validations' => [
        ['type' => ValidationType::MAX_LENGTH->value, 'parameter' => 255],
        ['type' => ValidationType::MAX_FILE_SIZE->value, 'parameter' => 2048],
    ],
];

Runtime validation & Laravel rules

use Fiachehr\LaravelEav\Domain\Services\AttributeValidator;

$validator = new AttributeValidator();

$validator->validate(
    AttributeType::TEXT,
    'user@example.com',
    [ValidationType::EMAIL->value]
);

$rules = $validator->getValidationRules(
    AttributeType::TEXT,
    [ValidationType::REQUIRED->value, ValidationType::EMAIL->value]
);

$request->validate(['field' => $rules]);

Common validation keys: text (required, min_length, max_length, email, url, slug, regex), numbers (min, max, integer, decimal), files (image, video, max_file_size, allowed_mime_types), formats (json, markdown, …), dates (date_format, after, before).

Attribute types (summary)

  • Text: TEXT, TEXTAREA, PASSWORD
  • Numbers: NUMBER, DECIMAL
  • Choice: RADIO, SELECT, MULTIPLE, CHECKBOX, COLOR
  • Date/time: DATE, TIME, DATETIME
  • Other: BOOLEAN, FILE, LOCATION, COORDINATES

Prefer base types + validations (e.g. TEXT + email validation) instead of duplicating types per format.

Multilingual & translations

Language on definitions

Attributes and groups carry a language column. Create separate rows per locale or use logical IDs to tie translations.

use Fiachehr\LaravelEav\Infrastructure\Persistence\Eloquent\EloquentAttribute;

EloquentAttribute::forLanguage('en')->get();
EloquentAttribute::forCurrentLanguage()->get();
EloquentAttribute::forLanguages(['en', 'fa'])->get();

eav_translations + HasTranslations

Extend EloquentAttribute (or groups), add Fiachehr\LaravelEav\Domain\Shared\Traits\HasTranslations, set $translatable, then use getTranslation, setTranslation, setTranslations, deleteTranslation, etc.

Search scopes (HasAttributes models)

Product::whereEavAttribute('color', 'red')->get();
Product::whereEavAttributeLike('title', 'laptop')->get();
Product::whereEavAttributeBetween('price', 100, 500)->get();
Product::whereEavAttributeDateBetween('created_at', '2024-01-01', '2024-12-31')->get();
Product::whereEavAttributeIn('color', ['red', 'blue'])->get();
Product::whereEavAttributeNotIn('status', ['archived'])->get();
Product::whereEavAttributeNull('notes')->get();
Product::whereEavAttributeNotNull('description')->get();

Product::whereEavAttributes([
    ['attribute' => 'color', 'value' => 'red'],
    ['attribute' => 'price', 'value' => 100, 'operator' => '>='],
])->get();

EavQueryBuilder

Prefer this for many AND/OR conditions — single SQL shape, easier to optimize.

$productIds = Product::eavQuery()
    ->whereText('color', 'red')
    ->whereNumber('price', '>', 100)
    ->whereBoolean('in_stock', true)
    ->whereDateBetween('created_at', '2024-01-01', '2024-12-31')
    ->orderByNumber('price', 'asc')
    ->limit(20)
    ->getAttributableIds();

$products = Product::whereIn('id', $productIds)->get();

// Helper
$products = Product::findByEav(function ($q) {
    $q->whereTextIn('color', ['red', 'blue'])
      ->whereNumberBetween('price', 50, 200)
      ->whereTrue('in_stock');
});

Other builder methods (examples)

whereTextLike, whereDecimal, whereDateTime, whereJsonContains, whereAny / nested where(function …), orderByText, orderByDateTime, sum / avg / min / max on attribute slugs, getDistinctAttributeValues, count().

Repositories & use cases

use Fiachehr\LaravelEav\Domain\Repositories\AttributeRepositoryInterface;

$repo = app(AttributeRepositoryInterface::class);
$repo->findById(1);
$repo->findBySlug('color');
$repo->findByLogicalId('uuid');
$repo->findActive();
use Fiachehr\LaravelEav\Application\UseCases\CreateAttributeUseCase;
use Fiachehr\LaravelEav\Application\DTOs\CreateAttributeDTO;
use Fiachehr\LaravelEav\Domain\Enums\AttributeType;

$useCase = app(CreateAttributeUseCase::class);
$attribute = $useCase->execute(CreateAttributeDTO::fromArray([
    'title' => 'Color',
    'slug' => 'color',
    'type' => AttributeType::COLOR->value,
    'values' => ['red', 'blue'],
    'validations' => ['required'],
    'is_active' => true,
    'language' => 'en',
]));

Performance tips

  • Eager-load eavAttributes (and constrain columns) when listing many models.
  • Use findByEav instead of chaining many whereEavAttribute* calls when logic is complex.
  • Rely on package indexes on value columns; avoid selecting all attributes when you only need a few slugs.
Product::with(['eavAttributes' => fn ($q) => $q->whereIn('slug', ['color', 'price'])])->get();

Testing

The package ships a large PHPUnit suite (on the order of 120+ tests). From a clone:

composer install
./vendor/bin/phpunit

Troubleshooting

  • Values not persisting — attribute type must match the PHP value you pass (text vs number vs date).
  • Slow lists — add eager loads; check missing indexes after manual DB edits.
  • Migration failures on upgrade — run php artisan migrate so additive migrations apply in order.