solution-forest / laravel-dynamic-properties
A fast, flexible dynamic property system for Laravel entities
Requires
- php: ^8.3
- illuminate/database: ^11.0|^12.0
- illuminate/support: ^11.0|^12.0
Requires (Dev)
- laravel/pint: ^1.24
- orchestra/testbench: ^9.0|^10.0
- pestphp/pest: ^4.0
This package is auto-updated.
Last update: 2025-08-15 10:53:19 UTC
README
A dynamic property system for Laravel that allows any entity (users, companies, contacts, etc.) to have custom properties with validation, search capabilities, and optimal performance.
Requirements
- PHP: 8.3 or higher
- Laravel: 11.0 or higher
- Database: MySQL 8.0+ or SQLite 3.35+ (with JSON support)
Features
- Simple Architecture: Clean 2-table design with optional JSON caching
- Type Safety: Support for text, number, date, boolean, and select properties
- Fast Performance: < 1ms property retrieval with JSON cache, < 20ms without
- Flexible Search: Property-based filtering with multiple operators
- Easy Integration: Simple trait-based implementation
- Database Agnostic: Works with MySQL and SQLite
- Validation: Built-in property validation with custom rules
Installation
Install the package via Composer:
composer require solution-forest/laravel-dynamic-properties
Publish and run the migrations:
php artisan vendor:publish --provider="SolutionForest\LaravelDynamicProperties\DynamicPropertyServiceProvider" --tag="migrations" php artisan migrate
Optionally, publish the configuration file:
php artisan vendor:publish --provider="SolutionForest\LaravelDynamicProperties\DynamicPropertyServiceProvider" --tag="config"
Quick Start
⚠️ IMPORTANT: You must create Property definitions before setting property values. Attempting to set a property that doesn't have a definition will throw a
PropertyNotFoundException
.
1. Add the Trait to Your Models
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use SolutionForest\LaravelDynamicProperties\Traits\HasProperties; class User extends Model { use HasProperties; // Your existing model code... }
2. Create Properties
⚠️ REQUIRED STEP: Before setting any property values, you must first create the property definitions. This ensures type safety, validation, and optimal performance.
use SolutionForest\LaravelDynamicProperties\Models\Property; // Create a text property Property::create([ 'name' => 'phone', 'label' => 'Phone Number', 'type' => 'text', 'required' => false, 'validation' => ['min' => 10, 'max' => 15] ]); // Create a number property Property::create([ 'name' => 'age', 'label' => 'Age', 'type' => 'number', 'required' => true, 'validation' => ['min' => 0, 'max' => 120] ]); // Create a select property Property::create([ 'name' => 'status', 'label' => 'User Status', 'type' => 'select', 'required' => true, 'options' => ['active', 'inactive', 'pending'] ]);
3. Set and Get Properties
✅ Only after creating property definitions can you set values:
$user = User::find(1); // ✅ This works - property 'phone' was defined above $user->setDynamicProperty('phone', '+1234567890'); $user->setDynamicProperty('age', 25); $user->setDynamicProperty('status', 'active'); // ❌ This will throw PropertyNotFoundException $user->setDynamicProperty('undefined_property', 'value'); // Or use magic methods $user->prop_phone = '+1234567890'; $user->prop_age = 25; // Get properties $phone = $user->getDynamicProperty('phone'); $age = $user->prop_age; // Magic method $allProperties = $user->properties; // All properties as array // Set multiple properties at once $user->setProperties([ 'phone' => '+1234567890', 'age' => 25, 'status' => 'active' ]);
💡 Pro Tip: Use the Artisan command to create properties interactively:
php artisan dynamic-properties:create
4. Search by Properties
🔍 Search works with or without property definitions, but defining properties first is strongly recommended for type safety:
// ✅ RECOMMENDED: Search with defined properties (uses correct column types) $activeUsers = User::whereProperty('status', 'active')->get(); $youngUsers = User::whereProperty('age', '<', 30)->get(); // ⚠️ FALLBACK: Search undefined properties (uses value-based type detection) $results = User::whereProperty('undefined_prop', 'some_value')->get(); // Find users by multiple properties $users = User::whereProperties([ 'status' => 'active', 'age' => 25 ])->get();
⚠️ Common Pitfalls and Warnings
1. Property Definition Required for Setting Values
// ❌ WRONG - Will throw PropertyNotFoundException $user->setDynamicProperty('new_field', 'value'); // Property 'new_field' doesn't exist // ✅ CORRECT - Create property definition first Property::create([ 'name' => 'new_field', 'label' => 'New Field', 'type' => 'text' ]); $user->setDynamicProperty('new_field', 'value'); // Now it works
2. Type Safety Depends on Property Definitions
// ✅ With property definition - Type safe Property::create(['name' => 'score', 'type' => 'number']); $users = User::whereProperty('score', '>', 80); // Uses number_value column correctly // ⚠️ Without property definition - Fallback behavior $users = User::whereProperty('undefined_score', '>', 80); // Uses value-based type detection
3. Validation Only Works with Property Definitions
// ✅ With validation rules Property::create([ 'name' => 'email', 'type' => 'text', 'validation' => ['email', 'required'] ]); $user->setDynamicProperty('email', 'invalid-email'); // Throws PropertyValidationException // ❌ Without property definition - No validation possible // (Would throw PropertyNotFoundException before validation could occur)
4. Performance Impact
// ✅ FAST - Uses correct column and indexes Property::create(['name' => 'department', 'type' => 'text']); $users = User::whereProperty('department', 'engineering'); // Optimized query // ⚠️ SLOWER - Uses fallback type detection $users = User::whereProperty('undefined_dept', 'engineering'); // Less optimal
Advanced Usage
Property Types and Validation
Text Properties
Property::create([ 'name' => 'bio', 'label' => 'Biography', 'type' => 'text', 'validation' => [ 'min' => 10, // Minimum length 'max' => 500, // Maximum length 'required' => true // Required field ] ]);
Number Properties
Property::create([ 'name' => 'salary', 'label' => 'Annual Salary', 'type' => 'number', 'validation' => [ 'min' => 0, 'max' => 1000000, 'decimal_places' => 2 ] ]);
Date Properties
Property::create([ 'name' => 'hire_date', 'label' => 'Hire Date', 'type' => 'date', 'validation' => [ 'after' => '2020-01-01', 'before' => 'today' ] ]);
Boolean Properties
Property::create([ 'name' => 'newsletter_subscribed', 'label' => 'Newsletter Subscription', 'type' => 'boolean', 'required' => false ]);
Select Properties
Property::create([ 'name' => 'department', 'label' => 'Department', 'type' => 'select', 'options' => ['engineering', 'marketing', 'sales', 'hr'], 'required' => true ]);
Performance Optimization
JSON Column Caching
For maximum performance, add a JSON column to your existing tables:
// In a migration Schema::table('users', function (Blueprint $table) { $table->json('dynamic_properties')->nullable(); }); Schema::table('companies', function (Blueprint $table) { $table->json('dynamic_properties')->nullable(); });
This provides:
- < 1ms property retrieval (vs ~20ms without cache)
- Automatic synchronization when properties change
- Transparent fallback to EAV structure when cache is unavailable
Search Performance
The package automatically creates optimized indexes:
-- Indexes for fast property search
INDEX idx_string_search (entity_type, property_name, string_value)
INDEX idx_number_search (entity_type, property_name, number_value)
INDEX idx_date_search (entity_type, property_name, date_value)
INDEX idx_boolean_search (entity_type, property_name, boolean_value)
FULLTEXT INDEX ft_string_content (string_value)
Advanced Search
Complex Queries
use YourVendor\DynamicProperties\Services\PropertyService; $propertyService = app(PropertyService::class); // Advanced search with operators $results = $propertyService->search('App\\Models\\User', [ 'age' => ['value' => 25, 'operator' => '>='], 'salary' => ['value' => 50000, 'operator' => '>'], 'status' => 'active' ]);
Text Search
// Full-text search on text properties $users = User::whereRaw( "EXISTS (SELECT 1 FROM entity_properties ep WHERE ep.entity_id = users.id AND ep.entity_type = ? AND MATCH(ep.string_value) AGAINST(? IN BOOLEAN MODE))", ['App\\Models\\User', '+marketing +manager'] )->get();
Error Handling
The package provides comprehensive error handling:
use YourVendor\DynamicProperties\Exceptions\PropertyNotFoundException; use YourVendor\DynamicProperties\Exceptions\PropertyValidationException; try { $user->setDynamicProperty('nonexistent_property', 'value'); } catch (PropertyNotFoundException $e) { // Handle property not found echo "Property not found: " . $e->getMessage(); } try { $user->setDynamicProperty('age', 'invalid_number'); } catch (PropertyValidationException $e) { // Handle validation error echo "Validation failed: " . $e->getMessage(); }
Artisan Commands
The package includes helpful Artisan commands:
dynamic-properties:create Create a new dynamic property
dynamic-properties:delete Delete a dynamic property and all its values
dynamic-properties:list List all dynamic properties
dynamic-properties:optimize-db Optimize database for dynamic properties with database-specific enhancements
dynamic-properties:sync-cache Synchronize JSON cache columns with entity properties
API Reference
HasProperties Trait
Methods
setDynamicProperty(string $name, mixed $value): void
- Sets a single property value
- Validates the value against property rules
- Updates JSON cache if available
getDynamicProperty(string $name): mixed
- Retrieves a single property value
- Returns null if property doesn't exist
setProperties(array $properties): void
- Sets multiple properties at once
- More efficient than multiple setDynamicProperty calls
getPropertiesAttribute(): array
- Returns all properties as an associative array
- Uses JSON cache when available, falls back to EAV queries
Magic Methods
__get($key): mixed
- Access properties with
prop_
prefix - Example:
$user->prop_phone
gets the 'phone' property
__set($key, mixed $value): void
- Set properties with
prop_
prefix - Example:
$user->prop_phone = '+1234567890'
sets the 'phone' property
Query Scopes
whereProperty(string $name, mixed $value, string $operator = '='): Builder
- Filter entities by a single property
- Supports operators: =, !=, <, >, <=, >=, LIKE
whereProperties(array $properties): Builder
- Filter entities by multiple properties
- Uses AND logic between properties
PropertyService
setDynamicProperty(Model $entity, string $name, mixed $value): void
- Core method for setting property values
- Handles validation and storage
setProperties(Model $entity, array $properties): void
- Set multiple properties efficiently
search(string $entityType, array $filters): Collection
- Advanced search with complex criteria
- Supports multiple operators and property types
Performance Characteristics
Single Entity Property Retrieval
Method | Performance | Use Case |
---|---|---|
JSON Column Cache | < 1ms | Entities with many properties (50+) |
EAV Fallback | < 20ms | Entities with few properties |
Mixed Access | Automatic | Transparent performance optimization |
Search Performance
Dataset Size | Single Property | Multiple Properties | Full-Text Search |
---|---|---|---|
1K entities | < 10ms | < 50ms | < 100ms |
10K entities | < 50ms | < 200ms | < 500ms |
100K entities | < 200ms | < 1s | < 2s |
Memory Usage
- Property definitions: ~1KB per property
- Entity properties: ~100 bytes per property value
- JSON cache: ~50% reduction in query overhead
Database Compatibility
MySQL (Recommended)
- Full JSON support with native functions
- Full-text search capabilities
- Optimal performance with all features
SQLite
- JSON stored as TEXT with JSON1 extension
- Basic text search with LIKE queries
- All core functionality supported
Configuration
Publish the config file to customize behavior:
// config/dynamic-properties.php return [ // Default property validation rules 'default_validation' => [ 'text' => ['max' => 1000], 'number' => ['min' => -999999, 'max' => 999999], ], // Enable/disable JSON caching 'json_cache_enabled' => true, // Cache sync strategy 'cache_sync_strategy' => 'immediate', // 'immediate', 'deferred', 'manual' // Database-specific optimizations 'database_optimizations' => [ 'mysql' => [ 'use_json_functions' => true, 'enable_fulltext_search' => true, ], 'sqlite' => [ 'use_json1_extension' => true, ], ], ];
Troubleshooting
Common Errors and Solutions
PropertyNotFoundException
// Error: "Property 'phone' not found" $user->setDynamicProperty('phone', '+1234567890');
Solution: Create the property definition first:
Property::create(['name' => 'phone', 'type' => 'text']); $user->setDynamicProperty('phone', '+1234567890'); // Now works
PropertyValidationException
// Error: "Validation failed for property 'age'" $user->setDynamicProperty('age', -5);
Solution: Check property validation rules:
$property = Property::where('name', 'age')->first(); var_dump($property->validation); // See what rules are defined $user->setDynamicProperty('age', 25); // Use valid value
Inconsistent Search Results
// Getting different results for the same logical query $users1 = User::whereProperty('level', '>', 5)->get(); // 10 results $users2 = User::whereProperty('level', '>', '5')->get(); // 3 results
Solution: This happens when property definition is missing. Create it:
Property::create(['name' => 'level', 'type' => 'number']); // Now both queries will return the same results
Testing
The package includes comprehensive tests. Run them with:
# Run all tests ./vendor/bin/pest # Run specific test suites ./vendor/bin/pest tests/Unit ./vendor/bin/pest tests/Feature # Run with coverage ./vendor/bin/pest --coverage
Contributing
Please see CONTRIBUTING.md for details on how to contribute to this project.
License
This package is open-sourced software licensed under the MIT license.
Changelog
Please see CHANGELOG.md for recent changes.
Documentation
- Installation Guide - Detailed installation and setup instructions
- API Documentation - Complete API reference for all classes and methods
- Usage Examples - Comprehensive examples for common and advanced scenarios
- Performance Guide - Optimization strategies and performance benchmarks
- Contributing Guide - How to contribute to the project
- Changelog - Version history and changes