# 10 — Fluxos de gravação: PHP 7.4 (original) x PHP 8.3 (migrado)

Data: 2026-10-22 · Banco exclusivo: `psec_vialgpd_fluxos` em `mysql57_php83` (cópia limpa, 225 tabelas, dump 13:36→13:56).
Ambientes: `fluxo74` = http://localhost:8174 (`php74_fluxos`, código `C:\projetos\apps\accia`) · `fluxo83` = http://localhost:8183 (`php83_fluxos`, código `C:\projetos\apps\accia-php83`).
Usuário: id 4 (`user_ceon`, CPF digitado com máscara). E-mail: Mailpit (`mailpit_php83`), nenhum envio externo.


## Resultado final (após o lote 7, commit `daa6ae2`)

Rodada completa dos 18 fluxos em `fluxo74` e `fluxo83` (`fluxos-comparacao.json`):

| Fluxo | 7.4 | 8.3 | Veredito |
|---|---|---|---|
| login + DbSession | OK | OK | IGUAL |
| e-mail inventário scope (Mailpit) | OK | OK | **IGUAL** (era regressão A1: SwiftMailer → symfony/mailer) |
| performance: árvore create+update | OK | OK | IGUAL |
| financial: tipo create+update | 403 | 403 | IGUAL (usuário sem contexto permitido, nos dois) |
| contract: provider create+update | OK | OK | IGUAL |
| risk: modelo de risco create+update | OK | OK | IGUAL |
| task: tarefa create | recusado | recusado | IGUAL (usuário sem painéis, nos dois) |
| operational: ocorrência create (JSON) | OK | OK | IGUAL |
| GPP `gpp.xlsx` (fixture com indicador inválido) | 500 | 500 | IGUAL |
| GPP `gpp-valido.xlsx` | OK | OK | **IGUAL** (era regressão A2) |
| GPP `gpp-valido.xls` | 500 | OK | **melhoria**: o 7.4 falha no PHPExcel antigo (`OLE.php:288`); o 8.3 importa |
| GPP `gpp.xls` | 500 | 500 | falha nos dois (7.4: PHPExcel; 8.3: validação do fixture) |
| PDF `ocorrencias/imprimir` (HTTP) | 500 | 500 | falha nos dois: `vendor/mpdf/mpdf/tmp` sem permissão de escrita (ambiente) |
| PDF mpdf (CLI, mesmas chamadas) | OK | OK | IGUAL (texto do PDF idêntico) |
| PDF `versionamento/exportar-pdf` | OK | OK | IGUAL |
| Word `anexo/create-relatorio` (HTTP) | 500 | 500 | falha nos dois: template do cliente inexistente (ambiente) |
| Word PhpWord (CLI, chamadas do laudo) | OK | OK | equivalente: o XML só difere em `<w:bidiVisual w:val="0"/>` (valor padrão, omitido pelo PhpWord 1.4); `noWrap` corrigido (A4); constante corrigida (A3) |
| API v1 GET autenticado | — | — | **IGUAL** nos 7 endpoints (era regressão A5/A6) |

Regressões A1–A6 da rodada inicial (abaixo): **todas corrigidas** no commit `daa6ae2`.

## Como reproduzir

```bash
cd /c/projetos/apps/accia-php83/tools/migracao-php83
export ACCIA_VAL_BANCO=psec_vialgpd_fluxos ACCIA_VAL_PASS='***' PYTHONIOENCODING=utf8
python fluxos.py --rotulo fluxo74      # grava docs/migracao-php83/validacao/fluxos-fluxo74.json
python fluxos.py --rotulo fluxo83      # grava fluxos-fluxo83.json
python fluxos.py --comparar            # grava fluxos-comparacao.json
python fluxos.py --rotulo fluxo83 --so fluxo_api_v1,fluxo_gpp_valido_xlsx   # subconjunto (o login sempre roda)
```

Regras da comparação (`comparar()` em `fluxos.py`): compara `status_http`, `resposta` (status, redirect sem ids, erros de formulário, título de exceção), `efeito` (linhas afetadas, sem id/timestamps, prefixo `MIGR83-<rotulo>-` normalizado para `MIGR83-*-`), `email` e `erros_cm_log` (erros level 1 do `cm_log` da sessão do navegador, `prefix like '%[<PHPSESSID>]'`, sem caminho:linha e stack). Os CRUDs só consideram linhas com `id > max(id)` lido antes do fluxo.

Arquivos de apoio criados: `tools/migracao-php83/fluxos.py` (fluxos novos), `php/word_template.php`, `php/mpdf_texto.php`, `php/gera_gpp_valido.php`, `fixtures/gpp-valido.xlsx`, `fixtures/gpp-valido.xls`.

## Resultado

| # | Fluxo | 7.4 | 8.3 | Efeito no banco igual? | Evidência |
|---|---|---|---|---|---|
| 1 | Login + DbSession (`/site/login`) | OK (200) | OK (200) | Sim | linha em `session` com `id = PHPSESSID` e `__id\|i:4;` nos dois |
| 2 | E-mail inventário scope (`/site/edita-inventario-scope?id=77&token=…`) | OK | **FALHA** | **Não** | 7.4: `inventario_scope.email_responsavel` alterado + e-mail no Mailpit. 8.3: nada gravado, nenhum e-mail; `ErrorException:8192` (achado A1) |
| 3a | performance: `/performance/arvore/create` + `update` | OK (302→view, 302→view) | OK (302, 302) | Sim | `ca_arvore`: `[24934, MIGR83-*-arvore-alterado, 1, descricao migr83, NULL, NULL, 4]` nos dois |
| 3b | financial: `/financial/tipo/create` | 403 | 403 | Sim (nada gravado) | `HttpException 403 "Contexto não permitido para seu usuário"` (TipoController.php:140) nos dois — a cópia não tem `cf_fc_tipo`, então a lista de contextos permitidos é vazia; update não exercitado |
| 3c | contract: `/contract/provider/create` + `update` | OK (200) | OK (200) | Sim | `cm_provider`: `[828, MIGR83-*-fornecedor-alterado, NULL, Contato Ação, NULL, contato@teste.local, 1]` nos dois |
| 3d | risk: `/risk/risco-modelo/create` + `update` | OK (302, 302) | OK (302, 302) | Sim | `risco_modelo`: `[MIGR83-*-modelo-alterado, desc migr83, 434, 1, 4, 245, 0]` nos dois |
| 3e | task: `/task/task/create` | 302, nada gravado | 302, nada gravado | Sim | usuário 4 sem painéis (`Logged::getMyEditorPanelsIds` vazio) e a cópia tem 0 `cm_task`; create recusado igual nos dois; update não exercitado |
| 3f | operational: `/operational/ocorrencias/create?contexto_id=25222&aplicacao_id=…` (resposta JSON) | OK | OK | Sim | JSON `{"status":"success","message":"Ocorrência salva com sucesso"}`; `co_ocorrencia` com os mesmos 13 campos nos dois. Update não exercitado (fluxo de update exige avaliação/coleta) |
| 4a | GPP `/performance/gpp-temp/upload` com `fixtures/gpp.xlsx` | 500 | 500 | Sim (rollback) | fixture tem coluna E = `IND-01`: `"Indicador ID" deve ser um número inteiro` (GppTemp.php:126) nos dois |
| 4b | GPP com `fixtures/gpp.xls` | 500 | 500 | Sim (rollback) | 7.4: `"continue" targeting switch…` em `vendor/phpoffice/phpexcel/.../OLE.php:288` (defeito do **original**, PHPExcel no PHP 7.4). 8.3 (PhpSpreadsheet) lê o .xls e chega à mesma validação de 4a |
| 4c | GPP com `fixtures/gpp-valido.xlsx` | **OK (302→index)** | **FALHA (500)** | **Não** | 7.4 grava `gpp_causa_provavel`=3, `gpp_acao_proposta`=3, `gpp_tarefa`=4. 8.3: `strtotime(): Passing null…` deprecated → exceção, rollback (achado A2) |
| 4d | GPP com `fixtures/gpp-valido.xls` | 500 (PHPExcel, 4b) | 500 (A2) | Sim (rollback), causas diferentes | 7.4 falha no leitor .xls do original; 8.3 lê o arquivo e falha em A2 |
| 5a | PDF mpdf HTTP `/operational/ocorrencias/imprimir?id=6017` | 500 | 500 | n/a | ambiente: `vendor/mpdf/mpdf/tmp` não gravável pelo www-data no bind mount (7.4: `MpdfException … not writable`; 8.3: `mkdir(): Permission denied`) |
| 5b | PDF mpdf CLI (mesmas chamadas de `actionImprimir`, `tempDir=/tmp`, `php/mpdf_texto.php`) | OK | OK | Texto igual | `pdftotext -layout` idêntico (acentos, tabela, quebra de linha) |
| 5c | PDF kartik/mpdf `/versionamento/exportar-pdf?id=1` | OK (200, application/pdf) | OK (200) | Texto igual | 5 linhas de texto idênticas |
| 5d | Word HTTP `/performance/anexo/create-relatorio?contexto_id=24934&aplicacao_id=284` | 500 | 500 | n/a | template `web/uploads/LaudoSegurancaPadrao<cliente>.docx` não existe em nenhum dos dois códigos (`copy(...): failed to open stream`); não verificado via HTTP |
| 5e | Word CLI (`php/word_template.php`: TemplateProcessor + setValue/setComplexBlock(Table)/setComplexValue(TextRun)/saveAs) | OK | OK com ajuste | **Não** (XML difere) | 8.3 (PhpWord 1.4.0): `Style\Table::WIDTH_PERCENT` não existe (achado A3); `document.xml` ganha `<w:noWrap/>` em toda célula e perde `<w:bidiVisual w:val="0"/>` (achado A4). Texto extraído igual |
| 6 | API v1 GET (`?access-token=` = `user_ceon.auth_key`; QueryParamAuth/HttpBearerAuth) | 200 em 6 de 7 | **500 em `cliente`**, JSON diferente em `lista-coleta` | **Não** | `cliente?id=245` (A5) e `lista-coleta` (A6). Iguais: `lista-cliente-contexto`, `coleta?coleta=550369`, `organograma?id=24934`, `pesquisa-estrutura?id=24934`, `lista-aplicacao?id=24934` (404 nos dois), sem token → 401 nos dois, Bearer → 200 nos dois |

Resumo: 18 fluxos executados nos dois ambientes; 11 iguais, 7 diferentes, dos quais 5 são regressões do 8.3 (A1, A2, A5, A6, A3/A4) e 2 diferenças só de mensagem em falhas de ambiente/base (5a, 5d) ou do original (4b/4d).

## E-mails capturados no Mailpit (http://localhost:8025)

| Ambiente | Para | Assunto | Observação |
|---|---|---|---|
| 7.4 | `migr83-fluxo74-<ts>@teste.local` (uma mensagem por execução: 16:58, 17:04, 17:13, 17:27 UTC) | Responda o Inventário de Dados! | destinatário e assunto conferidos pelo fluxo |
| 8.3 | — | — | nenhuma mensagem: o envio lança exceção (A1) |

## Achados (falham só no 8.3)

**Atenção ao nível de erro:** `php83_fluxos` carrega `/usr/local/etc/php/conf.d/zz-validacao.ini` com `error_reporting = E_ALL` (32767); o 7.4 roda com 22519 (`E_ALL & ~E_DEPRECATED & ~E_STRICT & ~E_NOTICE`). O `ErrorHandler` do Yii converte em exceção tudo que está em `error_reporting`. A1 e A2 são **deprecations** (quebram com E_ALL; com `php.ini-production` — `E_ALL & ~E_DEPRECATED & ~E_STRICT` — não quebrariam). A5 é **warning** e quebra em qualquer configuração; A6 é mudança de semântica do PHP 8 e independe de configuração.

- **A1 — e-mail não sai no 8.3 (deprecation).** `/site/edita-inventario-scope`: `yii\base\ErrorException:8192 Callables of the form ["Swift_Message", "Swift_Mime_SimpleMessage::__construct"] are deprecated` em `vendor/swiftmailer/swiftmailer/lib/classes/Swift/Message.php:46`, chamado de `controllers/SiteController.php:4275` (`Yii::$app->mailer->compose('_inventario_scope', …)`). Efeito: nem o e-mail nem a alteração de `inventario_scope` são gravados. Afeta todo `mailer->compose()` (SwiftMailer 6 no PHP 8.2+).
- **A2 — importação GPP falha no 8.3 (deprecation).** `/performance/gpp-temp/upload` com planilha válida: `strtotime(): Passing null to parameter #1 ($datetime) of type string is deprecated` em `modules/performance/models/GppTemp.php:261` (`strtotime($acaoExiste->termino_realizado) < strtotime($gppTarefa->termino_realizado)`; mesmo padrão nas comparações de `inicio`/`termino_previsto` em GppTemp.php:246–256). Rollback: nenhuma linha gravada; no 7.4 grava 3/3/4.
- **A3 — Word: constante removida no PhpWord 1.x (erro fatal).** `modules/performance/controllers/AnexoController.php` linhas 1116, 1179, 1234, 1283, 1326, 1400, 1457, 1515, 1659, 1754, 1909, 1958, 2007 usam `\PhpOffice\PhpWord\Style\Table::WIDTH_PERCENT`, que não existe no PhpWord 1.4.0 (`vendor/phpoffice/phpword/src/PhpWord/Style/Table.php` só tem `LAYOUT_*`; o valor `'pct'` está em `SimpleType\TblWidth::PERCENT`). Reproduzido no CLI: `Error: Undefined constant PhpOffice\PhpWord\Style\Table::WIDTH_PERCENT`. Via HTTP não chega a essa linha porque o template do laudo não existe na cópia (5d).
- **A4 — Word: layout das tabelas muda (PhpWord 1.4).** `Style\Cell::$noWrap = true` por padrão (`vendor/phpoffice/phpword/src/PhpWord/Style/Cell.php:147`) → toda célula sai com `<w:noWrap/>` (texto não quebra dentro da célula do laudo). Diferença de `document.xml` medida no fluxo 5e.
- **A5 — API `GET /api/v1/cliente?id=245` → 500 no 8.3 (warning).** `Undefined array key "contexto_ids"` em `models/UserSearch.php:263` (`$params['contexto_ids']`), chamado por `modules/api/controllers/V1Controller.php:454` (`UserSearch->searchOperador`). Código idêntico ao original; no 7.4 era notice silenciado.
- **A6 — API `GET /api/v1/lista-coleta`: texto dos indicadores muda no 8.3.** `modules/performance/models/Coleta.php:1513` `if ($e[2] == 0)` com `$e[2] = ''` (`coleta_indicador.taxa_substituicao` NULL, ex. id 11556349): no 7.4 `'' == 0` é true (nome puro, "Controle de acessos"); no PHP 8 é false e o texto vira `"Controle de acessos  (<conceito>)"`. JSON de 74 008 726 vs 74 015 086 bytes. O mesmo padrão `$e[2] == 0` existe nos outros `estrutura` do arquivo (listas montadas em Coleta.php:1684 e 5890).

## Não verificado

- Update em financial/task/operational: a cópia não tem `cf_fc_tipo`/`cm_task` e o usuário 4 não tem escopos/painéis; o create foi recusado igual nos dois. Exercitar exige dar permissões ao usuário 4 ou criar dados de apoio no banco (não feito: fora do escopo autorizado).
- PDF de ocorrência e laudo Word via HTTP: dependem de ambiente (diretório tmp do mpdf não gravável no bind mount; template do laudo ausente). Cobertos pelas reproduções CLI 5b e 5e.
- Conteúdo GPP do 8.3: não comparável enquanto A2 impede a gravação (os snapshots do 8.3 mostram as linhas do 7.4, pois o 8.3 faz rollback).
- Endpoints da API com parâmetros obrigatórios além de ids (ex. `dados-area-preocupacao`, `pesquisa-*` com `vertical_indicador_id`) e `POST /api/v1/login`: não exercitados.
