# L2.4 — Cache de consultas / eager loading (N+1) de origem

**Objetivo (A1):** reduzir o tempo de DB por requisição de portal, refletindo em
`origem`/`ttfb`. Complementa o FPC (L2.3): o FPC mascara o custo no HIT, mas no
MISS (1ª vez ou após flush por publicação) e nas rotas não cacheadas o custo de
DB continua integral. Esta tarefa ataca o custo de DB em si.

## Diagnóstico

Os **ServiceProviders de layout** (`app/Providers/*`) registram view composers
que rodam em **toda** requisição de portal e reexecutam dezenas de queries de
dados quase-estáticos a cada request — mesmo quando o FPC serviria o HTML do
corpo. Inventário antes da mudança:

| Provider | Queries por request (aprox.) | Dados |
|---|---|---|
| `MenuServiceProvider` | menu header (com `subItens` aninhados) + menu rápido + atalhos + `contact_link` | menu/categorias |
| `FooterServiceProvider` | config rodapé (~17 keys) + 2 menus rodapé + redes sociais + `UpdateLogs` | footer/config |
| `ConfigHeaderServiceProvider` | `Indicator` + ~28 keys de `Configuration` + `Supplier` (fornecedor) + acessos rápidos + highlight | header/config |
| `BannerServiceProvider` | banners + super banners | banners |
| `NewsMorePopularServiceProvider` | top-3 notícias populares | sidebar |
| `ModulosServiceProvider` | **já** `Cache::rememberForever('modulos')` | módulos |

Ou seja, ~15–20 queries de dados estáveis eram refeitas em todo request de
portal (e também em requests de admin, pois os providers são globais).

## Solução

Envolvido cada dataset de layout em `Cache::remember('<prefix>', $ttl, fn)` com
`$ttl = config('cache.page_cache.ttl', 300)` (mesma janela do FPC), nos providers:

- `MenuServiceProvider`: `layout.menu_header`, `layout.menu_rapido`,
  `layout.menu_atalhos`, `layout.contact_link`
- `FooterServiceProvider`: `layout.footer_config`, `layout.footer_menus`,
  `layout.footer_redes`, `layout.system_version`
- `ConfigHeaderServiceProvider`: `layout.header_bulletins`, `layout.header_config`,
  `layout.header_fornecedor`, `layout.header_acessos`, `layout.header_highlight`
- `BannerServiceProvider`: `layout.banners`, `layout.super_banners`
- `NewsMorePopularServiceProvider`: `layout.news_more_popular`

Os resultados (Collections/Models com relações eager-loaded) são serializados no
store de Cache (file, igual ao FPC). Ao reidratar, as relações permanecem
carregadas, então as views recebem exatamente os mesmos dados de antes — a
mudança é transparente para a renderização.

### Eager loading (with/load)

Os providers já usavam `with(...)` nas relações (nenhum N+1 novo introduzido). As
controllers de portal (`PortalController`, `NewsController`, `PublicationController`)
também já aplicam `with()` nos relacionamentos principais. O gargalo real não era
N+1 nas controllers, e sim a **repetição** das queries de layout em todo request —
resolvido pelo cache acima.

> Nota: otimizações mais agressivas no `PortalController::index` (ex.: cachear os
> blocos de homepage, `getTemplateActived()`) exigem perfilamento com DB real em
> canário e foram deixadas como follow-up para não arriscar regressão no HTML
> (o `index` já é coberto pelo FPC no HIT).

## Invalidação

As chaves de layout compartilham o TTL do FPC (teto de obsolescência de 300s).
Além disso, `PageCache::flush()` (disparado por `created`/`deleted` dos models de
conteúdo em `AppServiceProvider`) agora também chama `PageCache::flushLayout()`,
que faz `Cache::forget` de todas as chaves `layout.*`. Assim, uma publicação
invalida tanto o FPC quanto os caches de layout em conjunto.

`PageCache::LAYOUT_KEYS` é a lista canônica das chaves (deve espelhar os prefixes
usados nos providers).

### Reforço (edição de CMS sem esperar TTL)

Para que **edições** de menu/config/banner no CMS reflitam imediatamente (e não
só após expirar o TTL de 300s), `AppServiceProvider::boot()` registra observers
extras — sem o thrash do contador de views:

- `saved` + `deleted` em `MenuItem`, `Configuration`, `Highlight`, `Indicator`,
  `Supplier`, `UpdateLogs` → `PageCache::flush()` (FPC + layout na hora). Esses
  models são alterados por formulários admin, nunca por `increment('clicks')`.
- `Content::saved` **somente** quando a categoria do conteúdo está em
  `PageCache::LAYOUT_CONTENT_CATEGORIES` (`banners`, `super-banners`, `rede-social`,
  `acesso-rapido`, `destaques`) → `PageCache::flush()`. Assim editar banner/rede/
  acesso limpa na hora; o `increment('clicks')` de notícias/páginas (categoria
  comum) **não** dispara purga, preservando o FPC estável.

> O `increment()` do Eloquent usa o query builder e **não** dispara `saved`/`updated`,
> então o contador de visualizações continua sem purgar cache.

## Validação

- Teste unitário `PageCacheTest::flush_invalida_tambem_os_caches_de_layout_l2_4`:
  garante que `flushLayout()` e `flush()` limpam todas as chaves `layout.*`.
- `tests/Unit/LayoutCacheTest.php` (4 testes de invalidação): `MenuItem::saved` e
  `Highlight::saved` limpam FPC+layout; `Content::saved` em categoria de layout
  limpa, em categoria comum **não** limpa (sem thrash).
- Suite unitária: 37/37 OK no container.
- **Medir em canário** (`medir_estrutura.sh` / `comparar.py`): comparar `origem`/
  `ttfb` e número de queries por request ANTES (baseline) × DEPOIS. Esperado:
  queda de ~15–20 queries/req em MISS e admin; no HIT do FPC o ganho é indireto
  (menos pressão de DB sob tráfego).

## Arquivos

- `app/Providers/MenuServiceProvider.php`
- `app/Providers/FooterServiceProvider.php`
- `app/Providers/ConfigHeaderServiceProvider.php`
- `app/Providers/BannerServiceProvider.php`
- `app/Providers/NewsMorePopularServiceProvider.php`
- `app/Providers/AppServiceProvider.php` (observers de invalidação reforçada)
- `app/Http/Middleware/PageCache.php` (`LAYOUT_KEYS` + `LAYOUT_CONTENT_CATEGORIES` + `flushLayout()`, chamado em `flush()`)
- `tests/Unit/PageCacheTest.php` (teste de invalidação)
- `tests/Unit/LayoutCacheTest.php` (testes de invalidação por model event)
- `tests/Unit/CountingCacheRepository.php` (fake contador de execuções de closure)
