# L2.3 — Full-Page Cache (FPC) de origem · *Critérios A1/A2*

**Camada:** L2 (Cache de aplicação Laravel) · **Critérios:** `origem`, `ttfb`, `cache_hit`
**Stack:** Laravel 5.5.50 / PHP 7.2.34 · **Risco:** médio · **Esforço:** já implementado

> Documentação dedicada do Full-Page Cache de origem. Para minificação de HTML, ver
> `html-minify-l6-1.md`; para WebP, ver `webp-middleware-l5.md`.

---

## 1. Objetivo

Reduzir o custo de **origem/TTFB** das rotas públicas do portal servindo o HTML já
renderizado diretamente do store de Cache (file/redis) em hits subsequentes — o Laravel
**não re-renderiza** (controllers, ServiceProviders de layout, views e queries não rodam).

Funciona **em conjunto** com o `CacheControl` (L1.1/L1.2): num MISS deixa o `CacheControl`
rodar (remove cookie + estabiliza HTML via strip do token CSRF) e só então grava o corpo;
num HIT devolve cedo (antes do `StartSession`) e reaplica o `CacheControl` para manter
`Cache-Control: public`, `ETag` e o `304` (A2) idênticos a uma resposta fresca.

---

## 2. Arquitetura

### 2.1 Middleware — `app/Http/Middleware/PageCache.php`

Registrado como **1º do grupo `web`** (`app/Http/Kernel.php`), portanto roda antes de tudo
(inclusive `StartSession`) e pode short-circutar a resposta em caso de HIT.

**Guards (só cacheia):**
- `method() === 'GET'`
- controller no sub-namespace `Portal\` (rotas públicas do portal — home, notícias,
  conteúdos, carta de serviços, agenda, secretarias, transparência…)
- rota não excluída (apenas `csrf` — o endpoint que fornece o token client-side)
- usuário **não** autenticado (`auth()->check()`)
- resposta `200` + `Content-Type: text/html` + **sem** `Set-Cookie`

**Em HIT:**
```php
$body = Cache::get($key);
$cached = new Response($body, 200, ['Content-Type' => 'text/html; charset=UTF-8']);
$cached->headers->set('X-Page-Cache', 'HIT');
return (new CacheControl())->handle($request, function () use ($cached) {
    return $cached;
});
```
O HTML cached já está sem o token CSRF (foi estabilizado no MISS), então o `CacheControl`
reaplicado produz o mesmo `ETag` constante → `304` funciona.

**Em MISS:** deixa `$next` rodar, e se `shouldCache()` for verdadeiro, grava
`Cache::put($key, $body, $ttl)` e marca `X-Page-Cache: MISS`.

**Chave:** `page_cache:v1:` + `md5(fullUrl() . '|' . locale)`.

### 2.2 Invalidação

- **TTL** (`config/cache.page_cache.ttl`, default 300s) — teto de obsolescência.
- **Eventos de modelo** (`app/Providers/AppServiceProvider.php`): `created`/`deleted` dos
  models de conteúdo configurados em `cache.page_cache.invalidate_models` disparam
  `PageCache::flush()`; `saved` dos models de layout (menu/categorias/config) também.
  `Content` (Prefecture) dispara flush só quando a edição atinge categoria de layout
  (`PageCache::LAYOUT_CONTENT_CATEGORIES`) — views (`increment('clicks')`) não geram thrash.
- **Comando:** `php artisan cache:page:clear` (flush manual).
- **L2.4:** `PageCache::flushLayout()` invalida também os caches de dados de layout
  (menu/categorias/config/banners) que compartilham a janela de obsolescência do FPC.
- **(A4) purge do Multi-CDN:** fora do app — o cache de origem aqui é independente; o CDN
  respeita `s-maxage=300` + `stale-while-revalidate=86400` (ver L1.1).

### 2.3 Config — `config/cache.php`

```php
'page_cache' => [
    'enabled' => env('PAGE_CACHE_ENABLED', true),
    'ttl' => env('PAGE_CACHE_TTL', 300),
    'invalidate_models' => [
        \App\News::class,
        \App\Content::class,
        // ... models de conteudo publico
    ],
],
```

---

## 3. Arquivos afetados

- `app/Http/Middleware/PageCache.php` — **FPC**
- `app/Http/Kernel.php` — 1º do grupo `web`
- `app/Http/Middleware/CacheControl.php` — reaplicado no HIT
- `app/Providers/AppServiceProvider.php` — invalidação por evento de modelo
- `app/Console/Commands/ClearPageCache.php` — `cache:page:clear`
- `config/cache.php` — `page_cache`

---

## 4. Validação (container `public_infra_update-app-1`)

| Teste | Resultado |
|---|---|
| `php -l` em `PageCache.php` / `Kernel.php` | ✅ sem erros |
| `GET /` (warm-up) → `X-Page-Cache: MISS` | ✅ |
| `GET /` (2ª) → `X-Page-Cache: HIT` | ✅ (origem não re-renderiza) |
| `GET /agenda` (rota **não-home**, warm-up) → MISS | ✅ |
| `GET /agenda` (2ª) → HIT | ✅ **FPC cobre home E não-home igualmente** |
| `cache:page:clear` | ✅ limpa registry + layout |

---

## 5. Ressalvas / riscos conhecidos

- **Namespace `Portal\` apenas:** rotas públicas fora desse namespace (ex.:
  `Prefecture\NewsLetterController`, preview de diário) **não** são cacheadas — são de baixo
  valor e/ou autenticadas; se precisar, relaxar o filtro em `PageCache::handle()`.
- **Autenticado:** páginas com sessão logada não são cacheadas (defesa).
- **Obsolescência:** até `ttl` (300s) o conteúdo publicado pode aparecer "velho" na origem;
  a invalidação por evento de modelo corta isso na edição via CMS.
- `MAX_KEYS = 2000` (registry em memória do cache) — acima disso, desliza as mais antigas.

---

## 6. Rastreabilidade

- Issue: `public_infra_update-XXXX` (L2.3) — **implementado**.
- EPIC: `plan/2026-08-09-infra-update.epic.md` § L2.3 e § 8.
