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\PostCMS
- Example: App\Modules\LMS\Product\Entities\ProductLMS\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