# PLATAFORMA — Migração para Laravel Moderno

> Alvo: **PHP 8.4 + Laravel 12** (a "v7"), reconstruindo a plataforma a partir do
> comportamento observável do legado `v5`/`v6`.
> O repositório **já contém** a base da v7: `public-v7/` (portal) e `cms/` (painel),
> ambos com Laravel 12 e um registro de ADRs próprio — as decisões abaixo respeitam
> esse registro, que é canônico.

---

## 1. Tabela de equivalência: Laravel 5.5 → Laravel 11/12

| Preocupação | Legado (5.5 / PHP 7.2) | V7 (Laravel 12 / PHP 8.4) | Onde já está decidido na v7 |
|---|---|---|---|
| **Autenticação** | `cartalyst/sentinel` 2.0.18 (`Sentinel::check()`, `hasAnyAccess()`, `inRole()`) | Guard `web` + provider `eloquent` nativo sobre `cms_users`, hash **Argon2id** | `public-v7/wiki/adr/ADR-037-autenticacao-do-cms.md` |
| **Autorização** | `CheckRoutePermission` por **nome da rota** + `resources`/`modulos` | Permissão = `{modulo}.{acao}` com 4 ações fixas (`ver`/`editar`/`aprovar`/`arquivar`), via **Gate nativo** | `public-v7/wiki/adr/ADR-033-controle-acesso-permissoes-perfis-suporte.md` |
| **Senha** | `password_hash` default do PHP 5.5 (bcrypt) | Argon2id + rehash no login | ADR-020, ADR-037 |
| **Validação** | 78 FormRequests + `Validator::make()` inline + `CustomValidator` (CNPJ alfanumérico) | FormRequest nativo (Laravel 11+) por ação (criar/editar separados), regras em português via `lang/` | ADR-001, ADR-012 |
| **Respostas HTTP** | `response()->json()` manual; `formatValidationErrors()` devolvendo `status_code` no corpo | `JsonResponse` nativo; erros em **RFC 7807** (`application/problem+json`) | `public-v7/wiki/adr/ADR-024-observabilidade-nativa-json-rfc7807.md` |
| **API Resources** | 21 classes `App\Http\Resources\*` (Swagger + DynamicForms) | API Resource nativo por módulo, com `whenLoaded()` e `when()` preservados | ADR-015, ADR-018 |
| **Filas (queue)** | **0 jobs** — `ShouldQueue` = 0 ocorrências; `QUEUE_DRIVER=sync` | `ShouldQueue` + **worker** para geração de PDF do diário, carta de serviços e exportações | ADR-015 (services headless) |
| **Cache** | `file` (padrão) + `Cache::rememberForever('modulos', …)` sem invalidação | **Valkey/Redis** com TTL curto para ativação de módulo e invalidação imediata | `public-v7/wiki/adr/ADR-034-ativacao-modulos-tempo-real.md` |
| **Armazenamento (storage)** | `storage/app` + `public/storage` (bind mount), caminho gravado como string livre | **Object storage S3-like (RustFS local, AWS S3 via `.env`)** desde o dia zero | `public-v7/wiki/adr/ADR-030-object-storage-s3-rustfs.md` |
| **Banco** | PostgreSQL 10 + PostGIS (pacote declarado, sem uso) | **PostgreSQL 18** em container, sem porta exposta | `public-v7/wiki/adr/ADR-007-postgresql-18-container.md` |
| **Migrations** | 358 arquivos; 382 `DB::statement()`; 126 migrations de view | Migration declarativa com `declare(strict_types=1)`, transação + `lock_timeout`, `down()` → `backup_*`, PK `uuid`, `timestamptz`, JSONB | `public-v7/wiki/adr/ADR-017-migrations-seguras-dados-desacoplados.md` |
| **Views SQL** | 75 views criadas por `CREATE OR REPLACE VIEW` | **Não há mais HTML/CSS no banco**; tema/slots em JSONB; a view some | ADR-016, ADR-017 |
| **Testes** | PHPUnit 6, 34 arquivos, concentrados no dashboard | PHPUnit 13 + **Pest** opcional, **cobertura ≥ 90%** (gate PCOV) e `infection` | `public-v7/wiki/adr/ADR-038-cobertura-90-e-rigor-de-nao-regressao.md` |
| **Roteamento** | 928 rotas em 7 arquivos; nomes duplicados; `Route::` lido em controllers | Rotas por módulo em `Routes/{portal,cms,api}.php` carregadas por convenção; **sem closure**; nomes canônicos | `public-v7/wiki/adr/ADR-014-autodescoberta-modulos-agrupamento.md`, ADR-008 |
| **Front-end** | Bootstrap 4 + jQuery + AngularJS 1.x + 6 temas Blade duplicados + 2.026 `.js` | **Tailwind 4 + daisyUI 5 + Lucide**, build único Vite, **mínimo JS**, PWA | `public-v7/wiki/adr/ADR-009-ui-cms-proprio-tailwind-daisyui.md`, ADR-010 |
| **CMS (admin)** | AdminLTE 1.22 + AngularJS + 452 Blades `prefecture/**` | **CMS próprio** em Blade + Tailwind + daisyUI; shell único `<x-layout::cms>` | ADR-009, `public-v7/wiki/adr/ADR-036-shell-cms-como-dado.md` |
| **E-mail** | `swiftmailer/swiftmailer` 6.3 (abandonado) | **Symfony Mailer** (nativo do Laravel 9+) | ADR-001 |
| **HTTP client** | `guzzlehttp/guzzle` 5.3 (EOL 2015) | `guzzlehttp/guzzle ^7` ou `Http::` nativo do Laravel | ADR-001 |
| **Exportação** | `maatwebsite/excel` 3.1 (`FromView`) | `maatwebsite/excel ^3.1` atualizado, ou CSV nativo com `streamDownload` | ADR-018 |
| **Log de auditoria** | `spatie/laravel-activitylog` 2.8 + `ActivityObserver` (IP + User-Agent) | Trilha de auditoria própria, imutável, com `blake3` e evidência | `public-v7/wiki/adr/ADR-035-trilha-de-auditoria-e-evidencias.md`, ADR-029 |
| **Rate limiting** | `throttle:60,1` só na API | Hardening de rate limiting por perfil de rota | `public-v7/wiki/adr/ADR-021-rate-limiting-hardening.md` |
| **Observabilidade** | `Log::info` em texto livre | Log **JSON** (Monolog) + `X-Request-ID` (UUIDv7) + RFC 7807 + PII mascarada | ADR-024 |
| **Análise estática** | Nenhuma | PHPStan **nível máximo** (sem relaxamento) + Larastan | `public-v7/wiki/adr/ADR-040-analise-estatica-sem-relaxamento.md`, ADR-023 |
| **Acessibilidade** | Parcial (componente `weather-box`, `aria-label` no dashboard) | **Gate de CI** com axe/sniffCSS, WCAG 2.2 AA + eMAG | `public-v7/wiki/adr/ADR-025-acessibilidade-gate-ci.md` |

---

## 2. Decisões de arquitetura propostas

### 2.1 DTOs

Legado: 21 API Resources e nenhum DTO de entrada; os controllers montam arrays
soltos. V7: um DTO `readonly` por operação de escrita, em
`Logic/DTOs/` do módulo, construído a partir do FormRequest.

```php
// exemplo ilustrativo
// src/modules/Publicacoes/Noticias/Logic/DTOs/NoticiaData.php
final readonly class NoticiaData
{
    public function __construct(
        public string $titulo,
        public string $corpo,          // JSONB estruturado (ADR-028 TipTap)
        public ?CarbonImmutable $publicadoEm,
        public array $categorias,
    ) {}
}
```

### 2.2 Actions

Legado: o controller orquestra query + view + regra. V7: **uma Action por arquivo**,
com DI, e a Action é o **único** ponto testável da regra de negócio.

```php
// exemplo ilustrativo — a forma segue o blueprint do plano 13
// src/modules/Publicacoes/Noticias/Logic/Actions/PublicarNoticiaAction.php
final class PublicarNoticiaAction
{
    public function __construct(
        private readonly NoticiaRepository $repository,
        private readonly Clock $clock,          // Core\Contracts\Clock (ADR-001)
        private readonly AuditWriter $audit,
    ) {}

    public function handle(NoticiaData $data, Noticia $noticia): Noticia
    {
        $noticia->update([
            'titulo' => $data->titulo,
            'corpo' => $data->corpo,
            'publicado_em' => $data->publicadoEm ?? $this->clock->now(),
            'status' => StatusConteudo::Publicado,
        ]);

        $this->audit->record('noticias.aprovar', $noticia);

        return $noticia;
    }
}
```

**Regra dura da v7:** `Logic/Actions/` é o único lugar com regra de negócio.
Controller = borda (HTTP) fino.

### 2.3 Policies

Legado: `CheckRoutePermission` traduz nome de rota em permissão (23 leituras de
`Route::getCurrentRoute()` em `app/`). V7: **Policies Laravel nativas**, uma por
modelo, Registered no `AuthServiceProvider` ou via `Gate::policy()`.

```php
// exemplo ilustrativo
final class NoticiaPolicy
{
    public function view(User $user, Noticia $noticia): bool
    {
        return $user->can('noticias.ver');
    }

    public function publicar(User $user, Noticia $noticia): bool
    {
        return $user->can('noticias.aprovar');
    }
}
```

**Obrigação de equivalência:** o vocabulário de ações é o da ADR-033
(`ver`/`editar`/`aprovar`/`arquivar`). O nome do módulo é o **token canônico**.

### 2.4 Enums

Legado: 2 enums PHP em `v6/app/Enum/` (`AskStylesEnum`, `AskTypesEnum`); o
resto é string solta (`status`, `type`, `link_type`, `action`).

V7: enum PHP 8.1 por domínio fechado:

| Domínio | Enum sugerido | Origem no legado |
|---|---|---|
| Ação de permissão | `Permission::ACTIONS` (4 ações) | ADR-033 |
| Status de conteúdo | `StatusConteudo: string {rascunho, publicado, arquivado}` | `contents.status` (bool) |
| Tipo de status | `StatusType` | `statuses.type` |
| Origem da ouvidoria | `OrigemManifestacao` | `ombudsman_origins` |
| Método de resposta | `MetodoResposta` | `ombudsman_response_methods` |
| Tipo de modalidade | `ModalidadeLicitacao` | `bidding_modalities` |
| Link type do VIAP | `ViapLinkType {gabinete, outro_orgao}` | `viap.link_type` |
| Preset de layout | `LayoutPreset` | `configurations.layout` (modelo1..6) |
| Tipo de mídia | `MediaKind` | `archives` |
| Idioma | `Locale {pt_BR, en, es, fr}` | ADR-012 |

### 2.5 Casts

Legado: 17 models com `protected $casts`; nenhum cast para dado sensível.

V7: casts nativos do Laravel 12 (`Attribute`):

| Cast | Substitui |
|---|---|
| `AsEnumArray` / `AsEnumCollection` | colunas `type`/`status` |
| `AsEncryptedArray` (Laravel 11+) | JSONB de conteúdo |
| `AsStringable` | — |
| `Attribute::make(get: …)` para `slug` | `slug_fix()`/`clean_slug()` (`v6/app/Helpers/functions.php:814-848`) |
| `Attribute::make(set: …)` para `cpf`/`cnpj` com hash/blind index | LGPD (crypto-shredding) |
| `AsDateTime`/`immutable_datetime` | `publication_date` |

### 2.6 API Resources

Legado: `App\Http\Resources\Swagger\V1\Portal\*` com chaves em **português**
(`titulo`, `autor`, `fonte`, `data_publicacao`, `descricao`, `interacoes`).
V7: manter o **formato do payload** (é contrato público) mas expor via Resource
nativo por módulo, e versionar a API.

**Obrigação de equivalência:** o JSON de `/api/v1/noticias` deve continuar
retornando as mesmas chaves, ou a v7 precisa de uma política de versionamento
explícita (`/api/v2/…`) e um mapa de redirecionamento.

### 2.7 Sanctum

ADR-037 optou por **sessão nativa**, sem Sanctum e sem token. Isso é coerente
com o CMS server-rendered. **Sanctum** fica reservado para a API de leitura
pública (dados abertos), onde não há sessão — mas a ADR-018 já define CORS
aberto sem credenciais para a API pública, o que torna o token desnecessário
ali também.

**Conclusão:** Sanctum **não** é obrigatório. Onde ele seria útil: um futuro
**painel do cidadão** (o cidadão logado para ver suas solicitações LAI). Deixar
como item de backlog, não como dependência do MVP.

### 2.8 Filas para PDF

Legado: **0 filas**. Todo PDF (diário oficial, carta de serviços) é gerado
**dentro da requisição HTTP**, com `Imagick` + `FPDI`/`TCPDF` + `dompdf`.

V7:

```php
// exemplo ilustrativo
final class GerarPdfDiarioJob implements ShouldQueue
{
    use Queueable;

    public function __construct(public readonly int $diarioId) {}

    public function handle(PdfConverter $converter): void
    {
        // converte, monta, grava em S3, atualiza generation_status
    }
}
```

- Fila com **retry exponencial** e **dead-letter** (job que falhou N vezes).
- Progresso: `diarios.generation_status` (coluna já existe na v6 —
  migration `2026_09_10_100200_add_generation_status_to_diarios_table.php`).
- O endpoint do CMS que hoje gera PDF **síncrono** passa a enfileirar e
  devolver 202.

### 2.9 Widgets e slots (substitui a tabela `layouts`)

Legado: a tabela `layouts` guarda **HTML** (77 KB em `preview`, 76 KB em
`tags`) e o CMS edita esse HTML.

V7 (ADR-016): **tema e layout são dados**, não código.

- Presets em enum (`LayoutPreset`) — substitui `modelo1`..`modelo6`.
- Slots registrados em runtime (`SlotRegistry`) — substitui as linhas
  `type = 'element'` da tabela `layouts`.
- Configuração (ordem, visibilidade, conteúdo, tokens) em **JSONB**.
- Widgets de outros módulos: o módulo registra um provider no
  `Core\Contracts\WidgetRegistry`; o perfil de destino referencia só o `id`
  (nunca importa) — resolve a proibição de `modules/*` importar `modules/*`
  (ADR-013).
- Regra de precedência: `default do tipo → global → preset → perfil de URL →
  instância`, com a **acessibilidade sempre por último** (ADR-016).

**Obrigação de equivalência:** o efeito do `section = 'tags'` no carrossel
(hoje em `v6/app/Http/Traits/NewsTrait.php:178`) precisa de equivalente
funcional: o widget do carrossel continua decidindo entre "destaques" e
"mais recentes", mas por **configuração do slot**, não por HTML.


## 3. Plano de migração, estimativas e riscos

A **ordem de migração por módulo** (ondas), a **nomenclatura de pasta por
módulo**, a **estimativa de esforço por área** e os **riscos e decisões de
equivalência funcional** estão em
**`06b-PLANO-DE-MIGRACAO-E-ESTIMATIVAS.md`**.

