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:
<?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
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
- Business Events
- Eloquent Events
- Specific Model Events
Use custom application events:
relatedEvents: [
MediaUploadedEvent::class,
MediaDeletedEvent::class,
]
Use model lifecycle events:
relatedEvents: [
EloquentEventEnum::CREATED,
EloquentEventEnum::UPDATED,
EloquentEventEnum::DELETED,
]
Target specific models:
relatedEvents: [
new EloquentEventConfig(
modelClass: MediaLibrary::class,
events: [EloquentEventEnum::CREATED, EloquentEventEnum::DELETED]
),
]
Processing Modes
- Synchronous (SYNC)
- Asynchronous (ASYNC)
Processes immediately in the same request. Use for fast computations:
mode: ComputedValueModeEnum::SYNC
Best for:
- Simple counts
- Quick calculations (<100ms)
- Operations without external dependencies
Processes in background queue. Use for heavy computations:
mode: ComputedValueModeEnum::ASYNC
Best for:
- Complex aggregations
- External API calls
- Heavy database queries
- Operations that may take >100ms
Data Types (Cast)
Specify the data type for your computed value:
- Integer
- Boolean
- Array
- String
- Float
- DateTime
- Collection
- JSON
cast: ComputedValueCastEnum::Integer
For whole numbers like counts, IDs, quantities.
cast: ComputedValueCastEnum::Boolean
For true/false flags, status checks.
cast: ComputedValueCastEnum::Array
For complex data structures (default).
cast: ComputedValueCastEnum::String
For text values, labels, descriptions.
cast: ComputedValueCastEnum::Float
For decimal numbers, percentages, averages.
cast: ComputedValueCastEnum::DateTime
For Carbon datetime objects.
cast: ComputedValueCastEnum::Collection
For Laravel Collection instances.
cast: ComputedValueCastEnum::Json
For JSON string representation.
Common Use Cases
Use Case 1: Count Related Models
Count how many related models exist:
new ComputedValueConfig(
key: 'comment_count',
relatedEvents: [
new EloquentEventConfig(
modelClass: Comment::class,
events: [EloquentEventEnum::CREATED, EloquentEventEnum::DELETED]
)
],
mode: ComputedValueModeEnum::SYNC,
cast: ComputedValueCastEnum::Integer
)
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:
new ComputedValueConfig(
key: 'view_statistics',
relatedEvents: [PostViewedEvent::class],
mode: ComputedValueModeEnum::ASYNC,
cast: ComputedValueCastEnum::Array
)
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:
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
)
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:
new ComputedValueConfig(
key: 'last_activity_at',
relatedEvents: [
CommentAddedEvent::class,
PostLikedEvent::class,
PostSharedEvent::class,
],
mode: ComputedValueModeEnum::SYNC,
cast: ComputedValueCastEnum::DateTime
)
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:
new ComputedValueConfig(
key: 'engagement_metrics',
relatedEvents: [
PostViewedEvent::class,
PostLikedEvent::class,
CommentAddedEvent::class,
],
mode: ComputedValueModeEnum::ASYNC,
cast: ComputedValueCastEnum::Array
)
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
),
]);
}
Each computed value requires its own handler following the naming convention.
Handler Guidelines
Naming Convention
Handler class name must match the cache key:
| Cache Key | Handler Class Name |
|---|---|
media_count | MediaCountComputedValueHandler |
view_statistics | ViewStatisticsComputedValueHandler |
last_activity_at | LastActivityAtComputedValueHandler |
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:
- Model implements
HasComputedValuesinterface - Model uses
HasComputedValuesFieldtrait computed_valuescolumn exists in database- Handler class exists and follows naming convention
- 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:
- Handler class name matches cache key (StudlyCase)
- Handler is in correct directory for your module
- Handler implements
ComputedValueHandlerinterface - Handler has
handle()method
Async Jobs Not Processing
Check:
- Queue workers are running:
php artisan queue:work - Queue configuration is correct
- 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;
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
- Use descriptive cache keys:
media_countnotmc - Choose appropriate processing mode: SYNC for fast, ASYNC for slow
- Select correct cast type: Match your data type
- Handle null values: Always check for null in handlers
- Avoid N+1 queries: Use batch loading in handlers
- Log important operations: Use logger in handlers
- Test your handlers: Write unit and integration tests
- Clear cache after changes: Run discovery after configuration updates
- Monitor queue depth: Watch for ASYNC job backlogs
- 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:
- Check this guide
- Review logs in
storage/logs/laravel.log - Verify handler naming and location
- Run discovery command
- Check database for
computed_valuescolumn - Contact development team
Author: Behnam Moradi
Version: 1.0
Last Updated: December 2024