Skip to main content

ComputedValues System - Usage Guide

This guide shows you how to use the ComputedValues system to automatically compute and cache derived data in your Laravel models.

Prerequisites

  • Laravel model with database table
  • Basic understanding of Laravel events
  • Familiarity with PHP interfaces and traits

Step-by-Step Implementation

Step 1: Add Database Column

Add a computed_values JSON column to your table:

Schema::table('posts', function (Blueprint $table) {
$table->json('computed_values')->nullable();
});

Step 2: Implement Interface and Trait

Update your model:

use App\Services\ComputedValues\Contracts\HasComputedValues;
use App\Services\ComputedValues\Traits\HasComputedValuesField;

class Post extends Model implements HasComputedValues
{
use HasComputedValuesField;

// ... rest of your model
}

Step 3: Define Computed Value Configuration

Add the getComputedValueConfig() method to your model:

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;

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::SYNC,
cast: ComputedValueCastEnum::Integer
),
]);
}

Step 4: Create Handler

Create a handler class in your module's Services/ComputedValueHandlers/ directory:

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
{
// Extract post ID from event payload
$payload = $eventDto->getPayload();
$postId = $payload->get('post_id');

if ($postId === null) {
return new ComputedValueResultCollection([]);
}

// Count media for this post
$mediaCount = MediaLibrary::where('post_id', $postId)->count();

// Return result
return new ComputedValueResultCollection([
new ComputedValueResult(
entityId: $postId,
value: $mediaCount
),
]);
}
}

Step 5: Register Discovery

Run the discovery command to register your configuration:

php artisan computed-values:discover
Automatic Cache Clearing

The discovery command automatically clears any existing cache before discovering to ensure fresh results.

Step 6: Access Computed Values

Access computed values in your code:

$post = Post::find(1);

// Access computed value (automatic accessor)
$mediaCount = $post->media_count;

// Check if value exists
if ($post->hasComputedValue('media_count')) {
echo "Media count: " . $post->media_count;
}

// Get update timestamp
$updatedAt = $post->getComputedValueUpdatedAt('media_count');
echo "Last updated: " . $updatedAt;

Configuration Options

Event Types

Use custom application events:

relatedEvents: [
MediaUploadedEvent::class,
MediaDeletedEvent::class,
]

Processing Modes

Processes immediately in the same request. Use for fast computations:

mode: ComputedValueModeEnum::SYNC

Best for:

  • Simple counts
  • Quick calculations (<100ms)
  • Operations without external dependencies

Data Types (Cast)

Specify the data type for your computed value:

cast: ComputedValueCastEnum::Integer

For whole numbers like counts, IDs, quantities.

Common Use Cases

Count how many related models exist:

Configuration
new ComputedValueConfig(
key: 'comment_count',
relatedEvents: [
new EloquentEventConfig(
modelClass: Comment::class,
events: [EloquentEventEnum::CREATED, EloquentEventEnum::DELETED]
)
],
mode: ComputedValueModeEnum::SYNC,
cast: ComputedValueCastEnum::Integer
)
Handler
public function handle(ComputedValueEvent $eventDto): ComputedValueResultCollection
{
$payload = $eventDto->getPayload();
$postId = $payload->get('post_id');

$count = Comment::where('post_id', $postId)->count();

return new ComputedValueResultCollection([
new ComputedValueResult($postId, $count),
]);
}

Use Case 2: Calculate Statistics

Calculate aggregated statistics:

Configuration
new ComputedValueConfig(
key: 'view_statistics',
relatedEvents: [PostViewedEvent::class],
mode: ComputedValueModeEnum::ASYNC,
cast: ComputedValueCastEnum::Array
)
Handler
public function handle(ComputedValueEvent $eventDto): ComputedValueResultCollection
{
$payload = $eventDto->getPayload();
$postId = $payload->get('post_id');

$stats = [
'total_views' => View::where('post_id', $postId)->count(),
'unique_views' => View::where('post_id', $postId)->distinct('user_id')->count(),
'today_views' => View::where('post_id', $postId)
->whereDate('created_at', today())
->count(),
];

return new ComputedValueResultCollection([
new ComputedValueResult($postId, $stats),
]);
}

Use Case 3: Check Status/Flags

Compute boolean flags:

Configuration
new ComputedValueConfig(
key: 'has_active_comments',
relatedEvents: [
new EloquentEventConfig(
modelClass: Comment::class,
events: [
EloquentEventEnum::CREATED,
EloquentEventEnum::UPDATED,
EloquentEventEnum::DELETED
]
)
],
mode: ComputedValueModeEnum::SYNC,
cast: ComputedValueCastEnum::Boolean
)
Handler
public function handle(ComputedValueEvent $eventDto): ComputedValueResultCollection
{
$payload = $eventDto->getPayload();
$postId = $payload->get('post_id');

$hasActive = Comment::where('post_id', $postId)
->where('status', 'active')
->exists();

return new ComputedValueResultCollection([
new ComputedValueResult($postId, $hasActive),
]);
}

Use Case 4: Last Activity Timestamp

Track when something last happened:

Configuration
new ComputedValueConfig(
key: 'last_activity_at',
relatedEvents: [
CommentAddedEvent::class,
PostLikedEvent::class,
PostSharedEvent::class,
],
mode: ComputedValueModeEnum::SYNC,
cast: ComputedValueCastEnum::DateTime
)
Handler
public function handle(ComputedValueEvent $eventDto): ComputedValueResultCollection
{
$payload = $eventDto->getPayload();
$postId = $payload->get('post_id');

$lastActivity = Activity::where('post_id', $postId)
->latest()
->first()
?->created_at;

return new ComputedValueResultCollection([
new ComputedValueResult($postId, $lastActivity ?? now()),
]);
}

Use Case 5: Complex Aggregations

Calculate complex derived data:

Configuration
new ComputedValueConfig(
key: 'engagement_metrics',
relatedEvents: [
PostViewedEvent::class,
PostLikedEvent::class,
CommentAddedEvent::class,
],
mode: ComputedValueModeEnum::ASYNC,
cast: ComputedValueCastEnum::Array
)
Handler
public function handle(ComputedValueEvent $eventDto): ComputedValueResultCollection
{
$payload = $eventDto->getPayload();
$postId = $payload->get('post_id');

$metrics = [
'engagement_score' => $this->calculateEngagementScore($postId),
'trending_rank' => $this->calculateTrendingRank($postId),
'virality_index' => $this->calculateViralityIndex($postId),
'calculated_at' => now()->toIso8601String(),
];

return new ComputedValueResultCollection([
new ComputedValueResult($postId, $metrics),
]);
}

Accessing Computed Values

Basic Access

$post = Post::find(1);

// Direct access (automatic accessor)
$mediaCount = $post->media_count;

// Check existence
if ($post->hasComputedValue('media_count')) {
echo $post->media_count;
}

Get Metadata

// Get last update timestamp
$updatedAt = $post->getComputedValueUpdatedAt('media_count');
// Returns: "2024-01-15T10:30:00+00:00" or null

// Get all statistics
$stats = $post->getCacheStatistics();
// Returns:
// [
// 'total_keys' => 3,
// 'cached_keys' => 2,
// 'empty_keys' => 1,
// 'keys_with_data' => ['media_count', 'view_stats'],
// 'keys_without_data' => ['last_activity'],
// 'last_updated' => '2024-01-15T10:30:00+00:00'
// ]

Get Multiple Values

// Get specific values
$data = $post->getComputedValueForKeys(['media_count', 'view_stats']);
// Returns:
// [
// 'media_count' => [
// 'data' => 42,
// 'has_data' => true,
// 'updated_at' => '2024-01-15T10:30:00+00:00'
// ],
// 'view_stats' => [...]
// ]

// Get all computed values
$allData = $post->getAllCacheData();

Check Completeness

// Check if all configured keys have data
if ($post->hasCompleteCacheData()) {
echo "All computed values are available";
}

In API Responses

Computed values automatically appear in JSON responses:

return response()->json($post);

// Output includes:
// {
// "id": 1,
// "title": "My Post",
// "media_count": 42,
// "view_stats": {...},
// ...
// }

Multiple Computed Values

You can define multiple computed values for a single model:

public function getComputedValueConfig(): ComputedValueConfigCollection
{
return new ComputedValueConfigCollection([
// Media count
new ComputedValueConfig(
key: 'media_count',
relatedEvents: [
new EloquentEventConfig(
modelClass: MediaLibrary::class,
events: [EloquentEventEnum::CREATED, EloquentEventEnum::DELETED]
)
],
mode: ComputedValueModeEnum::SYNC,
cast: ComputedValueCastEnum::Integer
),

// Comment count
new ComputedValueConfig(
key: 'comment_count',
relatedEvents: [
new EloquentEventConfig(
modelClass: Comment::class,
events: [EloquentEventEnum::CREATED, EloquentEventEnum::DELETED]
)
],
mode: ComputedValueModeEnum::SYNC,
cast: ComputedValueCastEnum::Integer
),

// View statistics
new ComputedValueConfig(
key: 'view_statistics',
relatedEvents: [PostViewedEvent::class],
mode: ComputedValueModeEnum::ASYNC,
cast: ComputedValueCastEnum::Array
),
]);
}
tip

Each computed value requires its own handler following the naming convention.

Handler Guidelines

Naming Convention

Handler class name must match the cache key:

Cache KeyHandler Class Name
media_countMediaCountComputedValueHandler
view_statisticsViewStatisticsComputedValueHandler
last_activity_atLastActivityAtComputedValueHandler

Handler Location

Place handlers in your module's Services/ComputedValueHandlers/ directory:

app/Modules/CMS/Services/ComputedValueHandlers/
MediaCountComputedValueHandler.php
CommentCountComputedValueHandler.php
ViewStatisticsComputedValueHandler.php

Handler Structure

<?php

declare(strict_types=1);

namespace App\Modules\{Module}\Services\ComputedValueHandlers;

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 {CacheKey}ComputedValueHandler implements ComputedValueHandler
{
public function handle(ComputedValueEvent $eventDto): ComputedValueResultCollection
{
// 1. Extract data from event
$payload = $eventDto->getPayload();
$entityId = $payload->get('entity_id');

// 2. Validate
if ($entityId === null) {
return new ComputedValueResultCollection([]);
}

// 3. Compute value
$value = $this->computeValue($entityId);

// 4. Return result
return new ComputedValueResultCollection([
new ComputedValueResult($entityId, $value),
]);
}

private function computeValue(int $entityId): mixed
{
// Your computation logic
return 0;
}
}

Troubleshooting

Values Not Updating

Check:

  1. Model implements HasComputedValues interface
  2. Model uses HasComputedValuesField trait
  3. computed_values column exists in database
  4. Handler class exists and follows naming convention
  5. Discovery has been run: php artisan computed-values:discover

View logs:

tail -f storage/logs/laravel.log | grep "Computed value"

Handler Not Found Error

Verify:

  1. Handler class name matches cache key (StudlyCase)
  2. Handler is in correct directory for your module
  3. Handler implements ComputedValueHandler interface
  4. Handler has handle() method

Async Jobs Not Processing

Check:

  1. Queue workers are running: php artisan queue:work
  2. Queue configuration is correct
  3. Failed jobs: php artisan queue:failed

Retry failed jobs:

php artisan queue:retry all

Direct Field Access Blocked

This is expected behavior. The system prevents direct manipulation:

// ❌ These will throw ComputedValueFieldAccessException
$post->computed_values = ['data' => 'value'];
$post->update(['computed_values' => ['data' => 'value']]);

// ✅ Use automatic accessors instead
$mediaCount = $post->media_count;
Field Protection

Direct access to the computed_values field is intentionally blocked. This protects data integrity and prevents race conditions. Always use the provided accessor methods.

Best Practices

  1. Use descriptive cache keys: media_count not mc
  2. Choose appropriate processing mode: SYNC for fast, ASYNC for slow
  3. Select correct cast type: Match your data type
  4. Handle null values: Always check for null in handlers
  5. Avoid N+1 queries: Use batch loading in handlers
  6. Log important operations: Use logger in handlers
  7. Test your handlers: Write unit and integration tests
  8. Clear cache after changes: Run discovery after configuration updates
  9. Monitor queue depth: Watch for ASYNC job backlogs
  10. Document your computed values: Comment why they exist

Commands

# Discover and register models (automatically clears cache first)
php artisan computed-values:discover

# Only clear discovery cache (without re-discovering)
php artisan computed-values:discover --clear

# Show discovery statistics (uses cached results if available)
php artisan computed-values:discover --stats

# Check queue status
php artisan queue:work

# View failed jobs
php artisan queue:failed

# Retry failed jobs
php artisan queue:retry all

Support

If you encounter issues:

  1. Check this guide
  2. Review logs in storage/logs/laravel.log
  3. Verify handler naming and location
  4. Run discovery command
  5. Check database for computed_values column
  6. Contact development team

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