On this page
Cacheable facade methods
Cache a facade method's result for a given TTL with #[Cacheable] and $this->cached().
AbstractFacade includes CacheableTrait, so every Facade 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(),
);
}
}Later calls within the TTL return the cached value without running the callback.
How it works
#[Cacheable] is only metadata. The caching happens inside $this->cached(...), which:
- Reads the attribute through 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, cached() infers the method name and arguments from the caller's stack frame. Pass them explicitly on
performance-sensitive paths or for calls routed through a helper. See Opting out of backtrace.
Arguments shape the cache key
Each set of arguments gets its own cache entry.
#[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 entryA single int or string argument goes into the key directly (Facade::method::42). Anything else (arrays, objects,
several arguments) falls back to md5(serialize(...)).
Custom key templates
Use key with {N} placeholders to put the Nth argument into the cache key. This gives readable keys in an external
cache.
A template names an entry inside the declaring class and method, never across them. The stored key is Class::method::
followed by the interpolated template, so two classes using the same template keep separate entries. Since 2.4
#[Cacheable(ttl: 3600, key: 'user:{0}')]
public function getUser(int $id): array
{
return $this->cached(fn (): array =>
$this->getFactory()->createRepository()->find($id),
);
}A plain string with no placeholders ignores the arguments: every call shares one entry.
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 the exact Class::method:: prefix. Passing 'get' does not clear every method
whose name starts with get. It reaches an entry written under a custom key: template like any other, because those
keys carry the same prefix. Since 2.4
clearMethodCache() calls clear() on the shared backend; it is not scoped to the facade class. Prefer the
method-specific call unless you mean to clear every application entry.
Gacela::resetCache() clears only the default in-process method storage. It does not clear an external backend
registered through CacheableConfig::setStorage(); call clearMethodCache() for that.
Pluggable storage backend
By default, the cache lives in process memory through InMemoryCacheStorage. On PHP-FPM, entries die with the
request. That is fine for batch jobs and long-running workers, but close to a no-op for typical web traffic.
Swap in any backend that implements CacheStorageInterface, such as APCu, Redis or 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. Every facade using CacheableTrait shares that backend.
The TTL contract a backend must implement
$ttl |
Meaning |
|---|---|
> 0 |
The entry expires that many seconds from now |
0 |
The entry is stored without expiry, not "expire immediately" |
< 0 |
The entry is already expired when written, so no read returns it |
Zero is the case to read twice. FileCache has always treated it as "no expiry", and its own default TTL is 0. A
backend that computes time() + $ttl unconditionally stores an entry that is expired before set() returns.
Both built-in backends follow the table above; InMemoryCacheStorage was corrected to match in 2.1.
TTL overrides per method
Override the TTL declared on the attribute without changing code. This helps tune 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. Next to a typical expensive
method (DB, HTTP), the cost is negligible. Pass $method and $args explicitly when:
- The cached operation is very fast and the overhead matters.
- The method takes very large arguments (frame-construction cost scales with argument count).
cached()is called from a private helper, not from the attributed method.
#[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-run the callback. CacheableTrait tells
"cached null" from "cache miss" with a sentinel, so Optional-style return types work as expected.
Limitations
- Per-process by default. Entries in
InMemoryCacheStoragedo not outlive the request on PHP-FPM. Use a shared backend (APCu, Redis) for cross-request caching. - Serialization. The default key and miss detection rely on
serialize()for non-scalar arguments. Arguments holding closures or resources cannot be serialized and throw. - Memoised attribute metadata. Gacela reflects the
#[Cacheable]attribute once perClass::methodand keeps it for the life of the process. Changing the attribute at runtime has no effect; change the code and redeploy.