Cacheable facade methods
Cache the result of a facade method for a given TTL using the #[Cacheable] attribute.
Since 1.14
CacheableTrait is now built into AbstractFacade. You no longer need use CacheableTrait; — any facade extending AbstractFacade can use #[Cacheable] and $this->cached() out of the box.
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.
The method name and arguments are inferred from the caller's stack frame via debug_backtrace() automatically. You can pass them explicitly for performance or when calling from a helper (see Opting out of backtrace).
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 everything for this facade class
CatalogFacade::clearMethodCache();
// Clear all entries for a specific method (any args)
CatalogFacade::clearMethodCacheFor('getPopularProducts');clearMethodCacheFor() matches on the exact Class::method:: prefix. Passing 'get' does not clear every method whose name starts with get.
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. Cost is 1–5 µs — unmeasurable for 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.