Skip to main content

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

Custom application events implementing BusinessEvent interface:

relatedEvents: [MediaUploadedEvent::class]

Processing Modes

ModeDescriptionUse Case
SYNCImmediate processing in the same requestFast computations (<100ms)
ASYNCQueue-based processingHeavy computations, external API calls

Cast Types

The system supports comprehensive type casting applied on data read:

Cast TypePHP TypeDescription
ArrayarrayPHP array (default)
Booleanbooltrue/false with intelligent conversion
IntegerintWhole numbers
FloatfloatDecimal numbers
StringstringText
ObjectstdClassPHP object
CollectionCollectionLaravel Collection
JsonstringJSON string
DateTimeCarbonCarbon datetime object
DateCarbonCarbon date (start of day)
TimestampintUnix timestamp
Type Casting Behavior

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_countMediaCountComputedValueHandler
  • view_statisticsViewStatisticsComputedValueHandler
  • last_activity_atLastActivityAtComputedValueHandler

Handler Location & Resolution

The system tries multiple paths in priority order:

Recommended for models with same key names:

App\Modules\{Module}\Services\ComputedValueHandlers\{Entity}\{Handler}

Example:

App\Modules\CMS\Services\ComputedValueHandlers\Post\MediaCountComputedValueHandler

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');
Field Protection

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:

  1. Event timestamp is generated when processing starts
  2. Before storing, existing timestamp is compared with current timestamp
  3. If existing data is newer (later timestamp), update is skipped
  4. Comprehensive logging tracks prevented race conditions
  5. Works for both sync and async processing modes

Example Flow:

Event A (10:00:00)HandlerStorage (10:00:05)Stored
Event B (10:00:02)HandlerStorage (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

PropertyValue
Cache Duration24 hours (86400 seconds)
Cache Keycomputed_values_system:model_discovery
Cache DriverLaravel default cache driver
Deployment Best Practice

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

  1. Use ASYNC mode for heavy computations
  2. Cache discovery results (automatically cached for 24 hours)
  3. Avoid N+1 queries in handlers
  4. Use specific event targeting with EloquentEventConfig
  5. Batch process multiple entity updates in handlers

Best Practices

  1. Use descriptive keys: media_count not mc or m_cnt
  2. Keep handlers focused: One responsibility per handler
  3. Avoid circular dependencies: Don't trigger events that cause infinite loops
  4. Use appropriate cast types: Match data type to usage
  5. Choose correct processing mode: Sync for fast operations, Async for heavy computations
  6. Handle edge cases: Always check for null values and empty collections
  7. Avoid N+1 queries: Use batch loading and eager loading in handlers
  8. Log important operations: Use dependency-injected logger in handlers
  9. Write comprehensive tests: Unit test handlers, integration test full flow
  10. 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)

Version: 1.0
Author: Behnam Moradi
Last Updated: December 2025