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