Cookbook

Caching Recipes

Advanced caching — distributed coordination, advanced tagging, the edge/CDN seam, and production tuning.

This page covers the advanced caching surface: distributed coordination, advanced tagging, the edge/CDN seam, and production tuning.

Distributed Caching

Basic Distributed Setup

use Glueful\Cache\DistributedCacheService;

// Configuration
$config = [
    'nodes' => [
        ['id' => 'cache-01', 'driver' => 'redis', 'host' => '192.168.1.10', 'port' => 6379, 'weight' => 100],
        ['id' => 'cache-02', 'driver' => 'redis', 'host' => '192.168.1.11', 'port' => 6379, 'weight' => 100],
        ['id' => 'cache-03', 'driver' => 'redis', 'host' => '192.168.1.12', 'port' => 6379, 'weight' => 50],
    ],
    'replication' => 'consistent-hashing',
    'failover' => true,
    'health' => [
        'health_check' => [
            'interval' => 30,
            'timeout' => 5,
            'failure_threshold' => 3,
        ],
    ],
];

// Create distributed cache ($primaryCache is a CacheStore, e.g. app($context, CacheStore::class))
$distributedCache = new DistributedCacheService($primaryCache, $config, $context);

// Use like normal cache - operations are distributed automatically
$distributedCache->set('user:123', $userData, 3600);
$userData = $distributedCache->get('user:123');

Replication Strategies

// Built-in strategies: consistent-hashing (default), full-replication,
// primary-replica, key-pattern-sharding
$distributedCache->setReplicationStrategy('consistent-hashing');
$distributedCache->setReplicationStrategy('primary-replica');

// Per-strategy configuration (passed to the constructor)
$config['strategies'] = [
    'consistent-hashing' => ['virtualNodes' => 150, 'replicas' => 2],
    'primary-replica' => ['maxReplicas' => 2],
    'key-pattern-sharding' => [
        'patterns' => [/* pattern => selector */],
        'fallback' => 'consistent-hashing',
    ],
];

Health Monitoring and Failover

// Enable automatic failover
$distributedCache->setFailoverEnabled(true);

// Check failover status
if ($distributedCache->isFailoverEnabled()) {
    // Failover is active - unhealthy nodes are automatically excluded
}

// Get node manager for advanced operations
$nodeManager = $distributedCache->getNodeManager();
$healthyNodes = $nodeManager->getHealthyNodesForKey('user:123');
$allNodes = $nodeManager->getAllNodes();

Construct the service yourself: the container's DistributedCacheService is built with an empty config. config/cache.php has no distributed block (1.91 removed it, along with enable_tags and tags_store, because nothing read them), so keep the node list in your own config and pass it in.

Note: Enable or disable failover with $distributedCache->setFailoverEnabled(true|false). Replication strategies and health monitoring options are provided via the $config passed to the constructor.

Cache Tagging System

Basic Tagging

use Glueful\Cache\CacheTaggingService;

// Enable tagging
CacheTaggingService::enable();

// Tag cache entries
CacheTaggingService::tagCache('user:123', ['users', 'user_data']);
CacheTaggingService::tagCache('user:123:permissions', ['users', 'permissions']);
CacheTaggingService::tagCache('role:admin', ['roles', 'permissions']);

// Get tags for a key
$tags = CacheTaggingService::getKeyTags('user:123');

// Get keys for a tag
$userKeys = CacheTaggingService::getTaggedKeys('users');

Tag-based Invalidation

// Invalidate by single tag
$result = CacheTaggingService::invalidateByTag('users');
/*
[
    'status' => 'completed',
    'tag' => 'users',
    'invalidated' => ['user:123', 'user:456'],
    'failed' => [],
    'success_count' => 2,
    'failure_count' => 0
]
*/

// Invalidate by multiple tags
$result = CacheTaggingService::invalidateByTags(['users', 'permissions']);

// Invalidate by predefined category
$result = CacheTaggingService::invalidateRelated('config');

Predefined Tag Categories

// Built-in categories
$categories = CacheTaggingService::getPredefinedTags();
/*
[
    'config' => ['app_config', 'database_config', 'cache_config'],
    'permissions' => ['user_permissions', 'role_permissions', 'permission_definitions'],
    'roles' => ['user_roles', 'role_definitions', 'role_hierarchy'],
    'users' => ['user_data', 'user_sessions', 'user_profiles'],
    'auth' => ['jwt_tokens', 'session_data', 'auth_cache'],
    'api' => ['api_routes', 'api_definitions', 'api_metadata'],
    'files' => ['file_metadata', 'upload_cache', 'image_cache'],
    'notifications' => ['notification_templates', 'notification_preferences', 'notification_queue']
]
*/

// Add custom category
CacheTaggingService::addPredefinedTag('products', ['product_data', 'product_images', 'product_cache']);

// Invalidate entire category
CacheTaggingService::invalidateRelated('users');

Tag Management

// Get tagging statistics
$stats = CacheTaggingService::getTagStats();
/*
[
    'total_tags' => 12,
    'total_tagged_keys' => 45,
    'tags' => [
        'users' => ['key_count' => 15, 'keys' => ['user:123', 'user:456', ...]],
        'permissions' => ['key_count' => 8, 'keys' => ['perm:admin', ...]]
    ]
]
*/

// Cleanup orphaned tags and keys
$cleanup = CacheTaggingService::cleanup();
/*
[
    'status' => 'completed',
    'cleaned_keys' => ['user:789'], // Keys that no longer exist in cache
    'cleaned_tags' => ['temp_tag'], // Tags with no associated keys
    'key_count' => 1,
    'tag_count' => 1
]
*/

Cache Helper Utilities

Safe Cache Operations

use Glueful\Helpers\CacheHelper;

// Create cache instance with graceful fallback
$cache = CacheHelper::createCacheInstance($context); // Returns null if cache unavailable

// Check cache health
if (CacheHelper::isCacheHealthy($cache)) {
    // Cache is available and responding
}

// Safe cache operations with fallback
$result = CacheHelper::safeExecute(
    $cache,
    fn($cache) => $cache->get('user:123'),
    $defaultUserData // Fallback if cache operation fails
);

Key Management

// Automatic key prefixing (cache.prefix); every builder below adds it too
$key = CacheHelper::key('user:123'); // glueful:user:123
$keys = CacheHelper::keys(['user:123', 'user:456']); // Prefix multiple keys

// Specialized key generators
$userKey = CacheHelper::userKey('123', 'profile'); // user:123:profile
$sessionKey = CacheHelper::sessionKey('abc123', 'data'); // session:data:abc123
$rateLimitKey = CacheHelper::rateLimitKey('192.168.1.1', 'api'); // rate_limit:api:<sha256 of the IP>
$permissionKey = CacheHelper::permissionKey('user123', 'admin'); // permissions:user123:admin
$configKey = CacheHelper::configKey('database'); // config:database

// Remove prefix from keys
$baseKey = CacheHelper::unprefix('prefix:user:123'); // user:123

Cache Stampede Protection

CacheHelper::remember(), rememberWithStampedeProtection(), rememberWithEarlyExpiration() and the cache.stampede_protection config are covered in Features → Caching → Stampede Protection.

Edge Caching and CDN Integration

Requires the glueful/cdn extension

Core ships only the seam: response caching emits surrogate keys against the Glueful\Cache\Contracts\EdgeCacheInterface, bound by default to the no-op NullEdgeCache. To actually emit edge headers and purge a CDN, install glueful/cdn — it binds Glueful\Extensions\Cdn\EdgeCachePurger to the same interface and adds the cache:purge command. The purge API (purgeUrl(), purgeByTag(), purgeAll()) and cache.edge / EDGE_CACHE_* configuration are documented with that extension. Resolve the binding via the container rather than constructing it directly:

$edge = app($context, \Glueful\Cache\Contracts\EdgeCacheInterface::class);
// no-op without glueful/cdn; real CDN purges once the extension is installed

Production Optimization

High-Performance Configuration

// config/cache.php - Production optimizations
// The Redis factory (CacheFactory::createRedisConnection(), shared with the Redis
// lock store) reads only host, port, password, database and timeout;
// serializer/compression/persistent/pool options are not supported.
return [
    'stores' => [
        'redis' => [
            'driver' => 'redis',
            'host' => env('REDIS_HOST'),
            'port' => env('REDIS_PORT'),
            'password' => env('REDIS_PASSWORD'),
            'database' => 0,
            'timeout' => 1.0, // Faster timeout (the factory caps the connect timeout at 1s)
        ]
    ],
    
    'stampede_protection' => [
        'enabled' => true,
        'lock_ttl' => 30,        // Shorter locks in production
        'max_wait_time' => 10,   // Less waiting time
        'early_expiration' => [
            'enabled' => true,
            'threshold' => 0.85  // Earlier refresh
        ]
    ]
];

Monitoring and Alerting

class CacheMonitoringService
{
    private CacheStore $cache;
    
    public function getHealthMetrics(): array
    {
        $stats = $this->cache->getStats();
        
        // Shape is driver-specific; for Redis: memory.used, performance.total_commands, ...
        return [
            'hit_ratio' => $this->calculateHitRatio($stats), // your own calculation
            'memory_used' => $stats['memory']['used'] ?? 'unknown',
            'total_commands' => $stats['performance']['total_commands'] ?? 0,
            'avg_response_time' => $this->measureResponseTime()
        ];
    }
    
    public function checkAlerts(): array
    {
        $metrics = $this->getHealthMetrics();
        $alerts = [];
        
        if ($metrics['hit_ratio'] < 0.85) {
            $alerts[] = 'Cache hit ratio below 85%';
        }
        
        if ($metrics['avg_response_time'] > 10) {
            $alerts[] = 'Cache response time above 10ms';
        }
        
        return $alerts;
    }
    
    private function measureResponseTime(): float
    {
        $start = microtime(true);
        $this->cache->get('health_check_' . uniqid());
        return (microtime(true) - $start) * 1000; // ms
    }
}

Hit Ratio from Cache Events

Instead of computing a hit ratio from driver stats, count the CacheHitEvent / CacheMissEvent the container store fires from get() and remember(). The events are only built when a listener exists, and a listener failure never breaks the read:

use Glueful\Cache\CacheStore;
use Glueful\Events\Attributes\AsListener;
use Glueful\Events\Cache\{CacheHitEvent, CacheMissEvent};

final class CacheHitRatioRecorder
{
    public function __construct(private readonly CacheStore $cache) {}

    #[AsListener(CacheHitEvent::class)]
    public function onHit(CacheHitEvent $event): void
    {
        $this->cache->increment('metrics:cache:hits');
    }

    #[AsListener(CacheMissEvent::class)]
    public function onMiss(CacheMissEvent $event): void
    {
        $this->cache->increment('metrics:cache:misses');
    }
}

// boot(): subscribe() autowires the class; registering it in services() is optional
app($context, EventService::class)->subscribe(CacheHitRatioRecorder::class);

increment() doesn't fire events, so recording a metric never counts as a hit or miss itself. Only the container's cache.store fires these events: it is an EventDispatchingCacheStore wrapping the configured driver, so an instanceof RedisCacheDriver-style check against it fails (call getInnerStore() for the driver; driver-only methods are forwarded). Stores you build with CacheFactory::create() are bare drivers. See Events → Attribute Listeners.

Cache Partitioning Strategy

class CachePartitioningService
{
    private array $caches = [];
    
    public function __construct(ApplicationContext $context)
    {
        // Partition cache by data type; the first argument forces a driver.
        // These are bare drivers: they don't fire CacheHit/CacheMiss events.
        $this->caches = [
            'users' => CacheFactory::create('redis', $context),     // User data on Redis
            'sessions' => CacheFactory::create('redis', $context),  // Session data on Redis
            'static' => CacheFactory::create('file', $context),     // Static content on file
            'temp' => CacheFactory::create('memcached', $context)   // Temporary data on Memcached
        ];
    }
    
    public function getCache(string $partition): CacheStore
    {
        return $this->caches[$partition] ?? $this->caches['users'];
    }
    
    public function set(string $partition, string $key, mixed $value, int $ttl = 3600): bool
    {
        return $this->getCache($partition)->set($key, $value, $ttl);
    }
    
    public function get(string $partition, string $key, mixed $default = null): mixed
    {
        return $this->getCache($partition)->get($key, $default);
    }
}