Caching

Canvas includes a caching system with three built-in drivers — file, Redis, and Memcached. Caching can be applied declaratively to controller methods via @InterceptWith(CacheAspect::class), or used directly through the CacheInterface in any service.

Configuration

Cache configuration lives in config/cache.php. The default key sets the driver used when no driver is specified explicitly. If the file does not exist, Canvas falls back to the file driver with its built-in defaults.

// config/cache.php
return [
    'default' => 'file',

    'drivers' => [
        'file' => [
            'class' => Quellabs\Canvas\Cache\Drivers\FileCache::class,
        ],
        'redis' => [
            'class'    => Quellabs\Canvas\Cache\Drivers\RedisCache::class,
            'host'     => '127.0.0.1',
            'port'     => 6379,
            'timeout'  => 2.5,
            'database' => 0,
            'password' => null,
        ],
        'memcached' => [
            'class'   => Quellabs\Canvas\Cache\Drivers\MemcachedCache::class,
            'servers' => [
                ['127.0.0.1', 11211, 100], // [host, port, weight]
            ],
        ],
    ],
];

Caching with CacheAspect

CacheAspect is an around aspect that caches the return value of a controller method. The cache key is generated automatically from the class name, method name, and arguments — the same method called with different arguments gets different cache entries.

/**
 * @Route("/products/{id}")
 * @InterceptWith(CacheAspect::class, ttl=3600)
 */
public function show(int $id): Response {
    $product = $this->em()->find(ProductEntity::class, $id);
    return $this->render('products/show.tpl', ['product' => $product]);
}

CacheAspect Parameters

Parameter Type Default Description
ttlint3600Time to live in seconds. 0 = never expires.
driverstringnullCache driver to use (file, redis, memcached). Defaults to the configured default.
namespacestringdefaultGroups cache entries. Useful for targeted invalidation.
keystringnullCustom cache key. Auto-generated from method + arguments when omitted.
gracefulFallbackbooltrueWhen true, executes the method normally if caching fails instead of throwing.
betafloat0.0Stampede protection factor (0.0 = disabled, 0.5–1.0 recommended). See Cache Stampede Protection below.

Specifying a Driver

Override the default driver per method using the driver parameter:

/**
 * @Route("/products")
 * @InterceptWith(CacheAspect::class, ttl=300, driver="redis")
 */
public function index(): Response {
    // Cached in Redis regardless of the configured default
}

Namespacing

Use namespace to group related cache entries. This makes targeted invalidation straightforward — flush the namespace rather than individual keys:

/**
 * @Route("/products/{id}")
 * @InterceptWith(CacheAspect::class, ttl=3600, namespace="products")
 */
public function show(int $id): Response {
    // Cached under the "products" namespace
}

Cache Stampede Protection

When many requests hit an expired cache entry simultaneously, they all recompute the value at once — this is a cache stampede. Set beta to a value between 0.5 and 1.0 to enable probabilistic early expiration using the XFetch algorithm. Canvas will occasionally refresh the cache before it expires, preventing the stampede without coordination between requests:

/**
 * @Route("/reports/summary")
 * @InterceptWith(CacheAspect::class, ttl=3600, beta=1.0)
 */
public function summary(): Response {
    // Expensive computation — refreshed early under load
}

Stampede protection requires the cache driver to support metadata retrieval via getMetadata(). If the driver does not support it, Canvas falls back to normal cache behaviour silently.

Using CacheInterface Directly

Inject CacheInterface into any service or controller method to work with the cache directly:

use Quellabs\Contracts\Cache\CacheInterface;

class ProductService {

    public function __construct(private CacheInterface $cache) {}

    public function getProduct(int $id): ?ProductEntity {
        return $this->cache->remember("product.{$id}", 3600, function () use ($id) {
            return $this->em->find(ProductEntity::class, $id);
        });
    }
}

Use @WithContext on a route method to inject a specific driver:

/**
 * @Route("/products/{id}")
 * @WithContext(parameter="cache", context="redis")
 */
public function show(int $id, CacheInterface $cache): Response {
    $product = $cache->remember("product.{$id}", 3600, fn() =>
        $this->em()->find(ProductEntity::class, $id)
    );

    return $this->render('products/show.tpl', ['product' => $product]);
}

Cache Drivers

File (default)

Stores cache entries as files in storage/cache/. No additional dependencies required. Suitable for single-server deployments and development.

Redis

Requires the PHP redis extension. Suitable for high-traffic or multi-server deployments. Configured under the redis key in config/cache.php:

'redis' => [
    'class'        => Quellabs\Canvas\Cache\Drivers\RedisCache::class,
    'host'         => '127.0.0.1',
    'port'         => 6379,
    'timeout'      => 2.5,
    'read_timeout' => 2.5,
    'database'     => 0,
    'password'     => null,
],

Memcached

Requires the PHP memcached extension. Supports multiple servers with weighted distribution. Configured under the memcached key in config/cache.php:

'memcached' => [
    'class'   => Quellabs\Canvas\Cache\Drivers\MemcachedCache::class,
    'servers' => [
        ['127.0.0.1', 11211, 100], // [host, port, weight]
        ['192.168.1.10', 11211, 50],
    ],
],