Skip to main content

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.

  • ComputedValueManager delegates 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.

  • ComputedValueEventSubscriber observes 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​

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

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​

MethodPurpose
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

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​

MethodPurpose
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
Storage Behavior
  • 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​

PropertyValue
Cache Keycomputed_values_system:model_discovery
TTL86400 seconds (24 hours)
Cache DriverLaravel default cache driver

Key Methods​

MethodPurpose
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​

PHP Type: array

Conversion Examples:

[1, 2, 3] → [1, 2, 3]
'{"a":1}' → ['a' => 1]
stdClass → (array) $object

Error Handling​

  • Throws ComputedValueCastingException on 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​

MethodPurpose
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​

MediaStatsComputedValueHandler.php
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​

public function __construct(
private readonly LoggerInterface $logger,
private readonly SomeRepository $repository
) {}

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​

  1. Add to ComputedValueCastEnum
  2. 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
Security Considerations

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 ComputedValueHandler interface

Values Not Updating​

  • Check model implements HasComputedValues
  • Verify computed_values column 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