https://github.com/solutionforest/laravel-dynamic-properties
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.
https://github.com/solutionforest/laravel-dynamic-properties
Last synced: 23 days ago
JSON representation
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.
- Host: GitHub
- URL: https://github.com/solutionforest/laravel-dynamic-properties
- Owner: solutionforest
- License: mit
- Created: 2025-08-14T11:51:55.000Z (6 months ago)
- Default Branch: main
- Last Pushed: 2025-08-25T09:03:34.000Z (6 months ago)
- Last Synced: 2025-09-04T17:17:47.678Z (5 months ago)
- Language: PHP
- Size: 1.05 MB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
Awesome Lists containing this project
README
# Laravel Dynamic Properties
[](https://github.com/solutionforest/laravel-dynamic-properties/actions)
[/badge.svg)](https://github.com/solutionforest/laravel-dynamic-properties/actions)
[](https://packagist.org/packages/solution-forest/laravel-dynamic-properties)
[](https://packagist.org/packages/solution-forest/laravel-dynamic-properties)
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:
```bash
composer require solution-forest/laravel-dynamic-properties
```
Publish and run the migrations:
```bash
php artisan vendor:publish --provider="SolutionForest\LaravelDynamicProperties\DynamicPropertyServiceProvider" --tag="migrations"
php artisan migrate
```
Optionally, publish the configuration file:
```bash
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
'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:**
```php
$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:
> ```bash
> 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:**
```php
// ✅ 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
```php
// ❌ 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
```php
// ✅ 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
```php
// ✅ 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
```php
// ✅ 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
```php
Property::create([
'name' => 'bio',
'label' => 'Biography',
'type' => 'text',
'validation' => [
'min' => 10, // Minimum length
'max' => 500, // Maximum length
'required' => true // Required field
]
]);
```
#### Number Properties
```php
Property::create([
'name' => 'salary',
'label' => 'Annual Salary',
'type' => 'number',
'validation' => [
'min' => 0,
'max' => 1000000,
'decimal_places' => 2
]
]);
```
#### Date Properties
```php
Property::create([
'name' => 'hire_date',
'label' => 'Hire Date',
'type' => 'date',
'validation' => [
'after' => '2020-01-01',
'before' => 'today'
]
]);
```
#### Boolean Properties
```php
Property::create([
'name' => 'newsletter_subscribed',
'label' => 'Newsletter Subscription',
'type' => 'boolean',
'required' => false
]);
```
#### Select Properties
```php
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:
```php
// 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:
```sql
-- 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
```php
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
```php
// 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:
```php
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:
```bash
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:
```php
// 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`
```php
// Error: "Property 'phone' not found"
$user->setDynamicProperty('phone', '+1234567890');
```
**Solution**: Create the property definition first:
```php
Property::create(['name' => 'phone', 'type' => 'text']);
$user->setDynamicProperty('phone', '+1234567890'); // Now works
```
#### `PropertyValidationException`
```php
// Error: "Validation failed for property 'age'"
$user->setDynamicProperty('age', -5);
```
**Solution**: Check property validation rules:
```php
$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
```php
// 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:
```php
Property::create(['name' => 'level', 'type' => 'number']);
// Now both queries will return the same results
```
## Testing
The package includes comprehensive tests. Run them with:
```bash
# 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](CONTRIBUTING.md) for details on how to contribute to this project.
## License
This package is open-sourced software licensed under the [MIT license](LICENSE.md).
## Changelog
Please see [CHANGELOG.md](CHANGELOG.md) for recent changes.
## Documentation
- **[Installation Guide](INSTALLATION.md)** - Detailed installation and setup instructions
- **[API Documentation](docs/API.md)** - Complete API reference for all classes and methods
- **[Usage Examples](docs/EXAMPLES.md)** - Comprehensive examples for common and advanced scenarios
- **[Performance Guide](docs/PERFORMANCE.md)** - Optimization strategies and performance benchmarks
- **[Contributing Guide](CONTRIBUTING.md)** - How to contribute to the project
- **[Changelog](CHANGELOG.md)** - Version history and changes
## Credits
- [All Contributors](../../contributors)