ekyna/characteristics
Laravel package for managing entity characteristics: define reusable attributes, groups and values, attach them to models, and handle normalization/validation for consistent storage and querying across your application.
Installation
composer require ekyna/characteristics
Publish the migration (if needed):
php artisan vendor:publish --provider="Ekyna\Characteristics\CharacteristicsServiceProvider" --tag="migrations"
php artisan migrate
Basic Model Integration
Add the HasCharacteristics trait to your Eloquent model:
use Ekyna\Characteristics\Traits\HasCharacteristics;
class Product extends Model
{
use HasCharacteristics;
}
Define Characteristics
Create a characteristics table entry (manually or via migration) for each feature (e.g., color, size). Example:
$characteristic = \Ekyna\Characteristics\Models\Characteristic::create([
'name' => 'color',
'type' => 'string', // or 'boolean', 'number', 'select'
'options' => json_encode(['red', 'blue', 'green']), // for 'select' type
]);
Assign Characteristics to an Entity
$product = Product::find(1);
$product->characteristics()->attach([
$characteristic->id => ['value' => 'blue'], // for 'string'/'number'
// OR for boolean: ['value' => true]
// OR for select: ['value' => 'red']
]);
Retrieve Characteristics
$product->characteristics; // Collection of Characteristic models
$product->getCharacteristic('color'); // Returns 'blue' (or null)
Dynamic Filtering Use characteristics to filter entities in queries:
$blueProducts = Product::whereHas('characteristics', function ($query) {
$query->where('name', 'color')
->where('value', 'blue');
})->get();
Validation Rules Leverage characteristics in Form Requests:
use Ekyna\Characteristics\Rules\CharacteristicExists;
public function rules()
{
return [
'characteristics.color' => ['required', new CharacteristicExists('color')],
];
}
API Responses Serialize characteristics in JSON:
return Product::with('characteristics')->get()->map(function ($product) {
return [
'id' => $product->id,
'characteristics' => $product->characteristics->pluck('name', 'value'),
];
});
Bulk Updates Update characteristics for multiple entities:
$products = Product::whereIn('id', [1, 2, 3])->get();
foreach ($products as $product) {
$product->characteristics()->sync([
$characteristic->id => ['value' => 'updated_value'],
]);
}
Polymorphic Relationships
Extend the package to support polymorphic characteristics (e.g., for Product and User):
// In CharacteristicsServiceProvider boot():
$this->app->bind('characteristicable', function () {
return config('characteristics.polymorphic_model', null);
});
Caching Cache characteristic queries for performance:
$characteristics = Cache::remember("product_{$product->id}_characteristics", now()->addHours(1), function () use ($product) {
return $product->characteristics;
});
Events Listen for characteristic changes:
\Ekyna\Characteristics\Models\Characteristic::saved(function ($characteristic) {
// Log or notify when a characteristic is updated
});
Missing Migrations
Characteristic model errors.php artisan migrate post-install.Type Mismatches
string value in a number or boolean characteristic field will silently fail or corrupt data.if ($characteristic->type === 'number' && !is_numeric($value)) {
throw new \InvalidArgumentException("Value must be numeric for {$characteristic->name}");
}
Overwriting Values
sync() or attach() without preserving existing values will overwrite them.syncWithoutDetaching() or manually merge values:
$existing = $product->characteristics->pluck('value', 'id')->toArray();
$product->characteristics()->sync(array_merge($existing, [$newId => ['value' => $newValue]]));
Case Sensitivity
select type options are case-sensitive by default. E.g., 'Red' ≠ 'red'.$options = array_map('strtolower', json_decode($characteristic->options, true));
Performance with Large Datasets
with() to eager-load:
$products = Product::with('characteristics')->get();
Check Database Verify characteristics are stored correctly:
SELECT * FROM characteristics;
SELECT * FROM characteristicables;
Log Characteristic Attachments Debug sync issues:
\Ekyna\Characteristics\Models\Characteristic::saved(function ($characteristic) {
\Log::debug("Characteristic updated: ", [
'id' => $characteristic->id,
'value' => $characteristic->pivot->value,
]);
});
Validate Characteristic Types
Ensure the type column in the characteristics table matches your usage (string, boolean, number, select).
Custom Characteristic Types
Extend the package to support custom types (e.g., date, json):
// In app/Providers/CharacteristicsServiceProvider.php
$this->app->extend('characteristics.types', function ($types) {
$types['date'] = \Ekyna\Characteristics\Types\DateType::class;
return $types;
});
Custom Storage Store characteristics in a NoSQL database or cache:
// Override the pivot model
class CustomCharacteristicPivot extends \Ekyna\Characteristics\Models\CharacteristicPivot
{
public function save(array $options = [])
{
// Custom logic (e.g., Redis storage)
parent::save($options);
}
}
API Resources
Create a dedicated CharacteristicResource for API responses:
namespace App\Http\Resources;
use Ekyna\Characteristics\Models\Characteristic;
use Illuminate\Http\Resources\Json\JsonResource;
class CharacteristicResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'type' => $this->type,
'value' => $this->pivot->value,
'options' => $this->options,
];
}
}
Scopes for Querying Add reusable scopes to your models:
class Product extends Model
{
use HasCharacteristics;
public function scopeWithCharacteristic($query, $name, $value)
{
return $query->whereHas('characteristics', function ($q) use ($name, $value) {
$q->where('name', $name)
->where('value', $value);
});
}
}
How can I help you explore Laravel packages today?