ComputedValues System - Developer Guide
Architecture Overview
The ComputedValues system is built on event-driven architecture principles with clear separation of concerns and adherence to SOLID principles.
Design Patterns
Strategy Pattern
Processing mode (Sync/Async) determined at runtime based on configuration.
ComputedValueManagerdelegates to sync or async processing strategies- Allows switching between immediate and queued processing without code changes
Observer Pattern
Event-driven architecture using Laravel's event system.
ComputedValueEventSubscriberobserves both BusinessEvents and Eloquent events- Loose coupling between event sources and computed value updates
Adapter Pattern
EloquentEventAdapter converts Eloquent events to standardized EventDTO.
- Transforms Eloquent model events into unified event format
- Enables consistent processing regardless of event source
Template Method Pattern
HasComputedValuesField trait defines algorithm skeleton.
- Trait provides base functionality with extension points
- Models customize behavior through configuration method
SOLID Principles
- SRP - Single Responsibility
- OCP - Open/Closed
- LSP - Liskov Substitution
- ISP - Interface Segregation
- DIP - Dependency Inversion
Each class has single, well-defined responsibility:
- ComputedValueManager: Orchestrates processing flow
- ComputedValueStorageService: Handles database persistence
- ComputedValueCastingService: Manages type conversions
- ModelDiscoveryService: Discovers and maps configurations
- EloquentEventAdapter: Converts Eloquent events to DTOs
System is open for extension, closed for modification:
- New cast types added via enum extension
- New event types supported through configuration
- New handlers added without modifying core system
All handlers implement ComputedValueHandler interface:
- Any handler can be substituted without breaking system
- Consistent contract across all handler implementations
Small, focused interfaces:
HasComputedValues: Single method for configurationComputedValueHandler: Single method for processing- No client forced to depend on unused methods
Dependency injection throughout:
- All services depend on abstractions (interfaces)
- Dependencies injected via constructor
- Easy to test and mock
Core Components
1. ComputedValueManager
Location: app/Services/ComputedValues/Services/ComputedValueManager.php
Responsibilities:
- Orchestrates computed value update process
- Resolves handler classes using naming convention
- Executes handlers with dependency injection
- Routes to sync or async processing based on configuration
- Provides data access methods for computed values
Handler Resolution Algorithm
Input: ComputedValueEvent, EntityClass
1. Extract module path from entity class namespace
- Example: App\Modules\CMS\Entities\Post → CMS
- Example: App\Modules\LMS\Product\Entities\Product → LMS\Product
2. Build handler name from computed value key
- Convert snake_case to StudlyCase
- Append 'ComputedValueHandler'
- Example: media_count → MediaCountComputedValueHandler
3. Try paths in priority order:
Priority 1: Entity subfolder
- App\Modules\{Module}\Services\ComputedValueHandlers\{Entity}\{Handler}
- Allows same key names across different entities
Priority 2: Direct path
- App\Modules\{Module}\Services\ComputedValueHandlers\{Handler}
- For unique handlers
Also supports:
- App\Core\{Module}\Services\ComputedValueHandlers\{Handler}
- App\Services\ComputedValueHandlers\{Module}\{Handler}
4. Return first existing class or throw exception
Key Methods
| Method | Purpose |
|---|---|
processComputedValueUpdate() | Main entry point for processing |
executeComputedValueHandler() | Resolves and executes handler |
buildHandlerClassName() | Constructs handler class name |
extractModulePathFromEntityClass() | Parses entity namespace |
processSyncUpdate() | Handles synchronous processing |
processAsyncUpdate() | Dispatches async job |
2. ComputedValueStorageService
Location: app/Services/ComputedValues/Services/ComputedValueStorageService.php
Responsibilities:
- Persists computed value results to database
- Implements optimistic locking for race condition prevention
- Manages database transactions for atomicity
- Provides data retrieval and clearing methods
Storage Structure
The computed_values JSON column stores data in this format:
{
"media_count": {
"data": 42,
"updated_at": "2024-01-15T10:30:00+00:00"
},
"view_statistics": {
"data": {"total": 1523, "today": 42},
"updated_at": "2024-01-15T10:35:00+00:00"
}
}
Race Condition Prevention
// Optimistic locking algorithm
1. Get existing data with timestamp
2. Parse both timestamps (existing and current)
3. Compare timestamps:
if (existingTimestamp > currentTimestamp) {
// Skip update - newer data exists
log warning with time difference
return
}
4. Update with new data and timestamp
5. Wrap in database transaction
Race condition warnings in logs are normal and indicate the system is working correctly. They mean a newer update already exists, preventing stale data from overwriting fresh data.
Key Methods
| Method | Purpose |
|---|---|
storeComputedValueResults() | Main storage method with transaction |
processStorageTransaction() | Processes batch of results |
updateEntityComputedValue() | Updates single entity with locking |
getExistingEntities() | Retrieves entities by IDs |
getComputedValueData() | Retrieves specific computed value |
clearComputedValueData() | Removes computed value entry |
- Data is stored as-is without casting
- Casting is applied on read by HasComputedValuesField trait
- Uses
withBypassedComputedValueProtection()for legitimate updates - All operations wrapped in database transactions
3. ModelDiscoveryService
Location: app/Services/ComputedValues/Services/ModelDiscoveryService.php
Responsibilities:
- Discovers all models implementing HasComputedValues interface
- Builds event-to-configuration mappings
- Caches discovery results for performance
- Handles both BusinessEvents and Eloquent events
Discovery Algorithm
Scanned Directories:
app/Models/app/Modules/*/Entities/app/Modules/*/Models/app/Core/*/Entities/app/Core/*/Models/
Cache Management
| Property | Value |
|---|---|
| Cache Key | computed_values_system:model_discovery |
| TTL | 86400 seconds (24 hours) |
| Cache Driver | Laravel default cache driver |
Key Methods
| Method | Purpose |
|---|---|
discoverModelsWithComputedValues() | Main discovery method |
getDiscoveryWithFallback() | Safe discovery with fallback |
clearDiscoveryCache() | Clears cached discovery |
findModelsWithComputedValues() | Scans directories |
scanDirectoryForClasses() | Scans single directory |
processModelClass() | Processes single model |
resolveEventIdentifier() | Resolves event to identifier |
4. ComputedValueCastingService
Location: app/Services/ComputedValues/Services/ComputedValueCastingService.php
Responsibilities:
- Casts data to specified types on read
- Handles type conversion edge cases
- Provides intelligent type coercion
- Logs casting failures with context
Supported Cast Types
- Array
- Boolean
- Integer
- Collection
- DateTime
PHP Type: array
Conversion Examples:
[1, 2, 3] → [1, 2, 3]
'{"a":1}' → ['a' => 1]
stdClass → (array) $object
PHP Type: bool
Intelligent Conversion:
"true" → true
"1" → true
"yes" → true
"on" → true
1 → true
0 → false
"false" → false
"no" → false
PHP Type: int
Conversion Examples:
"123" → 123
true → 1
false → 0
12.7 → 12
PHP Type: Illuminate\Support\Collection
Conversion Examples:
[1, 2, 3] → collect([1, 2, 3])
'{"a":1}' → collect(['a' => 1])
Collection → passthrough
PHP Type: Carbon\Carbon
Conversion Examples:
"2024-01-15" → Carbon instance
1705334400 → Carbon instance
Carbon instance → passthrough
Error Handling
- Throws
ComputedValueCastingExceptionon failure - Logs full context including data type and cast type
- Includes stack trace for debugging
5. EloquentEventAdapter
Location: app/Services/ComputedValues/Services/EloquentEventAdapter.php
Responsibilities:
- Converts Eloquent model events to EventDTO format
- Builds comprehensive event payload
- Generates correlation IDs for tracing
- Provides conditional update filtering
Conversion Process
Payload Structure
[
'model_class' => 'App\Modules\CMS\Entities\Post',
'model_id' => 123,
'event_type' => 'updated',
'attributes' => ['id' => 123, 'title' => 'New Title', ...],
'original' => ['title' => 'Old Title', ...], // For updates
'changes' => ['title' => 'New Title'], // For updates
'soft_deleted' => false // If applicable
]
Key Methods
| Method | Purpose |
|---|---|
adaptToEventDTO() | Main conversion method |
buildEventName() | Constructs event name |
buildPayload() | Builds comprehensive payload |
generateCorrelationId() | Creates correlation ID |
shouldTriggerUpdate() | Conditional update check |
Event Processing Flow
Handler Development
Template
final class MediaStatsComputedValueHandler implements ComputedValueHandler
{
public function __construct(
private readonly LoggerInterface $logger
) {}
public function handle(ComputedValueEvent $eventDto): ComputedValueResultCollection
{
$payload = $eventDto->getPayload();
$postId = $payload->get('post_id');
if ($postId === null) {
return new ComputedValueResultCollection([]);
}
$count = MediaLibrary::where('post_id', $postId)->count();
return new ComputedValueResultCollection([
new ComputedValueResult($postId, $count),
]);
}
}
Best Practices
- Dependency Injection
- Comprehensive Logging
- Avoid N+1 Queries
- Handle Edge Cases
- Batch Processing
public function __construct(
private readonly LoggerInterface $logger,
private readonly SomeRepository $repository
) {}
$this->logger->error('Handler failed', [
'entity_id' => $entityId,
'exception' => $e,
'trace' => $e->getTraceAsString(),
]);
// ✅ GOOD
$counts = MediaLibrary::whereIn('post_id', $postIds)
->groupBy('post_id')
->selectRaw('post_id, COUNT(*) as count')
->pluck('count', 'post_id');
// ❌ BAD
foreach ($postIds as $postId) {
$count = MediaLibrary::where('post_id', $postId)->count();
}
if ($entityId === null) {
return new ComputedValueResultCollection([]);
}
$entity = Entity::find($entityId);
if ($entity === null) {
$this->logger->warning('Entity not found', ['id' => $entityId]);
return new ComputedValueResultCollection([]);
}
$affectedIds = $this->findAffectedEntities($eventDto);
$entities = Entity::whereIn('id', $affectedIds)->get();
$results = [];
foreach ($entities as $entity) {
$results[] = new ComputedValueResult(
$entity->id,
$this->compute($entity)
);
}
return new ComputedValueResultCollection($results);
Advanced Configurations
Multiple Event Sources
new ComputedValueConfig(
key: 'engagement_score',
relatedEvents: [
PostViewedEvent::class, // Business event
CommentAddedEvent::class, // Business event
new EloquentEventConfig( // Eloquent event
modelClass: Comment::class,
events: [
EloquentEventEnum::CREATED,
EloquentEventEnum::DELETED
]
),
],
mode: ComputedValueModeEnum::ASYNC,
cast: ComputedValueCastEnum::Float
)
Conditional Processing
public function handle(ComputedValueEvent $eventDto): ComputedValueResultCollection
{
$changes = $eventDto->getPayload()->get('changes', []);
// Only process if specific attributes changed
if (!isset($changes['status']) && !isset($changes['published_at'])) {
return new ComputedValueResultCollection([]);
}
// Continue processing...
}
Complex Data Structures
return new ComputedValueResultCollection([
new ComputedValueResult($postId, [
'views' => ['total' => 1523, 'today' => 42],
'engagement' => ['likes' => 89, 'comments' => 23],
'metadata' => ['last_calculated' => now()->toIso8601String()],
]),
]);
Performance Optimization
1. Use ASYNC for Heavy Computations
mode: ComputedValueModeEnum::ASYNC
2. Target Specific Models
// ✅ Efficient - only MediaLibrary updates
new EloquentEventConfig(
modelClass: MediaLibrary::class,
events: [EloquentEventEnum::UPDATED]
)
3. Batch Operations
Process multiple entities in single handler execution.
4. Run Discovery After Configuration Changes
# Discovery automatically clears cache before scanning
php artisan computed-values:discover
5. Configure Queue Workers
php artisan queue:work --queue=computed-values --tries=3
Debugging
Check Discovery
php artisan computed-values:discover
Inspect Computed Values
$post = Post::find(1);
dd($post->getAllCacheData());
dd($post->getCacheStatistics());
dd($post->getComputedValueConfigSummary());
Monitor Queue
php artisan queue:failed
php artisan queue:retry all
Enable Debug Logging
$this->logger->debug('Processing', [
'payload' => $eventDto->getPayload()->toArray(),
]);
Testing
Unit Test Handler
public function test_handler_computes_correctly()
{
$event = new ComputedValueEvent(
eventName: 'MediaCreated',
eventClass: MediaUploadedEvent::class,
originalEventData: new EventDTO(...),
cacheKey: 'media_stats'
);
$handler = new MediaStatsComputedValueHandler();
$results = $handler->handle($event);
$this->assertInstanceOf(ComputedValueResultCollection::class, $results);
}
Integration Test
public function test_computed_value_updates_on_event()
{
$post = Post::factory()->create();
event(new MediaUploadedEvent($media));
$post->refresh();
$this->assertTrue($post->hasComputedValue('media_stats'));
$this->assertEquals(1, $post->media_stats);
}
Extending the System
Add New Cast Type
- Add to
ComputedValueCastEnum - Implement casting in
ComputedValueCastingService
// In ComputedValueCastEnum
case Decimal = 'decimal';
// In ComputedValueCastingService
private function castToDecimal(mixed $data): string
{
return number_format((float) $data, 2, '.', '');
}
Custom Event Adapter
final class CustomEventAdapter
{
public function adaptToComputedValueEvent($event): ComputedValueEvent
{
return new ComputedValueEvent(...);
}
}
Custom Discovery
final class CustomDiscovery extends ModelDiscoveryService
{
protected function getModelDirectories(): array
{
$dirs = parent::getModelDirectories();
$dirs[] = app_path('CustomModels');
return $dirs;
}
}
Internal APIs
ComputedValueManager
processComputedValueUpdate(
ComputedValueEvent $event,
ComputedValueConfig $config,
string $entityClass
): void
getComputedValueData(
string $entityClass,
int|string $entityId,
string $key
): ?array
clearComputedValueData(
string $entityClass,
int|string $entityId,
string $key
): bool
ComputedValueStorageService
storeComputedValueResults(
string $entityClass,
string $cacheKey,
Collection $results,
ComputedValueConfig $config,
?string $eventTimestamp
): void
getComputedValueData(
string $entityClass,
int|string $entityId,
string $key
): ?array
clearComputedValueData(
string $entityClass,
int|string $entityId,
string $key
): bool
ModelDiscoveryService
discoverModelsWithComputedValues(): array
clearDiscoveryCache(): void
ComputedValueCastingService
castData(mixed $data, ComputedValueCastEnum $cast): mixed
Security
- Field protection prevents direct manipulation
- Validation on all configurations
- Enum-based type safety prevents invalid configurations
- Bypass mechanism only for storage service
- Audit trail with timestamps and correlation IDs
- Mass assignment protection via trait
- Query protection via global scope
- Handler isolation with dependency injection
The computed_values field is protected at multiple levels to ensure data integrity. Only the storage service can bypass protection through a controlled callback mechanism.
Common Issues
Handler Not Found
- Check naming:
{CacheKey}ComputedValueHandler - Verify location matches module structure
- Ensure implements
ComputedValueHandlerinterface
Values Not Updating
- Check model implements
HasComputedValues - Verify
computed_valuescolumn exists - Run discovery:
php artisan computed-values:discover - Check logs for errors
Race Condition Warnings
Normal behavior - indicates newer data exists, preventing stale updates.
Author: Behnam Moradi
Version: 1.0
Last Updated: December 2024