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.logcomdebug: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-unittestpara saber como executar testes de unidade, verificação de sintaxe e isolamento de falhas. Ophp -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.
※ Locais típicos são atribuições no construtor como/** @var ThemeFoldersServiceInterface */ protected $ThemeFoldersService; protected $_templateTypes = [];$this->Xxx = $this->getService(...). - Solução B: Em classes base que afetam muitas classes filhas (ex.: classe de teste base
BcTestCasecom 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ãoTipo $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 usamfgetcsv, portanto não são afetadas. Corrija apenas os locais que realmente chamamfgetcsv().
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
nullcomo 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 dePassing null to parameternos 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.jsoncom"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_headerpara 8.1- ❌ PDO com namespace (
Pdo\Mysql::ATTR_*etc., 8.4+) → MantenhaPDO::MYSQL_ATTR_*para 8.1Todos 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 linhausecorrespondente para resolver (as referências são para código não executado, marcado commarkTestIncomplete). - 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 -lem 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$keypode sernull). - 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 usegrep "null as an array offset" phpunit.logpara 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 deisset()/consulta. Receber retornos null degetData('batch')com?? ''também é eficaz. - Caso real no baserCMS (308 ocorrências no total):
BcFileUploader(alinhamento set/get deuploadingFiles[$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 imagemxml_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()emImageresizer.phpeBcFileUploader.php. A simples remoção é aceitável, mas para manter a intenção de liberação imediata de memória em loops, substitua porunset($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/BcDatabaseServiceetc.). 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. Usegrep -rn "setAccessible"(evite começar com-) ou usegrep -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
serializedeve 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 deMailMessagesController. - 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, mantenhaPDO::MYSQL_ATTR_*. - Os argumentos
$key_lengthdeopenssl_pkey_derive()e$contextdefinfo_buffer()estão obsoletos (ignorados). - A configuração INI
register_argc_argvem 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 declonede null com correspondência exata de string precisam atualizar o valor esperado (caso real no baserCMS:UploaderCategoriesControllerTest, testestest_copynas 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 givenno 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)
- Identifique o arquivo e linha onde o aviso de obsoletismo ocorre usando
php -l(verificação de sintaxe) oulogs/debug.log. - 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.
- Verifique novamente com
php -lpara confirmar se o aviso desapareceu. - Execute o teste relevante individualmente com
--filtere, 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.