jardiscore / cache
Advanced PSR-16 compliant multi-layer caching library
Installs: 38
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/jardiscore/cache
Requires
- php: >=8.2
- ext-apcu: *
- ext-json: *
- ext-pdo: *
- ext-redis: *
- psr/container: ^2.0
- psr/simple-cache: ^3.0
Requires (Dev)
- phpstan/phpstan: ^2.0.4
- phpunit/phpunit: ^10.5
- squizlabs/php_codesniffer: ^3.11.2
README
Jardis Cache is a powerful, PSR-16 compliant multi-layer caching library for PHP 8.2+ that provides blazing-fast performance through intelligent cache layering and automatic write-through propagation.
๐ Why Multi-Layer Caching?
Modern applications need different cache strategies for different data:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Request โ L1 (Memory) โ L2 (APCu/Redis) โ L3 (Redis/DB) โ
โ โ ยตs โ ยตs/ms โ ~5-50ms โ
โ FASTEST VERY FAST FAST/SLOW โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Power of Layering
Single Cache Problem:
// Traditional: One cache fits all? $redis->set('session:123', $data); // Too slow for sessions $memory->set('big-report.pdf', $data); // Too volatile for large data
Multi-Layer Solution:
// Example 1: Single-server with APCu $cache = new Cache( new CacheMemory(), // L1: Request cache (ยตs) new CacheApcu() // L2: Shared worker cache (ยตs) ); // Example 2: Distributed with Redis + Database $cache = new Cache( new CacheRedis($redis), // L1: Fast persistent cache (~5ms) new CacheDatabase($pdo) // L2: Durable storage (~50ms) ); // One API, optimal performance at each layer! $cache->set('session:123', $data); // Writes to all layers $cache->get('session:123'); // Reads from fastest available
Key Benefits
โ Automatic Write-Through: Cache miss in L1? Automatically populated from L2/L3 โ Transparent Failover: Redis down? Falls back to database seamlessly โ PSR-16 Compliant: Standard interface, works with any PSR-16 tool โ Zero Configuration: Works out-of-the-box with sensible defaults โ Battle-Tested: 70 tests, 81% coverage, production-ready
๐ฆ Installation
Via Composer
composer require jardiscore/cache
From GitHub
git clone https://github.com/jardiscore/cache.git
cd cache
make install
๐ฏ Quick Start
Basic Usage: Single Layer
use JardisCore\Cache\Adapter\CacheMemory; $cache = new CacheMemory(); // PSR-16 Simple Cache Interface $cache->set('user:123', $userData, 3600); $user = $cache->get('user:123', $default = null); $cache->delete('user:123'); $cache->clear();
Multi-Layer Cache: The Real Power
use JardisCore\Cache\Cache; use JardisCore\Cache\Adapter\CacheMemory; use JardisCore\Cache\Adapter\CacheRedis; use JardisCore\Cache\Adapter\CacheDatabase; // Create layers (fast โ slow) $cache = new Cache( new CacheMemory(), // L1: In-memory (fastest) new CacheRedis($redis), // L2: Redis (fast, persistent) new CacheDatabase($pdo) // L3: Database (durable) ); // Use like any PSR-16 cache $cache->set('product:456', $product, 3600); // Automatic optimization: // 1. Writes to ALL layers // 2. Reads from fastest available layer // 3. Populates faster layers on cache miss (write-through)
๐ก Available Cache Implementations
CacheMemory
Best for: Session data, request-scoped cache, frequently accessed small data
use JardisCore\Cache\Adapter\CacheMemory; $cache = new CacheMemory('my-namespace');
Characteristics:
- โ Ultra-fast (microseconds)
- โ Zero external dependencies
- โ Not persistent (lost on script end)
- โ Not shared between requests
CacheApcu
Best for: Shared cache between PHP-FPM workers, opcache-style data
use JardisCore\Cache\Adapter\CacheApcu; $cache = new CacheApcu('my-namespace');
Characteristics:
- โ Fast (shared memory)
- โ Shared across PHP-FPM workers
- โ Not persistent (lost on server restart)
- โ Not available in CLI by default
- โ Single-server only (not distributed)
Note: APCu shares data between all PHP-FPM worker processes on the same server.
CacheRedis
Best for: Distributed cache, API responses, cross-server sessions
use JardisCore\Cache\Adapter\CacheRedis; // Create Redis connection $redis = new Redis(); $redis->connect('127.0.0.1', 6379); $cache = new CacheRedis($redis, 'my-namespace');
Characteristics:
- โ Fast (milliseconds)
- โ Persistent across restarts
- โ Distributed (multi-server)
- โ Advanced features (TTL, atomic operations)
- โ External dependency (Redis server)
CacheDatabase
Best for: Large objects, file metadata, durable cache
use JardisCore\Cache\Adapter\CacheDatabase; $pdo = new PDO('sqlite::memory:'); // Create table (see schema below) $cache = new CacheDatabase($pdo, 'my-namespace'); // Custom table/column names $cache = new CacheDatabase( $pdo, namespace: 'app', cacheTable: 'my_cache_table', cacheKeyField: 'key', cacheValueField: 'data', cacheExpiresAt: 'expires' );
Required Table Schema:
-- Works for SQLite, MySQL, MariaDB, PostgreSQL CREATE TABLE cache ( cache_key TEXT PRIMARY KEY, -- PRIMARY KEY automatically creates index for fast key lookups cache_value TEXT NOT NULL, expires_at INTEGER ); -- Index for efficient expiration cleanup queries (cleanExpired method) CREATE INDEX idx_cache_expires_at ON cache(expires_at);
Characteristics:
- โ Durable (survives everything)
- โ Large data support
- โ SQL query capabilities
- โ Slower (disk I/O)
- โ External dependency (database)
Bonus Feature:
// Clean expired entries manually $cache->cleanExpired();
๐จ Multi-Layer Patterns
Pattern 1: Standard L1 โ L2 โ L3
$cache = new Cache( new CacheMemory(), // L1: Request cache new CacheRedis($redis), // L2: Persistent cache new CacheDatabase($pdo) // L3: Long-term storage ); // Get flow: // 1. Check L1 (Memory) โ Miss // 2. Check L2 (Redis) โ Hit! // 3. Write back to L1 (automatic) // 4. Return value
Pattern 2: Memory + APCu (Fast Single-Server)
$cache = new Cache( new CacheMemory(), // L1: Request-scoped cache new CacheApcu() // L2: Shared between PHP-FPM workers ); // Perfect for single-server setups with PHP-FPM // - Sessions, frequently accessed data // - No Redis/external cache needed // - Survives request lifecycle, shared across workers
Pattern 3: Memory + Redis (Fast & Distributed)
$cache = new Cache( new CacheMemory(), // L1: Request cache new CacheRedis($redis) // L2: Distributed cache ); // Perfect for: // - Multi-server environments // - API responses, cross-server sessions // - When you need persistence and distribution
Pattern 4: Redis + Database (Persistent & Durable)
$cache = new Cache( new CacheRedis($redis), // L1: Fast persistent cache new CacheDatabase($pdo) // L2: Durable long-term storage ); // Perfect for: // - Large objects that need persistence // - When Redis memory is limited // - Fallback when Redis is unavailable // - Audit-trail requirements (SQL queries on cache)
Pattern 5: APCu + Redis (Hybrid Local + Distributed)
$cache = new Cache( new CacheApcu(), // L1: Local server cache new CacheRedis($redis) // L2: Distributed cache ); // Perfect for: // - Multi-server with local optimization // - Frequently accessed data (APCu serves locally) // - Cross-server cache synchronization (Redis)
Pattern 6: Single Layer (Testing/Development)
$cache = new Cache( new CacheMemory() // Single layer is fine! );
๐ง Advanced Usage
Namespaces: Isolation & Organization
// Isolate different application parts $userCache = new CacheRedis($redis, 'users:'); $productCache = new CacheRedis($redis, 'products:'); $userCache->set('123', $userData); // Stored as: users:hash(123) $productCache->set('123', $prodData); // Stored as: products:hash(123) // Clear only user cache $userCache->clear(); // Products untouched!
Why Namespaces?
- โ Prevent key collisions
- โ Selective cache clearing
- โ Multi-tenant support
- โ Environment separation (dev/staging/prod)
TTL (Time To Live)
use DateInterval; // TTL in seconds $cache->set('key', 'value', 3600); // 1 hour // TTL with DateInterval $cache->set('key', 'value', new DateInterval('PT1H')); // 1 hour $cache->set('key', 'value', new DateInterval('P1D')); // 1 day // No expiration $cache->set('key', 'value', null); // Never expires
TTL Behavior in Multi-Layer:
$cache = new Cache($l1, $l2, $l3); $cache->set('key', 'value', 3600); // All layers get the SAME TTL // L1, L2, L3 will all expire after 1 hour
Batch Operations
// Get multiple keys at once $results = $cache->getMultiple(['user:1', 'user:2', 'user:3'], 'default'); // Returns: ['user:1' => $data, 'user:2' => 'default', ...] // Set multiple keys at once $cache->setMultiple([ 'user:1' => $userData1, 'user:2' => $userData2, ], 3600); // Delete multiple keys $cache->deleteMultiple(['user:1', 'user:2']);
๐ญ Design Patterns: Extending Functionality
Why Not Built-In Logging?
Jardis Cache stays focused: Cache libraries should only cache.
โ Advantages of keeping it simple:
- No logger dependency (PSR-3)
- No performance overhead
- No testing complexity
- User decides what they need
Decorator Pattern: Adding Logging
Want logging? Wrap it yourself with the Decorator Pattern:
use Psr\SimpleCache\CacheInterface; use Psr\Log\LoggerInterface; class LoggingCache implements CacheInterface { public function __construct( private CacheInterface $cache, private LoggerInterface $logger ) {} public function get(string $key, mixed $default = null): mixed { $this->logger->debug('Cache get', ['key' => $key]); $result = $this->cache->get($key, $default); if ($result === $default) { $this->logger->info('Cache miss', ['key' => $key]); } else { $this->logger->info('Cache hit', ['key' => $key]); } return $result; } public function set(string $key, mixed $value, null|int|\DateInterval $ttl = null): bool { $this->logger->debug('Cache set', ['key' => $key, 'ttl' => $ttl]); return $this->cache->set($key, $value, $ttl); } public function delete(string $key): bool { $this->logger->info('Cache delete', ['key' => $key]); return $this->cache->delete($key); } public function clear(): bool { $this->logger->warning('Cache clear called'); return $this->cache->clear(); } public function getMultiple(iterable $keys, mixed $default = null): iterable { return $this->cache->getMultiple($keys, $default); } public function setMultiple(iterable $values, null|int|\DateInterval $ttl = null): bool { return $this->cache->setMultiple($values, $ttl); } public function deleteMultiple(iterable $keys): bool { return $this->cache->deleteMultiple($keys); } public function has(string $key): bool { return $this->cache->has($key); } }
Usage:
// Without logging $cache = new Cache( new CacheMemory(), new CacheRedis($redis) ); // With logging (wrap it!) $cache = new LoggingCache( new Cache( new CacheMemory(), new CacheRedis($redis) ), $logger // PSR-3 Logger ); // Same API, now with logging! $cache->get('key'); // Logs: "Cache get: key"
Why Decorator Pattern?
- โ Separation of Concerns: Cache caches, Logger logs
- โ Composable: Stack multiple decorators
- โ Flexible: Only add logging where needed
- โ PSR-16 Compatible: Same interface
Metrics Decorator: Performance Monitoring
class MetricsCache implements CacheInterface { public function __construct( private CacheInterface $cache, private MetricsCollector $metrics ) {} public function get(string $key, mixed $default = null): mixed { $start = microtime(true); $result = $this->cache->get($key, $default); $duration = microtime(true) - $start; $this->metrics->timing('cache.get', $duration); if ($result === $default) { $this->metrics->increment('cache.miss'); } else { $this->metrics->increment('cache.hit'); } return $result; } // ... implement other methods with PSR-16 v3 type hints }
Stack Multiple Decorators:
$cache = new LoggingCache( new MetricsCache( new Cache($l1, $l2, $l3), $metrics ), $logger ); // Now you have: Logging + Metrics + Multi-Layer Cache!
Facade Pattern: Simplified API
Create a simpler, domain-specific API:
class CacheFacade { public function __construct( private CacheInterface $cache, private LoggerInterface $logger ) {} /** * Remember pattern: Get from cache or compute */ public function remember(string $key, callable $callback, int $ttl = 3600): mixed { if ($value = $this->cache->get($key)) { return $value; } $this->logger->info('Cache miss, computing value', ['key' => $key]); $value = $callback(); $this->cache->set($key, $value, $ttl); return $value; } /** * Remember forever (no TTL) */ public function rememberForever(string $key, callable $callback): mixed { return $this->remember($key, $callback, null); } /** * Warm up cache with multiple keys */ public function warmUp(array $keys, callable $loader): void { $values = []; foreach ($keys as $key) { if (!$this->cache->has($key)) { $values[$key] = $loader($key); } } if (!empty($values)) { $this->cache->setMultiple($values); $this->logger->info('Cache warmed up', ['count' => count($values)]); } } /** * Invalidate by pattern (requires namespace support) */ public function invalidatePattern(string $pattern): void { // Implementation depends on your cache backend $this->logger->warning('Invalidating pattern', ['pattern' => $pattern]); } }
Usage:
$facade = new CacheFacade($cache, $logger); // Elegant remember pattern $user = $facade->remember('user:123', function() use ($db) { return $db->findUser(123); }, 3600); // Warm up cache $facade->warmUp(['user:1', 'user:2', 'user:3'], fn($key) => $db->find($key));
๐งช Testing
Run Tests
# All tests make phpunit # With coverage make phpunit-coverage # Coverage HTML report make phpunit-coverage-html
Docker Setup
The project includes a complete Docker setup with Redis for testing:
# Start Redis for testing docker compose up -d redis # Run tests in container make phpunit # Stop everything docker compose down
Configuration:
- Redis runs on port
6389(isolated from system Redis) - No persistence (optimized for testing)
- SQLite in-memory for database tests
๐ Requirements
- PHP: 8.2 or higher
- Extensions:
ext-json(required)ext-pdo(for CacheDatabase)ext-redis(for CacheRedis)ext-apcu(for CacheApcu)
๐๏ธ Architecture
Class Hierarchy
CacheInterface (PSR-16)
โโโ Cache (Multi-Layer)
โโโ AbstractCache
โโโ CacheMemory
โโโ CacheApcu
โโโ CacheRedis
โโโ CacheDatabase
Design Principles
โ PSR-16 Compliant: Standard Simple Cache interface โ SOLID Principles: Single Responsibility, Open/Closed, Dependency Inversion โ Composition Over Inheritance: AbstractCache base class, no traits โ Chain of Responsibility: Multi-layer cache pattern โ Write-Through Cache: Automatic propagation to faster layers
# Quality checks make phpstan # Static analysis make phpcs # Coding standards make phpunit # Tests
๐ License
This project is licensed under the MIT License.
๐ฏ Jardis Framework
This tool is part of Jardis (Just a reliable domain integration system) - a collection of professional PHP packages for Domain-Driven Design applications.
Quality Standards:
- โ PHPStan Level 8
- โ PSR-12 Coding Standards
- โ 80%+ Test Coverage
- โ PHP 8.2+ Features
Quality Attributes:
- Analyzability, Adaptability, Expandability
- Modularity, Maintainability, Testability
- Scalability, High Performance
Enjoy caching! ๐