Communitygithub.com

php-migration

Guia de migração de versão PHP (baserCMS) com receitas para correção de funcionalidades obsoletas e alterações disruptivas, organizadas por versão.

O que é php-migration?

Este skill fornece um guia completo de migração de versão PHP para o baserCMS, cobrindo as versões 8.2, 8.4 e 8.5. Ele documenta as alterações obsoletas e disruptivas mais comuns encontradas durante a atualização do PHP, oferecendo receitas de correção práticas e exemplos de código. O guia aborda desde propriedades dinâmicas obsoletas no PHP 8.2 até a depreciação de `setAccessible()` e a introdução de nomes de classe reservados no PHP 8.5. Ele também fornece estratégias para lidar com a compatibilidade com PHP 8.1, destacando quais correções devem ser adiadas até que o suporte a 8.1 seja encerrado. O skill é projetado para desenvolvedores que mantêm o baserCMS ou aplicações CakePHP similares, ajudando a identificar, corrigir e prevenir erros durante a migração de versão PHP.

Funciona com~Claude Code~Codex CLI~Cursor
npx skills add baserproject/baser-skills

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

Guia de Migração de Versão PHP (baserCMS)

Coletânea de receitas para correção de funcionalidades obsoletas e alterações disruptivas encontradas durante a atualização da versão PHP. Organizado por versão e mantido em evolução. Problemas originados do framework CakePHP são tratados no skill separado cakephp-migration.

Inclusão: PHP 8.2 / 8.4 / 8.5. Ao suportar uma nova versão, adicione uma nova seção "## PHP X.Y". Atenção: PHP 8.5 é a versão imediatamente anterior ao 9.0. Todos os recursos obsoletos serão removidos no 9.0, portanto, corrija os avisos de obsoletismo o mais rápido possível no 8.5.

Princípios Fundamentais

  • Diferencie "erro fatal" de "aviso de obsoletismo". No modo de depuração, avisos de obsoletismo aparecem na parte inferior da tela e parecem "erros", mas aqueles que aparecem em logs/debug.log com debug: são avisos (a execução continua). Fatal/Exception são os verdadeiros erros.
  • Separe regressão de falha pré-existente. Use git diff HEAD -- <file> para verificar se o arquivo em questão é alvo da sua alteração.
  • Se você alterar a assinatura de uma classe pai, ajuste também as classes filhas e interfaces.
  • Análise estática como Rector é eficaz para detecção abrangente de todos os obsoletismos.

Consulte o skill basercms-unittest para saber como executar testes de unidade, verificação de sintaxe e isolamento de falhas. O php -l (verificação de sintaxe) também exibe avisos de obsoletismo, como nullable implícito.


PHP 8.2

Obsoletismo de Propriedades Dinâmicas

A atribuição a propriedades não declaradas está obsoleta. Mensagem: Creation of dynamic property X::$prop is deprecated.

  • Solução A (recomendada): Declare a propriedade na classe.
    /** @var ThemeFoldersServiceInterface */
    protected $ThemeFoldersService;
    protected $_templateTypes = [];
    
    ※ Locais típicos são atribuições no construtor como $this->Xxx = $this->getService(...).
  • Solução B: Em classes base que afetam muitas classes filhas (ex.: classe de teste base BcTestCase com mais de 400 subclasses), aplique #\AllowDynamicProperties. O atributo é herdado pelas classes filhas, resolvendo com uma única alteração.
    #\AllowDynamicProperties
    class BcTestCase extends TestCase { ... }
    

PHP 8.4

1. Obsoletismo de Parâmetro Nullable Implícito

Tipo $x = null é um nullable implícito e está obsoleto. Torne-o explícito com ?Tipo.

// Antes
function foo(int $contentId = null, BcAbstractDetector $agent = null)
// Depois
function foo(?int $contentId = null, ?BcAbstractDetector $agent = null)
  • Detecção (regex): Corresponde a parâmetros que não têm ? na frente e têm o padrão Tipo $name = null. Exemplo: ([A-Za-z_\\][A-Za-z0-9_\\]*)\s+(\$[A-Za-z0-9_]+)\s*=\s*null (que não tenha ? imediatamente antes).
  • Ao alterar a assinatura da classe pai, ajuste também classes filhas/interfaces.
  • Aviso: Implicitly marking parameter $x as nullable is deprecated, the explicit nullable type must be used instead.

2. Obsoletismo do Parâmetro $escape em fgetcsv()

Omitir o argumento $escape está obsoleto. Torne-o explícito.

// Antes
$head = fgetcsv($fp, 10240);
// Depois
$head = fgetcsv($fp, 10240, ',', '"', '\\');
  • Implementações próprias (funções que analisam CSV com fgets + regex) não usam fgetcsv, portanto não são afetadas. Corrija apenas os locais que realmente chamam fgetcsv().

3. Obsoletismo da Chamada com Argumento Único de ReflectionProperty::setValue()

Para propriedades não estáticas, passe o objeto alvo.

// Antes
$property->setValue($value);
// Depois
$property->setValue($object, $value);
  • Aviso: Calling ReflectionProperty::setValue() with a single argument is deprecated.
  • Facilmente encontrado em utilitários de teste (helpers para geração de requisições ou manipulação de propriedades privadas).
  • Para propriedades estáticas, passe null como primeiro argumento: setValue(null, $value). Isso é válido em todas as versões 8.1 a 8.5 (não quebra a compatibilidade com 8.1).

4. Passar null para Argumentos string de Funções Internas Está Obsoleto (na verdade desde 8.1+)

Passar null para argumentos que não são ?string, como parse_url(null) / version_compare($a, null) / str_replace(..., null), gera o aviso Passing null to parameter #N ($x) of type string is deprecated. Use cast (string) para conversão explícita.

// Antes → Depois
parse_url($url);                       →  parse_url((string)$url);
version_compare($current, $required);  →  version_compare((string)$current, (string)$required);
  • Este obsoletismo foi introduzido no 8.1, mas se manifesta em massa durante a migração (especialmente para 8.4/8.5), por isso está incluído neste guia.
  • O aviso só aparece em tempo de execução quando o valor é null, portanto não pode ser capturado completamente com grep estático. Agregue todas as ocorrências de Passing null to parameter nos logs de teste completos para identificar a origem.

PHP 8.5 (lançado em 2025-11-20)

Última versão menor antes do 9.0. A maioria dos itens abaixo gera avisos, mas a execução continua; no entanto, serão removidos no 9.0, portanto é necessário corrigi-los. Listados em ordem de impacto no aplicativo/framework. ※ Apenas "0." não é um aviso, mas um erro fatal (alteração disruptiva), então é prioridade máxima.

⚠️ Atenção ao manter a compatibilidade com PHP 8.1 (composer.json com "php": ">=8.1"). Corrigir os obsoletismos do 8.5 com as "substituições recomendadas" pode fazer com que a substituição não exista no 8.1, causando Fatal no 8.1. Os itens marcados com ❌ abaixo devem ser adiados até que o suporte ao 8.1 seja encerrado (o aviso no 8.5 é aceitável). A execução no 8.5 em si não quebra o 8.1 (apenas avisos).

  • ❌ str_increment() (8.3+) → Mantenha $s++ para 8.1
  • ❌ http_get_last_response_headers() (8.4+) → Mantenha $http_response_header para 8.1
  • ❌ PDO com namespace (Pdo\Mysql::ATTR_* etc., 8.4+) → Mantenha PDO::MYSQL_ATTR_* para 8.1

Todos os outros itens (cast não padrão, offset null → '', remoção de funções de liberação automática, crase → shell_exec, case ;, __serialize) podem ser aplicados com segurança mesmo no 8.1.

0. [Alteração Disruptiva, Fatal] Referenciar Nomes de Classe Reservados (ex.: Array) com use causa Fatal

No 8.5, nomes como Array são reservados como nomes de classe especiais; usar use ...\Array; ou class Array causa Fatal (não é aviso). Se isso ocorrer durante a coleta de testes (autoload), toda a suíte de testes pode falhar ao iniciar, portanto é prioridade máxima.

Fatal error: Cannot use BcMail\Test\TestCase\Model\Array as Array because 'Array' is a special class name
  • Caso real no baserCMS: No início de MailMessagesTableTest.php, restos da era CakePHP2 (use ...\Array; / use ...\ClassRegistry;) — imports mortos para classes inexistentes — eram inofensivos até o 8.4, mas se tornaram Fatal no 8.5. Remova a linha use correspondente para resolver (as referências são para código não executado, marcado com markTestIncomplete).
  • Detecção: grep -rnE "use [A-Za-z0-9_\\\\]+\\(Array|Object|Resource|Enum|Mixed|Never|Void|Null|False|True|Iterable|Numeric)\s*;" --include="*.php".
  • Atenção: Como é fatal, pode ser detectado com php -l em arquivos individuais. No entanto, varrer com grep antes de todos os testes pode evitar que a suíte falhe durante a coleta.

1. Obsoletismo de Nomes de Cast Não Padrão (Grande Impacto)

(boolean) (integer) (double) (binary) estão obsoletos. Substitua pelos nomes padrão.

// Antes → Depois
(boolean) → (bool)
(integer) → (int)
(double)  → (float)
(binary)  → (string)
  • Detecção: grep -rnE "\((boolean|integer|double|binary)\)" --include="*.php".
  • Comum em código antigo. Pode ser substituído mecanicamente.

2. Usar null como Offset de Array Está Obsoleto

Usar null como chave em $arr[null] / array_key_exists(null, $arr) está obsoleto (anteriormente convertido implicitamente para ""). Use explicitamente uma string vazia "".

// Antes
$arr[null] = $v;  array_key_exists(null, $arr);
// Depois
$arr[''] = $v;    array_key_exists('', $arr);
  • Cuidado com locais onde uma variável é usada como chave ($arr[$key] onde $key pode ser null).
  • Difícil de detectar com grep estático (o aviso só aparece em tempo de execução quando o valor é null). Execute todos os testes no 8.5 e use grep "null as an array offset" phpunit.log para agregar as origens (on line N of <file>).
  • Estratégia de correção: ① Use $key ?? '' para chaves de atribuição/consulta (equivalente à conversão implícita anterior, garantindo que set e get usem a mesma chave), ou ② Adicione um guard $key !== null && antes de isset()/consulta. Receber retornos null de getData('batch') com ?? '' também é eficaz.
  • Caso real no baserCMS (308 ocorrências no total): BcFileUploader (alinhamento set/get de uploadingFiles[$bcUploadId] com ?? ''), BcUploadBehavior (oldEntity[$alias][$entity->_bc_upload_id] com ?? ''), MailMessagesTable ($dists[$mailField->group_valid]), SearchIndexes/Contents/PermissionsController (getData('batch') ?? ''), BcTextHelper::arrayValue / BcUtil::decodeContent ($key !== null && isset(...)).

3. Obsoletismo de Funções de Liberação de Recursos que Agora Liberam Objetos Automaticamente

Os alvos agora são objetos, liberados pelo GC, portanto chamadas explícitas de close/free estão obsoletas. Basta remover a chamada.

  • curl_close() / curl_share_close() (CurlHandle / CurlShareHandle)
  • finfo_close() (finfo)
  • imagedestroy() (GdImage) ← Comum em utilitários de processamento de imagem
  • xml_parser_free() (XMLParser)
  • Detecção: grep -rnE "\b(curl_close|curl_share_close|finfo_close|imagedestroy|xml_parser_free)\s*\(" --include="*.php".
  • Caso real no baserCMS: 4 chamadas de imagedestroy() em Imageresizer.php e BcFileUploader.php. A simples remoção é aceitável, mas para manter a intenção de liberação imediata de memória em loops, substitua por unset($img) (GdImage é um objeto, então remover a referência permite que o GC o libere. Seguro a partir de 8.1).

3-2. Obsoletismo de ReflectionProperty/Method::setAccessible() (Maior Número de Ocorrências)

setAccessible() é no-op desde PHP 8.1 (é possível acessar diretamente propriedades/métodos private/protected com getValue/setValue/invoke). Foi obsoletizado no 8.5. Basta remover a linha de chamada (seguro a partir de 8.1).

// Antes
$property = new ReflectionProperty($obj, 'foo');
$property->setAccessible(true);   // ← Remova esta linha
$property->setValue($obj, $v);
  • Caso real no baserCMS: 33 locais entre src/testes (BcUtil/BcTestCase/BaserCorePlugin/PreviewController/BcDatabaseService etc.). Como está em caminhos quentes (como classes base de teste), o número de ocorrências em tempo de execução é desproporcionalmente alto (~6.300 em todos os testes ≈ a maioria dos obsoletismos do 8.5). Corrigir isso primeiro reduz drasticamente os logs.
  • Remoção em massa (apenas para declarações isoladas): sed -i -E "/^[[:space:]]*\$[A-Za-z0-9_]+->setAccessible\(true\);[[:space:]]*$/d" <file>.
  • Atenção ao detectar: Se o padrão grep começar com - (ex.: ->setAccessible), o grep pode interpretá-lo como opção, resultando em 0 resultados. Use grep -rn "setAccessible" (evite começar com -) ou use grep -e.

4. Incremento de String Não Numérica Obsoleto

$str++ (para strings não numéricas) está obsoleto. Use str_increment(). ※ str_increment() é 8.3+. Enquanto mantiver o 8.1, mantenha $str++.

// Antes
$code++;            // Quando $code é 'AZ', etc.
// Depois
$code = str_increment($code);

5. __sleep() / __wakeup() Suavemente Obsoletos

Use __serialize() / __unserialize() (migre se a compatibilidade com PHP 7 não for necessária).

6. Obsoletismo de $http_response_header

A superglobal $http_response_header está obsoleta. Use http_get_last_response_headers(). ※ http_get_last_response_headers() é 8.4+. Enquanto mantiver o 8.1, mantenha $http_response_header.

7. Obsoletismo do Operador Crase

`comando` (um alias de shell_exec()) está obsoleto. Use shell_exec() diretamente.

8. Obsoletismo de Terminar Cláusulas case com ;

case 1; está obsoleto. Use case 1:.

  • Detecção: grep -rnE "case\s+.+;\s*$" --include="*.php" (pode ter falsos positivos; verifique visualmente).

8-2. Obsoletismo de array_key_exists(null, ...) (Induzido por Formato Inválido de Serialize)

Passar null como primeiro argumento para array_key_exists() está obsoleto (use string vazia). Quando ocorre internamente no framework (CakePHP JsonView::_dataToSerialize), a causa é um formato inválido da opção serialize em seu próprio código. Misturar 'key' => valor em uma lista de nomes de variáveis, como setOption('serialize', ['foo', 'message' => $message]), faz com que, quando $message é null, a entrada 'message' => null entre no array serialize, acionando array_key_exists(null, ...).

  • Correção: A opção serialize deve ser uma lista de nomes de variáveis de visão. Altere ['foo', 'message' => $message] → ['foo', 'message'] (os valores já foram definidos com $this->set()). Caso real no baserCMS: ações index/view de MailMessagesController.
  • Como identificar a origem quando o erro está dentro do framework: Temporariamente, no vendor, adicione if ($key === null) file_put_contents(..., var_export($serialize,true).getTraceAsString()) → Execute o teste do plugin relevante → Identifique o primeiro controlador próprio no stack trace → Reverta a alteração no vendor.

8-3. Obsoletismo de Passar um Objeto como Array de Backing para ArrayObject

Passar um objeto (como stdClass de json_decode()) para new ArrayObject($obj) está obsoleto. Faça cast para array.

  • Correção: new ArrayObject(json_decode($body)->devices) → new ArrayObject((array)json_decode($body)->devices). Caso real no baserCMS: SitesControllerTest.

9. Outros (Apenas Quando Aplicável)

  • Constantes/Métodos Específicos de Driver PDO foram migrados para namespace (PDO::MYSQL_ATTR_* → Pdo\Mysql::ATTR_*, PDO::sqliteCreateFunction() → Pdo\Sqlite::createFunction(), etc.). A maioria está interna ao framework, mas se você usar constantes de driver PDO diretamente, faça a adaptação. O esquema DSN "uri:" do PDO também está obsoleto. ※ Classes PDO com namespace são 8.4+. Enquanto mantiver o 8.1, mantenha PDO::MYSQL_ATTR_*.
  • Os argumentos $key_length de openssl_pkey_derive() e $context de finfo_buffer() estão obsoletos (ignorados).
  • A configuração INI register_argc_argv em SAPI não-CLI está obsoleta (use $_GET / $_SERVER['QUERY_STRING']).
  • Alguns bindings de closures, como bind para closure estática, estão obsoletos.
  • Alteração na mensagem de erro de clone null (impacta testes): Até 8.4: __clone method called on non-object; 8.5: clone(): Argument #1 ($object) must be of type object, null given. Testes que verificam casos anômalos de clone de null com correspondência exata de string precisam atualizar o valor esperado (caso real no baserCMS: UploaderCategoriesControllerTest, testes test_copy nas versões Admin/Api).

⚠️ Para isolar erros em cadeia durante a execução completa de testes, execute novamente primeiro: Nesta base de código, ocasionalmente durante a execução completa, um teste anterior pode poluir o estado global (como configuração Configure do plugin), fazendo com que quase todos os testes subsequentes falhem em cadeia com erros como PluginCollection::create(): $config ... null given no bootstrap do Application. Se uma suíte de plugin individual (ex.: plugins/baser-core/tests/TestCase) passar e a reexecução estiver limpa, não é causado pelo código, mas sim por flaky test. Sempre verifique com isolamento e reexecução.


Como Proceder com a Investigação (Checklist)

  1. Identifique o arquivo e linha onde o aviso de obsoletismo ocorre usando php -l (verificação de sintaxe) ou logs/debug.log.
  2. Classifique em qual padrão da seção de versão correspondente ele se encaixa e corrija. Para itens substituíveis mecanicamente (como cast não padrão), use grep para detecção em massa.
  3. Verifique novamente com php -l para confirmar se o aviso desapareceu.
  4. Execute o teste relevante individualmente com --filter e, em seguida, execute todos os testes novamente para finalizar.

Relacionado: Para problemas originados do framework (valores de retorno de eventos, duplicação de Association, ResultSet, FormProtection, etc.), consulte o skill cakephp-migration.


Fontes

Habilidades Relacionadas