Attrify Package
Attrify is a powerful Laravel package that enables dynamic attributes for Eloquent models. It allows you to store flexible key-value pairs with JSON support while maintaining the familiar Eloquent syntax and providing advanced querying capabilities.
Features Overview
🎯 Dynamic Attributes
- Flexible Storage: Store any type of data (strings, arrays, objects) as model attributes
- JSON Support: Native JSON casting with path-based querying
- Seamless Integration: Works transparently with existing Eloquent models
- Performance Optimized: Smart caching system to minimize database queries
🔍 Advanced Querying
- Eloquent Scopes: Query dynamic attributes using familiar Eloquent methods
- JSON Path Queries: Deep querying of nested JSON structures
- Pattern Matching: Like queries and regex support
- Sorting & Ordering: Order results by dynamic attribute values
🚀 Developer Experience
- Simple API: Intuitive trait-based implementation
- Type Safety: Proper type handling and casting
- Mass Assignment: Support for fillable dynamic attributes
- Automatic Syncing: Seamless synchronization with model lifecycle events
Installation
Install the package via Composer:
composer require obelaw/attrifyThe package will automatically register its service provider and publish migrations.
Database Setup
Run the migration to create the attributes table:
php artisan migrateThis creates the obelaw_attrifys table with the following structure:
Schema::create('obelaw_attrifys', function (Blueprint $table) {
$table->id();
$table->morphs('modelable'); // Polymorphic relationship
$table->string('key')->index(); // Attribute key
$table->json('value'); // JSON value storage
$table->unique(['modelable_id', 'modelable_type', 'key']); // Unique constraint
$table->timestamps();
});Basic Usage
Adding Attrify to Your Models
Add the HasAttrify trait to any Eloquent model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Obelaw\Attrify\Concerns\HasAttrify;
class Product extends Model
{
use HasAttrify;
protected $fillable = ['name', 'price'];
// Define which attributes should be stored dynamically
protected $fillableAttrify = [
'brand',
'color',
'specifications',
'metadata',
'tags'
];
}Setting Dynamic Attributes
Dynamic attributes work just like regular model attributes:
// Create a new product with dynamic attributes
$product = new Product([
'name' => 'Wireless Headphones',
'price' => 299.99
]);
// Set dynamic attributes
$product->brand = 'AudioTech';
$product->color = 'Black';
$product->specifications = [
'battery_life' => '30 hours',
'weight' => '250g',
'connectivity' => ['Bluetooth 5.0', 'USB-C']
];
$product->save();Getting Dynamic Attributes
Retrieve dynamic attributes seamlessly:
$product = Product::find(1);
// Access like regular attributes
echo $product->brand; // 'AudioTech'
echo $product->color; // 'Black'
// Access JSON data
$specs = $product->specifications;
echo $specs['battery_life']; // '30 hours'
// Get all dynamic attributes
$allAttributes = $product->getAttrifys();Mass Assignment
Dynamic attributes support mass assignment:
$product = Product::create([
'name' => 'Gaming Mouse',
'price' => 89.99,
'brand' => 'GameTech',
'color' => 'RGB',
'specifications' => [
'dpi' => 16000,
'buttons' => 8,
'polling_rate' => '1000Hz'
]
]);Advanced Querying
Basic Attribute Queries
Query models by their dynamic attributes:
// Find products by brand
$products = Product::whereAttrify('brand', 'AudioTech')->get();
// Find products with specific color
$blackProducts = Product::whereAttrify('color', 'Black')->get();
// Pattern matching
$techBrands = Product::whereAttrifyLike('brand', '%Tech%')->get();JSON Path Querying
Query nested JSON structures:
// Find products with specific battery life
$longBatteryProducts = Product::whereAttrifyJson(
'specifications',
'battery_life',
'30 hours'
)->get();
// Find products with Bluetooth connectivity
$bluetoothProducts = Product::whereAttrifyJsonContains(
'specifications',
'connectivity',
'Bluetooth 5.0'
)->get();
// Pattern matching in JSON
$usbProducts = Product::whereAttrifyJsonLike(
'specifications',
'connectivity',
'%USB%'
)->get();Advanced JSON Queries
Work with complex JSON structures:
// Find products with specific array length
$multiConnectivity = Product::whereAttrifyJsonLength(
'specifications',
'connectivity',
2 // exactly 2 connectivity options
)->get();
// Query nested objects
$product->metadata = [
'reviews' => [
'average' => 4.5,
'count' => 128
],
'availability' => [
'in_stock' => true,
'quantity' => 50
]
];
// Find highly rated products
$highRated = Product::whereAttrifyJson(
'metadata',
'reviews.average',
'>=',
4.0
)->get();Ordering by Dynamic Attributes
Sort results by dynamic attribute values:
// Order by brand name
$products = Product::orderByAttrify('brand', 'asc')->get();
// Order by JSON values
$products = Product::orderByAttrifyJson(
'specifications',
'battery_life',
'desc'
)->get();
// Complex ordering
$products = Product::whereAttrify('color', 'Black')
->orderByAttrify('brand')
->orderByAttrifyJson('specifications', 'price', 'asc')
->get();Working with Complex Data Types
Storing Arrays and Objects
$product = new Product();
// Store array data
$product->tags = ['wireless', 'premium', 'noise-cancelling'];
// Store complex objects
$product->specifications = [
'dimensions' => [
'length' => 180,
'width' => 150,
'height' => 80,
'unit' => 'mm'
],
'features' => [
'noise_cancellation' => true,
'wireless' => true,
'voice_assistant' => ['Siri', 'Google Assistant']
]
];
$product->save();Querying Arrays
// Find products with specific tags
$wirelessProducts = Product::whereAttrifyJsonContains('tags', 'wireless')->get();
// Find products with noise cancellation
$ncProducts = Product::whereAttrifyJson(
'specifications',
'features.noise_cancellation',
true
)->get();
// Find products supporting specific voice assistants
$siriProducts = Product::whereAttrifyJsonContains(
'specifications',
'features.voice_assistant',
'Siri'
)->get();Performance Optimization
Caching System
Attrify includes an intelligent caching system:
// Attributes are cached after first load
$product = Product::find(1);
$brand1 = $product->brand; // Loads from database
$brand2 = $product->brand; // Uses cached value
// Cache is automatically invalidated on updates
$product->brand = 'NewBrand';
$product->save(); // Cache is refreshedEager Loading
Optimize queries by eager loading attributes:
// Load all products with their dynamic attributes
$products = Product::with('attrifys')->get();
// The attributes are now available without additional queries
foreach ($products as $product) {
echo $product->brand; // No additional DB query
}Indexing Strategy
Optimize database performance:
// The package automatically creates indexes on:
// - key column for fast attribute lookup
// - unique constraint preventing duplicate attributes
// - morphable columns for efficient polymorphic queries
// For high-volume applications, consider adding custom indexes:
Schema::table('obelaw_attrifys', function (Blueprint $table) {
// Index for specific key queries
$table->index(['key', 'modelable_type']);
// JSON indexes for MySQL 8.0+
$table->index(['key', DB::raw('(JSON_EXTRACT(value, "$.price"))')]);
});Integration Examples
E-commerce Product Catalog
class Product extends Model
{
use HasAttrify;
protected $fillableAttrify = [
'brand', 'color', 'size', 'material',
'specifications', 'warranty', 'origin_country'
];
}
// Create products with varied attributes
Product::create([
'name' => 'T-Shirt',
'price' => 29.99,
'brand' => 'FashionBrand',
'color' => 'Blue',
'size' => 'Medium',
'material' => 'Cotton',
'origin_country' => 'USA'
]);
Product::create([
'name' => 'Laptop',
'price' => 999.99,
'brand' => 'TechCorp',
'specifications' => [
'cpu' => 'Intel i7',
'ram' => '16GB',
'storage' => '512GB SSD'
],
'warranty' => '2 years'
]);User Profiles with Custom Fields
class User extends Model
{
use HasAttrify;
protected $fillableAttrify = [
'preferences', 'social_links', 'custom_fields'
];
}
// Store user preferences
$user = User::find(1);
$user->preferences = [
'theme' => 'dark',
'language' => 'en',
'notifications' => [
'email' => true,
'push' => false,
'sms' => true
]
];
$user->social_links = [
'twitter' => '@username',
'linkedin' => 'linkedin.com/in/username'
];
$user->save();Content Management System
class Article extends Model
{
use HasAttrify;
protected $fillableAttrify = [
'seo_meta', 'custom_fields', 'display_options'
];
}
// SEO and custom content attributes
$article = Article::create([
'title' => 'Laravel Best Practices',
'content' => '...',
'seo_meta' => [
'meta_title' => 'Laravel Best Practices - Complete Guide',
'meta_description' => 'Learn Laravel best practices...',
'keywords' => ['laravel', 'php', 'best-practices']
],
'display_options' => [
'show_author' => true,
'show_date' => true,
'comments_enabled' => true
]
]);API Reference
Trait Methods
getAttribute($key)
Get an attribute value (overridden for dynamic attributes)
$value = $model->getAttribute('custom_field');setAttribute($key, $value)
Set an attribute value (overridden for dynamic attributes)
$model->setAttribute('custom_field', 'value');getAttrifys()
Get all dynamic attributes as a collection
$attributes = $model->getAttrifys();syncAttrify()
Manually sync dynamic attributes to database
$model->syncAttrify();Query Scopes
whereAttrify($key, $value)
Filter by exact attribute value
Model::whereAttrify('status', 'active');whereAttrifyLike($key, $pattern)
Filter by pattern matching
Model::whereAttrifyLike('name', '%search%');whereAttrifyJson($key, $jsonPath, $value)
Filter by JSON path value
Model::whereAttrifyJson('config', 'settings.theme', 'dark');whereAttrifyJsonLike($key, $jsonPath, $pattern)
Filter by JSON path pattern
Model::whereAttrifyJsonLike('meta', 'tags', '%tech%');whereAttrifyJsonContains($key, $jsonPath, $value)
Filter by JSON array contains
Model::whereAttrifyJsonContains('data', 'categories', 'electronics');whereAttrifyJsonLength($key, $jsonPath, $length)
Filter by JSON array length
Model::whereAttrifyJsonLength('data', 'items', 5);orderByAttrify($key, $direction = 'asc')
Order by attribute value
Model::orderByAttrify('priority', 'desc');orderByAttrifyJson($key, $jsonPath, $direction = 'asc')
Order by JSON path value
Model::orderByAttrifyJson('config', 'sort_order', 'asc');Configuration
Model Configuration
class YourModel extends Model
{
use HasAttrify;
// Define which attributes are dynamic
protected $fillableAttrify = [
'attribute1',
'attribute2',
'json_field'
];
// Optional: Override the relationship name
public function customAttrifys()
{
return $this->morphMany(Attrify::class, 'modelable');
}
}Service Provider Configuration
The package automatically registers the service provider, but you can customize it:
// In your AppServiceProvider
public function register()
{
// Custom configuration if needed
$this->app->bind(AttrifyContract::class, CustomAttrifyService::class);
}Best Practices
1. Attribute Naming
- Use consistent naming conventions for dynamic attributes
- Avoid conflicts with existing model properties
- Use descriptive names for JSON structures
// Good
protected $fillableAttrify = [
'product_specifications',
'shipping_options',
'customer_preferences'
];
// Avoid
protected $fillableAttrify = [
'spec', 'opts', 'prefs'
];2. JSON Structure
- Keep JSON structures flat when possible for better query performance
- Use consistent schemas within the same attribute across records
- Consider indexing frequently queried JSON paths
// Good structure
$product->specifications = [
'cpu' => 'Intel i7',
'ram' => 16,
'storage_gb' => 512
];
// Avoid deeply nested structures for frequently queried data
$product->specifications = [
'hardware' => [
'processor' => [
'brand' => 'Intel',
'model' => 'i7'
]
]
];3. Performance Considerations
- Use eager loading for bulk operations
- Index frequently queried attributes
- Consider caching for read-heavy applications
// Efficient bulk processing
$products = Product::with('attrifys')
->whereAttrify('status', 'active')
->get();
// Cache frequently accessed attributes
$popularSpecs = Cache::remember('popular_specs', 3600, function () {
return Product::whereAttrifyJson('specs', 'popular', true)
->pluck('specifications');
});4. Data Validation
- Validate dynamic attributes before saving
- Use consistent data types for the same attributes
- Implement proper error handling
// Validation example
public function setSpecificationsAttribute($value)
{
// Validate the structure
$validator = Validator::make($value, [
'cpu' => 'required|string',
'ram' => 'required|integer|min:4',
'storage_gb' => 'required|integer|min:128'
]);
if ($validator->fails()) {
throw new InvalidArgumentException('Invalid specifications format');
}
$this->setAttribute('specifications', $value);
}Troubleshooting
Common Issues
Dynamic Attributes Not Saving
Ensure the attribute is listed in $fillableAttrify:
protected $fillableAttrify = [
'your_attribute' // Must be listed here
];JSON Queries Not Working
Verify your database supports JSON operations:
- MySQL 5.7+ required for JSON functions
- PostgreSQL 9.3+ required for JSON support
Performance Issues
- Add appropriate indexes for frequently queried attributes
- Use eager loading to reduce N+1 queries
- Consider caching for read-heavy operations
// Add custom indexes for better performance
Schema::table('obelaw_attrifys', function (Blueprint $table) {
$table->index(['key', 'modelable_type']);
});Migration Guide
From Other Attribute Packages
If migrating from other dynamic attribute packages:
- Update trait usage:
// Replace old trait
use OldPackage\HasAttributes;
// With Attrify trait
use Obelaw\Attrify\Concerns\HasAttrify;- Update fillable configuration:
// Old configuration
protected $attributable = ['field1', 'field2'];
// New configuration
protected $fillableAttrify = ['field1', 'field2'];- Update queries:
// Old query method
Model::whereAttribute('key', 'value');
// New query method
Model::whereAttrify('key', 'value');Contributing
We welcome contributions! Please see our contributing guidelines for details.
License
The Attrify package is open-source software licensed under the MIT license.
Related Packages
- OConfigs - Dynamic configuration management
- Permit System - Role-based permissions
- Catalog System - Product management with attributes