# 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

```php
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:

1. Reads the attribute via reflection (memoised per `Class::method`).
2. Builds a cache key from the class, method, and arguments.
3. 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](#opting-out-of-backtrace).

::: tip 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.

```php
#[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 entry
```

Single `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.

```php
#[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

```php
// 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):

```php
use Gacela\Framework\Attribute\CacheableConfig;

CacheableConfig::setStorage(new RedisCacheStorage($redis));
```

```php
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.

```php
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.

```php
#[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 `InMemoryCacheStorage` do 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 per `Class::method` and cached for the lifetime of the process. Changing the attribute at runtime has no effect; change the code and redeploy.
