<!--
	covers: helpers/CacheHelper.php config/components/cache.php config/components/db.php base/Widget.php
	verified: 54fd0a6
-->
# Caching

Four layers, all sharing one cache component, all flushed together.

## The component

[`config/components/cache.php`](../../config/components/cache.php) picks a backend at config time:

```
CACHE_REDIS_ENABLED     → yii\redis\Cache      (default; unix socket + password, or host/port/database)
CACHE_MEMCACHED_ENABLED → yii\caching\MemCache
otherwise               → yii\caching\FileCache
```

Every backend uses `keyPrefix => APP_ID`, so two applications sharing a Redis instance do not collide. Only
`FileCache` gets `defaultDuration => CACHE_DEFAULT_DURATION` — **with Redis there is no default TTL**, so an
entry written without an explicit duration lives until it is flushed.

## Layer 1 — database schema cache

`config/components/db.php`: `enableSchemaCache`, `schemaCacheDuration` (24 h), stored in the same `cache`
component.

This is why the deploy flushes the cache **before** running migrations: a stale schema cache makes migrations
see the old table definitions.

## Layer 2 — query cache

`queryCacheDuration => DB_QUERY_CACHE_DURATION` (900 s). Opt-in per query with `->cache()`:

```php
$query = Article::find()->where( [ 'isPublished' => true ] )->cache();
```

Used throughout `services/` and in the read paths of `controllers/` and the content models. Eager-loaded
relations need their own `->cache()` inside the `with()` closure — it does not inherit:

```php
->with( [
    'photos' => fn( ActiveQuery $q ) => $q->orderBy( … )->cache(),
    'photos.picture' => fn( ActiveQuery $q ) => $q->cache()
] )
```

## Layer 3 — per-request memoization

`static $x; $x ??= …` inside service methods. Not a cache component at all — process-local, free, and gone at
the end of the request. See [SERVICES.md](SERVICES.md).

## Layer 4 — widget output cache

[`base/Widget.php:78`](../../base/Widget.php):

```php
$cacheIdParams = CACHE_DISABLE_WIDGET_CACHE ? null : $this->cacheIdParams;
if( $cacheIdParams !== null ) {
    $cacheIdParams[ 'tagName' ]        = $this->tagName;
    $cacheIdParams[ 'unwrapped' ]      = $this->unwrapped;
    $cacheIdParams[ 'htmlAttributes' ] = $this->htmlAttributes;
    $cacheKey = sha1( serialize( $cacheIdParams ) );
    return (string)Yii::$app->cache->getOrSet( $cacheKey, fn() => $this->output );
}
```

**`getCacheIdParams()` returns `null` in the base class and no widget overrides it.** The machinery is fully
built and currently inert — every widget renders on every request. If you switch it on for a widget, remember:

- the key is a `sha1` of the serialized params **plus** tag name, unwrapped flag and HTML attributes — the
  `id` attribute is in there, so two instances with different auto-generated ids get different keys;
- `getOrSet()` is called **without a duration**, so on Redis the entry never expires on its own;
- `CACHE_DISABLE_WIDGET_CACHE` defaults to on in `dev`, so you will not see the effect locally.

## Explicit `getOrSet()` calls

Only one in the frontend: the sitemap
([`controllers/SiteController.php:124`](../../controllers/SiteController.php)), keyed `[ 'sitemap', $lang ]`,
24 h.

## Invalidation

There is no tag-based or key-based invalidation anywhere. Everything is **flush-all**:

| Trigger | Path |
|---------|------|
| Admin save / delete / sort | the admin actions' `flushCache` flag → `CacheHelper::flush()` |
| Dashboard "flush cache" button | `admin/dashboard/delete-cache`, gated by the `DELETE_CACHE` right |
| Deploy | `./yii deploy/flush-cache`, run twice — see [DEPLOYMENT.md](../DEPLOYMENT.md) |

[`CacheHelper::flush()`](../../helpers/CacheHelper.php) is one line: `Yii::$app->cache->flush()`. It clears the
schema cache, the query cache and the sitemap in one go.

## Consequences

1. **Anything written to the database outside the admin needs a manual flush** — raw SQL, an import, a
   translation run.
2. **A flush is genuinely global.** After it, the next request re-reads the schema, so the first hit after a
   deploy is measurably slower.
3. **Fragment caching uses the default `cache` component.** `View` used to override `beginCache()` to force a
   `fileCache` component that was never configured; the override was removed on 2026-08-08. Nothing calls
   `beginCache()` today, so switching it on for the first time is untested ground.
