# Migração do accia de PHP 7.4 para PHP 8.3 — Design

- **Data:** 2026-10-22
- **Branch:** `migracao-php83` (worktree `C:\projetos\apps\accia-php83`)
- **Base:** `main` @ `3205ad5`
- **Status:** aprovado em conversa (seções 1, 2 e 3); aguardando revisão da spec escrita

## 1. Objetivo

O sistema accia passa a rodar em PHP 8.3 em um container Docker próprio, **sem regressões** em relação ao PHP 7.4 atual. Isso precisa ser comprovado por uma validação comparativa (7.4 × 8.3) feita com os dados reais do banco `psec_vialgpd`, usando uma cópia.

Durante a transição, o mesmo código deve continuar rodando também em PHP 7.4. Isso permite comparação lado a lado e rollback trivial.

### Fora de escopo
- Modernizar a sintaxe (tipagem, readonly, enums, match etc.).
- Merge na `main`, deploy em produção e mudanças no container `php74_apache` existente, que também atende outros 8 sistemas.
- CasperJS (`modules/performance/controllers/AnexoController.php:1097`): o caminho fixo `/var/www/html/psec/node_modules/casperjs` não existe nem no ambiente 7.4 atual. Fica registrado como "não verificável localmente".
- Reescrever o pagamento: o PagSeguro é **removido** (seção 4.3), não substituído.

## 2. Situação atual (levantada em 2026-10-22)

| Item | Valor | Origem |
|---|---|---|
| Código | `C:\projetos\apps\accia`, 1.843 arquivos PHP próprios, 123 controllers, 1.084 actions | `find`/`grep` |
| Framework | `yiisoft/yii2` **2.0.16.1** | `vendor/yiisoft/yii2/BaseYii.php:96`, `composer.json` |
| Runtime | container `php74_apache` (imagem `projetos-webserver`, PHP 7.4.33, Apache + mod_rewrite), vhost `accia.localhost` → `/var/www/html/accia/web` | `docker inspect`, `C:\projetos\apache\vhosts` |
| Extensões | ctype curl date dom fileinfo filter ftp gd hash iconv intl json libxml mbstring mysqli mysqlnd openssl pcre PDO pdo_mysql pdo_sqlite Phar posix readline Reflection session SimpleXML sodium SPL sqlite3 standard tokenizer xml xmlreader xmlwriter zip zlib | `php -m` |
| Banco | `psec_vialgpd` no `mysql57` (MySQL 5.7.44), 225 tabelas, ~7,5 GB, rede `projetos_default` | `information_schema` |
| Config | `.env` carregado por `Dotenv::createUnsafeImmutable` (`web/index.php:24`, `yii:23`): variáveis de ambiente do container **prevalecem** sobre o `.env` | leitura |
| Sessão | `yii\web\DbSession` (`config/web.php`) | leitura |
| E-mail | `yii\swiftmailer\Mailer` → `smtp.sendgrid.net:587` TLS (`config/web.php:65-73`, `config/console.php:59`) | leitura |
| error_reporting 7.4 | `22519` = `E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED` (sem php.ini) | `php -r` no container |
| error_reporting 8.3 | `32767` = `E_ALL` (imagem oficial) | `php -r` em `php:8.3-cli` |

### 2.1 Risco principal: avisos viram erro 500
`vendor/yiisoft/yii2/base/ErrorHandler.php:207-228` lança `ErrorException` para qualquer erro incluído em `error_reporting()`.

No PHP 8, os avisos de "Undefined array key / variable / property / offset" passaram de `E_NOTICE` (silenciado hoje) para `E_WARNING`. Todo acesso a índice inexistente que hoje passa em silêncio vira **HTTP 500** no 8.3. Isso só é detectável em runtime, e é o motivo do crawl comparativo da seção 6.

### 2.2 Dependências que bloqueiam PHP 8.3 (composer.lock)

| Pacote (versão no lock) | Restrição PHP | Uso no accia | Decisão |
|---|---|---|---|
| `yiisoft/yii2` 2.0.16.1 | sem suporte a 8.x | todo o sistema | `~2.0.53` (min. PHP 7.4; suporte a 8.3 desde a 2.0.51) |
| `phpoffice/phpexcel` 1.8.2 | `^5.2\|^7.0`, abandonado | `modules/performance/models/GppTemp.php:95-96` | trocar por `phpoffice/phpspreadsheet ^1.29` |
| `2amigos/qrcode-library` 1.1.2 → `bacon/bacon-qr-code` 1.0.3 | `^5.4\|^7.0` | `modules/performance/controllers/ArvoreController.php:238`, `modules/performance/controllers/DefaultController.php:505` | `~3.1` (php ≥7.3, bacon ^2), ajustando a API se houver diferença |
| `rkorebrits/htmltoopenxml` 0.1.9 | `^7.2`, abandonado | só em código comentado (`AnexoController.php:1901-1916`) | remover do composer |
| `pagseguro/pagseguro-php-sdk` 6.0.0 | `^7.1.3`, sem release desde 2018 | `models/Licenca.php:322-415` | **remover a funcionalidade** (seção 4.3) |
| `mpdf/mpdf` 8.0.13 | até `~8.1.0` | relatórios PDF | `^8.2` |
| cadeia dev: `codeception/*` 4.x, `module-yii2` 1.1.3 (`<8.1`), `phpspec/prophecy` 1.14 (`<8.2`), `symfony/browser-kit` 4.2.4, `mockery` | `<8.1`/`<8.2` | `tests/` é o modelo padrão do Yii (`ContactFormCest`, `LoginFormCest`), não testa o accia | remover do `require-dev` (manter `yii2-debug`, `yii2-gii`, `yii2-faker`); a pasta `tests/` não é apagada |
| pacotes `dev-master` (kartik-v/*, mdmsoft/yii2-admin, kop, darkcs, webvimark, buttflattery, bitcko, nirvana-msu, bscheshirwork) | variável | widgets | fixar na tag estável compatível com 7.4 e 8.3 quando houver; se não houver, fixar o commit atual por hash (`dev-master#<hash>`) |

Fontes:
- [UPGRADE.md do Yii2](https://raw.githubusercontent.com/yiisoft/yii2/master/framework/UPGRADE.md)
- [Yii 2.0.51 — correção do error handler para PHP 8.3](https://www.yiiframework.com/news/650/yii-2-0-51)
- [Packagist: pagseguro-php-sdk](https://repo.packagist.org/p2/pagseguro/pagseguro-php-sdk.json)
- [Packagist: qrcode-library](https://repo.packagist.org/p2/2amigos/qrcode-library.json)
- [Packagist: htmltoopenxml](https://repo.packagist.org/p2/rkorebrits/htmltoopenxml.json)

### 2.3 Código próprio já identificado (busca estática; a lista completa sai da etapa 6.2)
- Parâmetro opcional antes de obrigatório (deprecation 8.0+; o parâmetro passa a ser obrigatório):
  - `modules/financial/controllers/BpTipoController.php:456`
  - `modules/financial/controllers/DreTipoController.php:421`
  - `modules/financial/controllers/TipoController.php:427`
  - `modules/operational/controllers/DashboardController.php:504`
  - `modules/performance/models/Coleta.php:6613`
- `strftime` (deprecated 8.1): `components/PHPlot.php:5379`.
- `utf8_decode`/`utf8_encode` (deprecated 8.2): 13 ocorrências.
- Quebras do Yii 2.0.16 → 2.0.5x que tocam o accia:
  - `DbSession` com strict mode (2.0.38);
  - validação de sessão por `authKey` (2.0.40): `models/User.php:230` retorna `auth_key`; 1 dos 447 registros de `user_ceon` está sem `auth_key`;
  - `parent::init()` obrigatório em controllers e semântica de validadores inline (2.0.35);
  - `db\Exception::getCode()` passa a retornar string (2.0.36);
  - typecast de colunas `UNSIGNED` (2.0.42).

## 3. Infraestrutura

Todos os arquivos novos ficam em `C:\projetos\php83\`. Nada no `C:\projetos\docker-compose.yml` nem no container `php74_apache` é alterado.

### 3.1 Imagem `php83-apache` (`C:\projetos\php83\Dockerfile`)
- Base `php:8.3-apache` (Debian bookworm).
- Extensões: `pdo_mysql mysqli gd` (com jpeg/png/freetype) `zip intl`; as demais da lista da seção 2 já vêm na imagem base. A paridade é conferida por `php -m` nos dois containers.
- `a2enmod rewrite`.
- Composer 2 copiado de `composer:2`.
- `php.ini` derivado de `php.ini-development`:
  - `error_reporting=E_ALL`, `display_errors=Off`, `log_errors=On`;
  - `error_log=/var/log/php/php_errors.log` (volume para o host, para medição);
  - `memory_limit` e `upload_max_filesize` iguais aos efetivos do 7.4, conferidos por `php -i`.
- Certificado do Mailpit (seção 3.4) instalado no CA store (`update-ca-certificates`).

### 3.2 `C:\projetos\php83\docker-compose.yml`
- **`php83_apache`:**
  - porta `8083:80`;
  - volumes `C:\projetos\apps:/var/www/html` e `C:\projetos\php83\vhosts:/etc/apache2/sites-enabled`;
  - ambiente `APP_DATABASE_HOST=mysql57`, `APP_DATABASE_DB=psec_vialgpd_php83`, demais valores iguais ao `.env`;
  - redes: `projetos_default` (externa, para alcançar o `mysql57`) e `php83_val` (nova, interna ao compose). O `mailpit_php83` fica **só** na `php83_val`, com alias `smtp.sendgrid.net`. Containers fora dela, como o `php74_apache` real, não enxergam o alias e continuam enviando ao SendGrid de verdade;
- **`mailpit_php83`:** `axllent/mailpit`, UI em `8025`, SMTP 587 com STARTTLS, apenas na rede `php83_val`, com alias `smtp.sendgrid.net`.
- **`php74_val`** (perfil `validacao`, temporário): imagem `projetos-webserver` existente, porta `8074:80`, mesma montagem, banco `psec_vialgpd_php83`, redes `projetos_default` e `php83_val` (SMTP interceptado como no alvo). Usado como base de comparação.
- **Vhosts** (`C:\projetos\php83\vhosts`):
  - `accia.localhost` → `/var/www/html/accia-php83/web` (alvo);
  - `php74_val` monta um diretório próprio, `C:\projetos\php83\vhosts-74val`, com `accia.localhost` → `/var/www/html/accia/web` (base). Na rodada final (critério 7), esse vhost é trocado para `/var/www/html/accia-php83/web` e o container é reiniciado.

### 3.3 Cópia do banco
- Dentro do `mysql57`: `mysqldump --single-transaction --routines --triggers --events psec_vialgpd | mysql psec_vialgpd_php83`.
- O banco original só é **lido**.
- Conferência: `COUNT(*)` das 225 tabelas nos dois bancos, com resultado idêntico, salvo em relatório.

### 3.4 Mailpit
- Certificado autoassinado com CN/SAN `smtp.sendgrid.net`, gerado em `C:\projetos\php83\mailpit\`.
- O código continua configurado para o SendGrid; dentro dos containers de validação, o host resolve para o Mailpit. Nenhum e-mail sai para a internet.
- Prova: `curl` do container de validação para `smtp.sendgrid.net:587` chegando ao Mailpit (banner SMTP do Mailpit).

## 4. Mudanças de código e dependências

Todas na branch `migracao-php83`, **um commit por camada**, cada um com mensagem descritiva.

### 4.1 Composer
1. Editar o `composer.json` conforme a tabela 2.2.
2. Adicionar `config.platform.php = "7.4.33"`, para que o resolvedor só aceite versões que rodam nos dois PHPs.
3. `composer update` dentro de `php83_apache`.
4. `composer validate`, `composer check-platform-reqs` (em 7.4 e 8.3) e `composer audit` (só registro).

### 4.2 Correções em camadas
- **a. Lint:** `php -l` (8.3) em todos os arquivos próprios; erros de sintaxe corrigidos primeiro.
- **b. Estático:** `phpcs --standard=PHPCompatibility --runtime-set testVersion 7.4-8.3` no código próprio (exclui `vendor/`, `runtime/`, `web/assets/`). Relatório salvo e cada item corrigido à mão:
  - `strftime` → `date()`/`IntlDateFormatter`, com a mesma saída;
  - `utf8_decode`/`utf8_encode` → `mb_convert_encoding($s, 'ISO-8859-1', 'UTF-8')` e o inverso;
  - interpolação `"${var}"` → `"{$var}"`;
  - parâmetro opcional antes de obrigatório: para cada método, os chamadores são lidos antes de decidir entre tornar obrigatório ou dar default ao seguinte. O critério é preservar o comportamento atual observado no 7.4.
- **c. Rector:** só regras de compatibilidade que não exigem PHP 8 e não mudam comportamento. Nenhum set de nível (`UP_TO_PHP_*`). Cada regra aplicada é listada no commit.
- **d. Quebras do Yii 2.0.16 → 2.0.5x:** cada nota do UPGRADE.md listada em 2.3 é verificada no código e registrada como "afeta / não afeta". O usuário de `user_ceon` sem `auth_key` é registrado. Correção de dados só com autorização.
- **e. Runtime:** cada warning ou exceção nova encontrada pelo crawl (seção 6) é corrigida **na causa** (`isset`, `??`, verificação de tipo). Nunca com supressão `@` nem reduzindo o `error_reporting`.

### 4.3 Remoção do PagSeguro
O fluxo já está inativo no código: `actionCompra` retorna `false` em `controllers/LicencaController.php:553` e `actionUsar` lança HTTP 500 em `:683`.

**Removido:**
- `pagseguro/pagseguro-php-sdk` do composer;
- `Licenca::geraPagseguro()` e `Licenca::validaTransaction()` (`models/Licenca.php:322-415`);
- `LicencaController::actionPagseguro()` (`:853-857`) e `views/licenca/pagseguro.php`;
- o bloco inalcançável depois do `return false;` em `actionCompra` (`:554-677`);
- o `<script>` do lightbox do PagSeguro em `views/layouts/main_1.php:42` (layout sem nenhuma referência no código).

**Mantido:**
- tabela `licenca` e seus dados, colunas e constantes (`FORMA_PAGSEGURO`, `PAGSEGURO_PAGO` etc.);
- o rótulo "Cód. PagSeguro";
- `actionNotificacao`, que não usa o SDK;
- o botão "Comprar licença" de `main_1.php:333`.

Imports e `use` que ficarem órfãos são removidos.

### 4.4 `error_reporting`
- Validação: `E_ALL`.
- Configuração final recomendada para produção (entregue no Dockerfile como `php.ini` de produção):
  - `error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT`;
  - deprecations registradas em log, não exibidas.
- A decisão de aplicar isso em produção é do usuário.

## 5. Critérios de aceite

1. `php -l`: zero erros em 8.3 e em 7.4 para todos os arquivos próprios.
2. PHPCompatibility 7.4–8.3: zero erros; avisos residuais listados com justificativa.
3. `composer check-platform-reqs` ok em 7.4 e 8.3; `composer.lock` com `platform.php = 7.4.33`.
4. `./yii` e `./yii help <cmd>` de todos os comandos com saída equivalente em 7.4 e 8.3.
5. Crawl comparativo: zero divergências não explicadas entre `php74_val` (código original) e `php83_apache` (código migrado). Divergência = status diferente, erro só no 8.3, entrada nova em log, erro JS novo ou diferença de HTML normalizado acima do ruído.
6. Fluxos críticos de escrita (6.4) com o mesmo efeito no banco nos dois ambientes.
7. Rodada final: `php74_val` servindo `accia-php83` também sem divergências, o que prova a compatibilidade dupla.
8. `grep -rn 'PagSeguro\\'` no código próprio sem nenhum resultado; telas de licença com o mesmo comportamento da base.
9. Nenhum e-mail enviado para fora (log do Mailpit com todas as mensagens geradas na validação).
10. Banco `psec_vialgpd` e container `php74_apache` inalterados (`CHECKSUM TABLE` das tabelas de log/sessão antes e depois; `docker inspect` sem mudanças).

## 6. Validação com dados do banco

Relatórios em `docs/migracao-php83/validacao/` (Markdown + JSON + capturas).

### 6.1 Ambientes
| | Base | Alvo |
|---|---|---|
| Container | `php74_val` (7.4.33), porta 8074 | `php83_apache` (8.3), porta 8083 |
| Código | `accia/` (`main`) | `accia-php83/` (`migracao-php83`) |
| Banco | `psec_vialgpd_php83` | `psec_vialgpd_php83` |

Como cada acesso grava log (`Base::loga`) e sessão (`DbSession`), o banco real nunca é usado na validação.

### 6.2 Etapas automáticas
1. Lint (critério 1).
2. PHPCompatibility (critério 2).
3. Composer (critério 3).
4. Console (critério 4).
5. **Inventário de rotas:** script lê os 123 controllers e gera módulo/controller/action/parâmetros/regras de acesso (`behaviors()`, `AccessControl`, RBAC). Para parâmetros de ID, busca IDs reais na cópia (menor, maior e mediano da tabela do model correspondente). Rotas sem como inferir parâmetro ficam listadas como "sem parâmetro inferido" e são cobertas pela navegação por links.
6. **Crawl (Python Playwright + Chromium já instalados):**
   - login com o usuário de validação local (`999.999.999-99`) nos dois ambientes;
   - passada A: rotas do inventário (só GET); passada B: BFS pelos links do menu e das telas (mesmo host, só GET);
   - exclusões: rotas com `delete`, `excluir`, `remove`, `logout`, `limpa`, `envia`, `send`, `backup`, `restore`, além de verbos não-GET definidos em `VerbFilter`;
   - por URL, nos dois ambientes: status, tempo, marcador de página de erro do Yii, novas linhas em `runtime/logs/app.log` e em `php_errors.log`, erros no console do navegador, tamanho e diff do HTML normalizado (remove CSRF, timestamps, IDs de sessão e nonces).
7. **Capturas de tela:** página inicial e uma tela por controller, nos dois ambientes, com diff visual (pixelmatch). Diferenças acima do limiar são anotadas.

Ciclo: divergência → correção (4.2.e) → novo crawl, até atender o critério 5.

### 6.3 Saída do crawl
Tabela por URL: rota, status 7.4, status 8.3, erros/logs novos, diferença de HTML, captura, veredito ✅/❌ e motivo.

### 6.4 Fluxos críticos de escrita (POST, na cópia, nos dois ambientes)
Para cada um: estado do banco antes/depois (consulta das tabelas afetadas), resposta HTTP e e-mail no Mailpit, quando houver.
1. Login, logout e persistência da sessão (`session`, `auth_key`).
2. Um create + update representativo por módulo: `performance`, `financial`, `operational`, `contract`, `risk`, `task`. A tela exata de cada módulo é escolhida no plano a partir do inventário.
3. Importação de planilha (`GppTemp`, PhpSpreadsheet), com o mesmo arquivo nos dois ambientes.
4. Geração de PDF (mpdf) e Word (PhpWord, `AnexoController`), comparando texto extraído e número de páginas.
5. QR code (`ArvoreController`, `performance/DefaultController`), comparando o decodificado.
6. Endpoints JSON de `modules/api`, comparando a estrutura e os valores do JSON.
7. Um fluxo que envia e-mail, conferindo destinatário e assunto no Mailpit.

### 6.5 Não verificável localmente
- CasperJS (seção 1).
- Integrações externas reais: SendGrid, que é interceptado. Qualquer outra chamada externa encontrada no inventário entra nesta lista com o motivo.

## 7. Entregáveis
- Branch `migracao-php83` com commits por camada.
- `C:\projetos\php83\` (Dockerfile, docker-compose.yml, vhosts, php.ini de validação e de produção, Mailpit).
- Banco `psec_vialgpd_php83` (cópia de validação; a remoção depois da entrega é decisão do usuário).
- Relatórios em `docs/migracao-php83/validacao/`.
- Relatório final: Feito / Critérios / Não verificado / Pendências.

## 8. Rollback
- Código: a `main` não é alterada; descartar a branch e a worktree volta ao estado atual.
- Infra: `docker compose -f C:\projetos\php83\docker-compose.yml down` remove apenas os containers novos.
- Banco: `DROP DATABASE psec_vialgpd_php83` (só com autorização).
