Cacheable facade methods
Cache the result of a facade method for a given TTL using the #[Cacheable] attribute.
AbstractFacade includes CacheableTrait, so Facades can use #[Cacheable] and $this->cached() directly.
Quick start
use Gacela\Framework\Attribute\Cacheable;
use Gacela\Framework\AbstractFacade;
final class CatalogFacade extends AbstractFacade
{
#[Cacheable(ttl: 3600)]
public function getPopularProducts(): array
{
return $this->cached(fn (): array =>
$this->getFactory()->createRepository()->fetchPopular(),
);
}
}Subsequent calls within the TTL return the cached value without invoking the callback.
How it works
#[Cacheable] is metadata only. The real caching happens inside $this->cached(...), which:
- Reads the attribute via reflection (memoised per
Class::method). - Builds a cache key from the class, method, and arguments.
- Returns the cached value on hit, or runs the callback and stores the result on miss.
By default, the method name and arguments are inferred from the caller's stack frame. Pass them explicitly for performance-sensitive paths or calls routed through a helper; see Opting out of backtrace.
Generic return type
cached() is generic (@template T), so static analysis infers the return type from the callback without a call-site annotation or cast.
Arguments shape the cache key
Calls with different arguments are cached separately.
#[Cacheable(ttl: 600)]
public function findUser(int $id): User
{
return $this->cached(fn (): User =>
$this->getFactory()->createRepository()->find($id),
);
}
$facade->findUser(1); // runs callback, caches under key ending in "::1"
$facade->findUser(1); // cache hit
$facade->findUser(2); // runs callback, separate entrySingle int or string arguments become part of the key directly (Facade::method::42). Other types (arrays, objects, multiple args) fall back to md5(serialize(...)).
Custom key templates
Use key with {N} placeholders to interpolate the Nth argument into the cache key. Useful for shared keys across modules or for readable keys in an external cache.
#[Cacheable(ttl: 3600, key: 'user:{0}')]
public function getUser(int $id): array
{
return $this->cached(fn (): array =>
$this->getFactory()->createRepository()->find($id),
);
}A bare string with no placeholders is args-agnostic. Every call shares the same entry regardless of arguments.
Clearing the cache
// Clear all entries for a specific method (any args)
CatalogFacade::clearMethodCacheFor('getPopularProducts');
// Clear the whole shared storage backend, across every facade
CatalogFacade::clearMethodCache();clearMethodCacheFor() matches on the exact Class::method:: prefix. Passing 'get' does not clear every method whose name starts with get.
clearMethodCache() calls clear() on the shared backend and is not scoped to the facade class. Prefer the method-specific operation unless clearing all application entries is intentional.
Custom key templates do not contain the normal Class::method:: prefix, so clearMethodCacheFor() cannot find them. Invalidate those keys through the configured storage backend.
Gacela::resetCache() clears only the default in-process method storage. It does not clear an external backend registered through CacheableConfig::setStorage(); call clearMethodCache() when that is the intended scope.
Pluggable storage backend
By default, cache lives in process memory via InMemoryCacheStorage. On PHP-FPM that means entries die with the request. Fine for batch jobs and long-running workers, but effectively a no-op for typical web traffic.
Swap in any backend that implements CacheStorageInterface (e.g. APCu, Redis, a PSR-16 adapter):
use Gacela\Framework\Attribute\CacheableConfig;
CacheableConfig::setStorage(new RedisCacheStorage($redis));interface CacheStorageInterface
{
public function has(string $key): bool;
public function get(string $key, mixed $default = null): mixed;
public function set(string $key, mixed $value, int $ttl): void;
public function delete(string $key): void;
public function clear(): void;
public function deleteByPrefix(string $prefix): void;
}Call CacheableConfig::setStorage() once at bootstrap. All facades using CacheableTrait share the same backend.
TTL overrides per method
Override the TTL declared on the attribute without changing code. Useful for tuning hot paths per environment.
CacheableConfig::setTtlOverrides([
CatalogFacade::class . '::getPopularProducts' => 60, // tighten in staging
UserFacade::class . '::getUser' => 86400, // loosen in prod
]);The override applies on the next set(); existing entries keep their original expiry until evicted.
Opting out of backtrace
cached() calls debug_backtrace() (limit 2) to infer the method name and arguments. The cost is negligible next to typical "expensive" methods (DB, HTTP). Pass $method and $args explicitly when:
- The cached operation itself is very fast and the overhead matters.
- The method takes very large arguments (frame-construction cost scales with arg count).
cached()is called from a private helper rather than the attributed method itself.
#[Cacheable(ttl: 3600)]
public function getUser(int $id): array
{
return $this->cached(
fn (): array => $this->getFactory()->createRepository()->find($id),
__METHOD__,
[$id],
);
}Caching null
A method that returns null is cached correctly. Repeated calls do not re-invoke the callback. CacheableTrait distinguishes "cached null" from "cache miss" via a sentinel, so Optional-style return types work as expected.
Limitations
- Per-process by default. Entries in
InMemoryCacheStoragedo not survive the request on PHP-FPM. Use a shared backend (APCu, Redis) if you need cross-request caching. - Serialization. The default key and miss detection rely on
serialize()for non-scalar arguments. Arguments containing closures or resources cannot be serialized and will throw. - Memoised attribute metadata. The
#[Cacheable]attribute is reflected once perClass::methodand cached for the lifetime of the process. Changing the attribute at runtime has no effect; change the code and redeploy.