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.
- PHP: 8.3 or higher
- Laravel: 11.0 or higher
- Database: MySQL 8.0+ or SQLite 3.35+ (with JSON support)
- 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
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"
⚠️ IMPORTANT: You must create Property definitions before setting property values. Attempting to set a property that doesn't have a definition will throw aPropertyNotFoundException
.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use SolutionForest\LaravelDynamicProperties\Traits\HasProperties;
class User extends Model
{
use HasProperties;
// Your existing model code...
}
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']
]);
âś… 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
🔍 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();
// ❌ 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
// âś… 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
// âś… 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)
// âś… 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
Property::create([
'name' => 'bio',
'label' => 'Biography',
'type' => 'text',
'validation' => [
'min' => 10, // Minimum length
'max' => 500, // Maximum length
'required' => true // Required field
]
]);
Property::create([
'name' => 'salary',
'label' => 'Annual Salary',
'type' => 'number',
'validation' => [
'min' => 0,
'max' => 1000000,
'decimal_places' => 2
]
]);
Property::create([
'name' => 'hire_date',
'label' => 'Hire Date',
'type' => 'date',
'validation' => [
'after' => '2020-01-01',
'before' => 'today'
]
]);
Property::create([
'name' => 'newsletter_subscribed',
'label' => 'Newsletter Subscription',
'type' => 'boolean',
'required' => false
]);
Property::create([
'name' => 'department',
'label' => 'Department',
'type' => 'select',
'options' => ['engineering', 'marketing', 'sales', 'hr'],
'required' => true
]);
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
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)
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'
]);
// 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();
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();
}
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
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
__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
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
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
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 |
Dataset Size | Single Property | Multiple Properties | Full-Text Search |
---|---|---|---|
1K entities | < 10ms | < 50ms | < 100ms |
10K entities | < 50ms | < 200ms | < 500ms |
100K entities | < 200ms | < 1s | < 2s |
- Property definitions: ~1KB per property
- Entity properties: ~100 bytes per property value
- JSON cache: ~50% reduction in query overhead
- Full JSON support with native functions
- Full-text search capabilities
- Optimal performance with all features
- JSON stored as TEXT with JSON1 extension
- Basic text search with LIKE queries
- All core functionality supported
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,
],
],
];
// 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
// 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
// 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
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
Please see CONTRIBUTING.md for details on how to contribute to this project.
This package is open-sourced software licensed under the MIT license.
Please see CHANGELOG.md for recent changes.
- 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