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

1.0.0 2025-12-19 11:38 UTC

This package is auto-updated.

Last update: 2025-12-19 11:41:55 UTC


README

Build Status License: MIT PHP Version PHPStan Level PSR-4 PSR-12 PSR-16 Coverage

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! ๐Ÿš€