ComputedValues System
Overview
The ComputedValues system is an event-driven architecture for automatically computing and caching derived data in Laravel models. It eliminates manual cache management by automatically updating computed values when business events or Eloquent model lifecycle events occur.
Key Features
- Event-Driven Architecture: Automatically responds to BusinessEvents and Eloquent model events
- Type Safety: Full enum-based type system with comprehensive validation
- Sync/Async Processing: Strategy pattern for synchronous or queue-based processing
- Race Condition Prevention: Optimistic locking using ISO 8601 timestamps
- Field Protection: Prevents direct manipulation of computed_values field
- Type Casting: Automatic data type conversion on read (integer, boolean, array, JSON, datetime, etc.)
- Model Discovery: Automatic scanning and registration of models with computed values
- Comprehensive Logging: Full context logging for debugging and monitoring
- Handler Resolution: Convention-based handler discovery with entity subfolder support
Quick Start
1. Implement the Interface
use App\Services\ComputedValues\Contracts\HasComputedValues;
use App\Services\ComputedValues\Traits\HasComputedValuesField;
use App\Services\ComputedValues\Collections\ComputedValueConfigCollection;
use App\Services\ComputedValues\DTOs\ComputedValueConfig;
use App\Services\ComputedValues\DTOs\EloquentEventConfig;
use App\Services\ComputedValues\Enums\ComputedValueCastEnum;
use App\Services\ComputedValues\Enums\ComputedValueModeEnum;
use App\Services\ComputedValues\Enums\EloquentEventEnum;
class Post extends Model implements HasComputedValues
{
use HasComputedValuesField;
public function getComputedValueConfig(): ComputedValueConfigCollection
{
return new ComputedValueConfigCollection([
new ComputedValueConfig(
key: 'media_count',
relatedEvents: [
new EloquentEventConfig(
modelClass: MediaLibrary::class,
events: [
EloquentEventEnum::CREATED,
EloquentEventEnum::DELETED
]
)
],
mode: ComputedValueModeEnum::ASYNC,
cast: ComputedValueCastEnum::Integer
),
]);
}
}
2. Add computed_values Column
Schema::table('posts', function (Blueprint $table) {
$table->json('computed_values')->nullable();
});
3. Create Handler
Create handler in app/Modules/CMS/Services/ComputedValueHandlers/MediaCountComputedValueHandler.php:
<?php
declare(strict_types=1);
namespace App\Modules\CMS\Services\ComputedValueHandlers;
use App\Core\Media\Entities\MediaLibrary;
use App\Services\ComputedValues\Collections\ComputedValueResultCollection;
use App\Services\ComputedValues\Contracts\ComputedValueHandler;
use App\Services\ComputedValues\DTOs\ComputedValueEvent;
use App\Services\ComputedValues\DTOs\ComputedValueResult;
final class MediaCountComputedValueHandler implements ComputedValueHandler
{
public function handle(ComputedValueEvent $eventDto): ComputedValueResultCollection
{
$payload = $eventDto->getPayload();
$attributes = $payload->get('attributes');
if ($attributes === null) {
return new ComputedValueResultCollection([]);
}
$postId = $attributes['post_id'] ?? null;
if ($postId === null) {
return new ComputedValueResultCollection([]);
}
$mediaCount = MediaLibrary::where('post_id', $postId)->count();
return new ComputedValueResultCollection([
new ComputedValueResult(
entityId: (int) $postId,
value: $mediaCount
),
]);
}
}
4. Run Discovery
php artisan computed-values:discover
5. Access Computed Values
$post = Post::find(1);
// Automatic accessor with type casting
$mediaCount = $post->media_count; // Returns integer (cast applied)
// Check existence
$hasData = $post->hasComputedValue('media_count');
// Get metadata
$updatedAt = $post->getComputedValueUpdatedAt('media_count');
Architecture
Event Flow
Directory Structure
app/Services/ComputedValues/
├── Collections/ # Typed collections
│ ├── ComputedValueConfigCollection.php
│ └── ComputedValueResultCollection.php
├── Console/ # Artisan commands
│ └── ComputedValueDiscoveryCommand.php
├── Contracts/ # Interfaces
│ ├── ComputedValueHandler.php
│ └── HasComputedValues.php
├── DTOs/ # Data Transfer Objects
│ ├── ComputedValueConfig.php
│ ├── ComputedValueEvent.php
│ ├── ComputedValueResult.php
│ └── EloquentEventConfig.php
├── Enums/ # Enumerations
│ ├── ComputedValueCastEnum.php
│ ├── ComputedValueModeEnum.php
│ └── EloquentEventEnum.php
├── Exceptions/ # Custom exceptions
│ ├── ComputedValueCastingException.php
│ ├── ComputedValueFieldAccessException.php
│ └── ... (21 specific exceptions)
├── Jobs/ # Queue jobs
│ └── ProcessComputedValueUpdateJob.php
├── Listeners/ # Event subscribers
│ └── ComputedValueEventSubscriber.php
├── Providers/ # Service providers
│ └── ComputedValuesServiceProvider.php
├── Services/ # Core services
│ ├── ComputedValueCastingService.php
│ ├── ComputedValueManager.php
│ ├── ComputedValueStorageService.php
│ ├── EloquentEventAdapter.php
│ └── ModelDiscoveryService.php
└── Traits/ # Reusable traits
└── HasComputedValuesField.php
Configuration Options
ComputedValueConfig
new ComputedValueConfig(
key: 'computed_value_key', // Snake_case key (must start with letter)
relatedEvents: [ // Events that trigger recomputation
BusinessEventClass::class, // BusinessEvent class name
new EloquentEventConfig( // Or specific model + events config
modelClass: SomeModel::class,
events: [
EloquentEventEnum::CREATED,
EloquentEventEnum::UPDATED
]
)
],
mode: ComputedValueModeEnum::SYNC, // Sync or Async processing
cast: ComputedValueCastEnum::Integer // Target data type (applied on read)
);
Event Types
- Business Events
- Eloquent Events
- Mixed Events
Custom application events implementing BusinessEvent interface:
relatedEvents: [MediaUploadedEvent::class]
Target specific models with EloquentEventConfig:
relatedEvents: [
new EloquentEventConfig(
modelClass: MediaLibrary::class,
events: [
EloquentEventEnum::CREATED,
EloquentEventEnum::DELETED
]
)
]
Combine BusinessEvents and Eloquent events:
relatedEvents: [
MediaUploadedEvent::class, // BusinessEvent
new EloquentEventConfig( // Eloquent event
modelClass: Comment::class,
events: [EloquentEventEnum::CREATED]
)
]
Processing Modes
| Mode | Description | Use Case |
|---|---|---|
| SYNC | Immediate processing in the same request | Fast computations (<100ms) |
| ASYNC | Queue-based processing | Heavy computations, external API calls |
Cast Types
The system supports comprehensive type casting applied on data read:
| Cast Type | PHP Type | Description |
|---|---|---|
Array | array | PHP array (default) |
Boolean | bool | true/false with intelligent conversion |
Integer | int | Whole numbers |
Float | float | Decimal numbers |
String | string | Text |
Object | stdClass | PHP object |
Collection | Collection | Laravel Collection |
Json | string | JSON string |
DateTime | Carbon | Carbon datetime object |
Date | Carbon | Carbon date (start of day) |
Timestamp | int | Unix timestamp |
Casting is applied when reading data, not when storing. Data is stored as-is in the JSON field, ensuring flexibility and preventing data loss.
Handler Naming Convention
Handlers must follow this naming pattern:
{Key}ComputedValueHandler
Examples:
media_count→MediaCountComputedValueHandlerview_statistics→ViewStatisticsComputedValueHandlerlast_activity_at→LastActivityAtComputedValueHandler
Handler Location & Resolution
The system tries multiple paths in priority order:
- Priority 1: Entity Subfolder
- Priority 2: Direct Path
- Alternative Paths
Recommended for models with same key names:
App\Modules\{Module}\Services\ComputedValueHandlers\{Entity}\{Handler}
Example:
App\Modules\CMS\Services\ComputedValueHandlers\Post\MediaCountComputedValueHandler
For unique handlers:
App\Modules\{Module}\Services\ComputedValueHandlers\{Handler}
Example:
App\Modules\CMS\Services\ComputedValueHandlers\MediaCountComputedValueHandler
Also supports:
App\Core\{Module}\Services\ComputedValueHandlers\{Handler}
App\Services\ComputedValueHandlers\{Module}\{Handler}
Field Protection
The system prevents direct manipulation of the computed_values field:
// ❌ BLOCKED - Throws ComputedValueFieldAccessException
$post->computed_values = ['data' => 'value'];
$post->update(['computed_values' => ['data' => 'value']]);
Post::where('computed_values->key', 'value')->get();
// ✅ ALLOWED - Access through automatic accessors
$mediaStats = $post->media_stats;
$hasData = $post->hasComputedValue('media_stats');
$updatedAt = $post->getComputedValueUpdatedAt('media_stats');
Direct access to the computed_values field is intentionally blocked to maintain data integrity and prevent race conditions. Always use the provided accessor methods.
Race Condition Prevention
The system uses optimistic locking with ISO 8601 timestamps:
- Event timestamp is generated when processing starts
- Before storing, existing timestamp is compared with current timestamp
- If existing data is newer (later timestamp), update is skipped
- Comprehensive logging tracks prevented race conditions
- Works for both sync and async processing modes
Example Flow:
Event A (10:00:00) → Handler → Storage (10:00:05) ✓ Stored
Event B (10:00:02) → Handler → Storage (10:00:06) ✗ Skipped (A is newer)
Model Discovery
The system automatically discovers models implementing HasComputedValues:
# Discover and cache model configurations
# Note: This automatically clears the cache before discovery
php artisan computed-values:discover
# Only clear discovery cache (without re-discovering)
php artisan computed-values:discover --clear
# Show discovery statistics
php artisan computed-values:discover --stats
Discovery Scans
The discovery process scans these directories:
app/Models/app/Modules/*/Entities/app/Modules/*/Models/app/Core/*/Entities/app/Core/*/Models/
Discovery Cache Management
| Property | Value |
|---|---|
| Cache Duration | 24 hours (86400 seconds) |
| Cache Key | computed_values_system:model_discovery |
| Cache Driver | Laravel default cache driver |
Always run the discovery command during deployment to pre-warm the cache before the application comes back online. This eliminates overhead on the first request after deployment.
Helper Methods
Check Data Existence
$post->hasComputedValue('media_stats'); // bool
Get Update Timestamp
$post->getComputedValueUpdatedAt('media_stats'); // ISO 8601 string or null
Get Statistics
$stats = $post->getComputedValueStatistics();
// Returns:
// [
// 'total_keys' => 3,
// 'keys_with_data' => 2,
// 'keys_without_data' => 1,
// 'configured_keys' => ['media_count', 'view_count', 'last_activity'],
// 'keys_with_values' => ['media_count', 'view_count'],
// 'keys_without_values' => ['last_activity'],
// 'last_updated' => '2024-01-15T10:30:00+00:00'
// ]
Get Multiple Values
$data = $post->getComputedValueForKeys(['media_count', 'view_count']);
// Returns:
// [
// 'media_count' => ['data' => 42, 'has_data' => true, 'updated_at' => '...'],
// 'view_count' => ['data' => 1523, 'has_data' => true, 'updated_at' => '...']
// ]
Get All Computed Values
$allData = $post->getAllComputedValueData();
// Returns raw computed_values JSON data
Check Complete Data
$post->hasCompleteComputedValueData(); // bool - true if all keys have data
Performance Considerations
- Use ASYNC mode for heavy computations
- Cache discovery results (automatically cached for 24 hours)
- Avoid N+1 queries in handlers
- Use specific event targeting with EloquentEventConfig
- Batch process multiple entity updates in handlers
Best Practices
- Use descriptive keys:
media_countnotmcorm_cnt - Keep handlers focused: One responsibility per handler
- Avoid circular dependencies: Don't trigger events that cause infinite loops
- Use appropriate cast types: Match data type to usage
- Choose correct processing mode: Sync for fast operations, Async for heavy computations
- Handle edge cases: Always check for null values and empty collections
- Avoid N+1 queries: Use batch loading and eager loading in handlers
- Log important operations: Use dependency-injected logger in handlers
- Write comprehensive tests: Unit test handlers, integration test full flow
- Monitor performance: Watch queue depths for Async operations
System Requirements
- PHP 8.1+
- Laravel 10+
- MySQL 5.7+ or PostgreSQL 9.5+ (for JSON column support)
- Redis (optional, for queue processing)
Related Documentation
- Usage Guide: Step-by-step implementation guide with examples
- Developer Guide: Technical architecture and advanced topics
Version: 1.0
Author: Behnam Moradi
Last Updated: December 2025