Cache Field Protection
Introduction
The Cache Field Protection system provides a clean and isolated structure for protecting the cache field in database models. This system ensures that:
- No model can directly update the
cachefield - Automatic accessors are created for all cache keys
- Direct querying on the
cachefield is prevented
This protection layer is essential for maintaining data integrity and preventing accidental or unauthorized modifications to cached data.
Architecture Components
HasCacheField Trait
The HasCacheField trait provides all the functionality needed to protect and manage the cache field:
trait HasCacheField
{
// Methods for protecting cache field from direct changes
// Automatic accessor generation
// Prevention of direct queries
// Helper methods for cache management
// Cache statistics and reporting
// Safe access to cache data
}
Key Features:
- Prevents direct
setAttributeon the cache field - Creates automatic accessors using the
__callmagic method - Protects against direct queries with global scope
- Hides the cache field from
toArray/toJson - Provides statistics and reporting methods
- Integrates fully with the cache system
CacheFieldAccessException
A specialized exception for cache field access violations.
Cache System Bypass Mechanism
The cache system includes a bypass mechanism for legitimate updates. This mechanism allows only the cache system itself to update the cache field:
// Internal use in CacheStorageService
$entity->withBypassedCacheProtection(function () use ($entity, $cacheData) {
$entity->update(['cache' => $cacheData]);
});
Key Features:
- Designed for internal use by the cache system only
- Thread-safe and exception-safe
- Bypass mode is automatically restored
- Prevents misuse
Automatic Accessors
The system automatically creates an accessor for each defined cache key:
// If cache config includes 'profile_data' key
$user->profile_data; // Automatically translates to getCacheValue('profile_data')
// If cache config includes 'user_statistics' key
$user->user_statistics; // Automatically translates to getCacheValue('user_statistics')
How it works:
- When calling
$model->some_key - The system checks if
some_keyis defined in the cache config - If yes, the value is returned from
cache['some_key']['data'] - If no, it continues with the normal Laravel flow
Cache Data Structure
Cache data is stored in the database with the following structure:
{
"profile_data": {
"data": {
"name": "John Doe",
"avatar_url": "https://example.com/avatar.jpg",
"completion_percentage": 85
},
"updated_at": "2023-12-01T10:30:00Z"
},
"statistics": {
"data": {
"total_posts": 42,
"total_likes": 156,
"engagement_rate": 0.75
},
"updated_at": "2023-12-01T11:15:00Z"
}
}
Helper Methods
Cache Statistics
$user = User::find(1);
$stats = $user->getCacheStatistics();
// Result:
[
'total_keys' => 2,
'cached_keys' => 1,
'empty_keys' => 1,
'keys_with_data' => ['profile_data'],
'keys_without_data' => ['statistics'],
'last_updated' => '2023-12-01T10:30:00Z'
]
Cache Configuration Summary
$user = User::find(1);
$summary = $user->getCacheConfigSummary();
// Result:
[
[
'key' => 'profile_data',
'events' => ['App\\Events\\UserUpdatedEvent'],
'module' => 'User',
'mode' => 'sync',
'has_data' => true,
'updated_at' => '2023-12-01T10:30:00Z'
],
[
'key' => 'statistics',
'events' => ['App\\Events\\UserActivityEvent'],
'module' => 'User',
'mode' => 'async',
'has_data' => false,
'updated_at' => null
]
]
Multiple Cache Keys Access
$user = User::find(1);
// Get data for multiple keys
$data = $user->getCacheDataForKeys(['profile_data', 'statistics']);
// Get all cache data
$allData = $user->getAllCacheData();
JSON Serialization
When converting a model to array or JSON:
$user = User::find(1);
$array = $user->toArray();
// Result includes:
// - All normal model attributes
// - Cache-derived attributes (profile_data, statistics, etc.)
// - No direct 'cache' field
Safe vs. Unsafe Operations
✅ SAFE Operations
$user = User::find(1);
// Access cache data through automatic accessors
$profileData = $user->profile_data;
$statistics = $user->statistics;
// Check cache data existence
$hasProfile = $user->hasCacheData('profile_data');
$hasStats = $user->hasCacheData('statistics');
// Access with type casting for better type safety
$viewCount = (int) $user->view_count; // Ensure integer type
$isActive = (bool) $user->is_active; // Ensure boolean type
$createdDate = $user->created_date instanceof Carbon
? $user->created_date
: Carbon::now(); // Handle potential null values
// Get cache metadata
$profileUpdatedAt = $user->getCacheUpdatedAt('profile_data');
$statsUpdatedAt = $user->getCacheUpdatedAt('statistics');
// Get cache statistics
$cacheStats = $user->getCacheStatistics();
echo "Cache completeness: {$cacheStats['cached_keys']}/{$cacheStats['total_keys']}";
// Get all cache data
$allCacheData = $user->getAllCacheData();
// Check completeness
$isComplete = $user->hasCompleteCacheData();
// Normal model operations
$user->name = 'Updated Name';
$user->save(); // ✅ Works fine
// Mass assignment (non-cache fields)
$user->fill(['name' => 'New Name', 'email' => 'new@email.com']);
// Normal queries
$activeUsers = User::where('status', 'active')->get();
❌ UNSAFE Operations (Will Throw Exceptions)
$user = User::find(1);
// Direct cache field access
$cacheData = $user->cache; // ❌ CacheFieldAccessException
// Direct cache field assignment
$user->cache = ['data' => 'value']; // ❌ CacheFieldAccessException
// Mass assignment to cache field
$user->fill(['cache' => ['data' => 'value']]); // ❌ CacheFieldAccessException
// Direct cache field update
$user->update(['cache' => ['data' => 'value']]); // ❌ CacheFieldAccessException
// Querying on cache field
User::where('cache->profile_data', '!=', null)->get(); // ❌ CacheFieldAccessException
// Using whereCache scope
User::whereCache('profile_data', 'value')->get(); // ❌ CacheFieldAccessException
Error Handling
CacheFieldAccessException
This exception is thrown in the following cases:
- Direct Update: Attempting to update the cache field directly
- Direct Query: Attempting to query directly on the cache field
- Mass Assignment: Attempting to mass assign to the cache field
try {
$user->cache = ['data' => 'value'];
} catch (CacheFieldAccessException $e) {
// Handle the error
logger()->warning('Cache field access violation', [
'message' => $e->getMessage(),
'user_id' => $user->id
]);
}
Performance Considerations
Memory Usage
- The cache field is excluded from
toArray()to reduce JSON size - Cache-derived attributes are only shown when data exists
Database Queries
- No additional queries are performed to access cache data
- All operations are performed on loaded data
Caching Strategy
- Cache configurations are kept in memory
- No additional overhead for accessors
Best Practices
1. Cache Key Naming
// ✅ Good: descriptive and consistent
'user_profile_data'
'post_statistics'
'product_recommendations'
// ❌ Bad: vague or inconsistent
'data'
'info'
'stuff'
2. Cache Configuration
// ✅ Good: specific events and clear purpose
new CacheConfigDTO(
key: 'user_engagement_metrics',
relatedEvents: [
UserPostCreatedEvent::class,
UserCommentCreatedEvent::class,
UserLikeGivenEvent::class,
],
sourceModule: 'User',
sourceEntity: User::class,
mode: CacheModeEnum::ASYNC
)
3. Error Handling
// ✅ Good: graceful handling
public function getUserProfile(int $userId): ?array
{
$user = User::find($userId);
if (!$user) {
return null;
}
// Safe cache access
$profileData = $user->profile_data;
if (!$profileData) {
// Trigger cache refresh if needed
event(new UserProfileRequested($user));
return null;
}
return $profileData;
}
Troubleshooting
Common Issues
1. "Cache field access not allowed"
Cause: Attempting to directly access the cache field Solution: Use automatic accessors
2. "Querying on cache field is not allowed"
Cause: Attempting to query directly on the cache field Solution: Use model methods to check cache
3. "Accessor not working"
Cause: Cache key not defined in configuration
Solution: Add the key to getCacheConfig()
4. Cache data not showing in toArray/toJson
Cause: This is intentional to prevent exposing internal data Solution: Use accessors for API data:
// In model
protected $appends = ['view_count', 'has_video'];
// Accessors are automatically created by HasCacheField
5. Undefined property when accessing cache key
Cause: Missing trait or incorrect key name Solution:
- Verify that HasCacheField trait is used in the model
- Check that the key exactly matches the definition in getCacheConfig (case-sensitive)
- Check that cache data exists for this key
- Use hasCacheData to check data existence:
if ($post->hasCacheData('view_count')) {
$count = $post->view_count;
} else {
// Cache not yet created
}
Debug Commands
// Check cache configuration
$user = User::find(1);
dd($user->getCacheConfigSummary());
// Check cache statistics
dd($user->getCacheStatistics());
// Debug cache structure
Logger::debug('Cache structure', [
'raw_cache' => $model->getRawAttribute('cache'),
'config_keys' => $model->getCacheConfigKeys(),
'has_trait' => method_exists($model, 'hasCacheData')
]);