An open API service indexing awesome lists of open source software.

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.

Awesome Lists containing this project

README

          

# Laravel Dynamic Properties

[![Tests](https://github.com/solutionforest/laravel-dynamic-properties/workflows/Tests/badge.svg)](https://github.com/solutionforest/laravel-dynamic-properties/actions)
[![Code Style](https://github.com/solutionforest/laravel-dynamic-properties/workflows/Code%20Style%20(Pint)/badge.svg)](https://github.com/solutionforest/laravel-dynamic-properties/actions)
[![Latest Stable Version](https://poser.pugx.org/solution-forest/laravel-dynamic-properties/v/stable)](https://packagist.org/packages/solution-forest/laravel-dynamic-properties)
[![License](https://poser.pugx.org/solution-forest/laravel-dynamic-properties/license)](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)