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.jsonilluminate/*) - 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
findByEavinstead of chaining manywhereEavAttribute*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
typemust 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 migrateso additive migrations apply in order.
Links
- Packagist / GitHub:
fiachehr/laravel-eav - Full README on GitHub covers every scope, translation edge cases, and aggregate examples in depth.