Skip to content

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:

bash
composer require obelaw/attrify

The package will automatically register its service provider and publish migrations.

Database Setup

Run the migration to create the attributes table:

bash
php artisan migrate

This creates the obelaw_attrifys table with the following structure:

php
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
<?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:

php
// 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:

php
$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:

php
$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:

php
// 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:

php
// 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:

php
// 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:

php
// 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

php
$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

php
// 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:

php
// 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 refreshed

Eager Loading

Optimize queries by eager loading attributes:

php
// 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:

php
// 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

php
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

php
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

php
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)

php
$value = $model->getAttribute('custom_field');

setAttribute($key, $value)

Set an attribute value (overridden for dynamic attributes)

php
$model->setAttribute('custom_field', 'value');

getAttrifys()

Get all dynamic attributes as a collection

php
$attributes = $model->getAttrifys();

syncAttrify()

Manually sync dynamic attributes to database

php
$model->syncAttrify();

Query Scopes

whereAttrify($key, $value)

Filter by exact attribute value

php
Model::whereAttrify('status', 'active');

whereAttrifyLike($key, $pattern)

Filter by pattern matching

php
Model::whereAttrifyLike('name', '%search%');

whereAttrifyJson($key, $jsonPath, $value)

Filter by JSON path value

php
Model::whereAttrifyJson('config', 'settings.theme', 'dark');

whereAttrifyJsonLike($key, $jsonPath, $pattern)

Filter by JSON path pattern

php
Model::whereAttrifyJsonLike('meta', 'tags', '%tech%');

whereAttrifyJsonContains($key, $jsonPath, $value)

Filter by JSON array contains

php
Model::whereAttrifyJsonContains('data', 'categories', 'electronics');

whereAttrifyJsonLength($key, $jsonPath, $length)

Filter by JSON array length

php
Model::whereAttrifyJsonLength('data', 'items', 5);

orderByAttrify($key, $direction = 'asc')

Order by attribute value

php
Model::orderByAttrify('priority', 'desc');

orderByAttrifyJson($key, $jsonPath, $direction = 'asc')

Order by JSON path value

php
Model::orderByAttrifyJson('config', 'sort_order', 'asc');

Configuration

Model Configuration

php
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:

php
// 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
php
// 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
php
// 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
php
// 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
php
// 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:

php
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
php
// 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:

  1. Update trait usage:
php
// Replace old trait
use OldPackage\HasAttributes;

// With Attrify trait
use Obelaw\Attrify\Concerns\HasAttrify;
  1. Update fillable configuration:
php
// Old configuration
protected $attributable = ['field1', 'field2'];

// New configuration
protected $fillableAttrify = ['field1', 'field2'];
  1. Update queries:
php
// 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.

Released under the MIT License.