Guía de migración de versiones de PHP (baserCMS)
Colección de recetas para cambios obsoletos y disruptivos encontrados al actualizar PHP. Organizado por versión. Los problemas originados en el framework CakePHP se tratan en la habilidad separada cakephp-migration.
Contenido: PHP 8.2 / 8.4 / 8.5. Al agregar soporte para una nueva versión, añadir una nueva sección "## PHP X.Y". Nota: PHP 8.5 es la versión menor inmediatamente anterior a 9.0. Todas las funciones obsoletas se eliminarán en 9.0, por lo que las advertencias de obsolescencia en 8.5 deben corregirse lo antes posible.
Principios generales
- Distinguir entre "errores fatales" y "advertencias de obsolescencia". En modo depuración, las advertencias de obsolescencia aparecen en la parte inferior de la pantalla y pueden parecer "errores", pero las que aparecen en
logs/debug.logcondebug:son advertencias (la ejecución continúa). Fatal/Exception son los verdaderos errores. - Separar regresiones de fallos preexistentes. Usa
git diff HEAD -- <file>para verificar si el archivo es parte de tus cambios. - Si cambias la firma de una clase padre, actualiza también las clases hijas e interfaces.
- Para la detección exhaustiva de todas las obsolescencias, es efectivo el análisis estático con Rector, etc.
Consulta la habilidad
basercms-unittestpara conocer métodos de ejecución de pruebas unitarias, verificación de sintaxis y aislamiento de fallos.php -l(verificación de sintaxis) también mostrará advertencias de obsolescencia como nullable implícito.
PHP 8.2
Propiedades dinámicas obsoletas
La asignación a propiedades no declaradas está obsoleta. Aparece Creation of dynamic property X::$prop is deprecated.
- Solución A (recomendada): Declarar la propiedad en la clase.
/** @var ThemeFoldersServiceInterface */ protected $ThemeFoldersService; protected $_templateTypes = [];- Nota: son típicos los lugares donde se asigna en el constructor como
$this->Xxx = $this->getService(...).
- Nota: son típicos los lugares donde se asigna en el constructor como
- Solución B: Para clases base que afectan a muchas clases hijas (ej: clase base de pruebas
BcTestCasecon más de 400 subclases), agregar#[ AllowDynamicProperties]. El atributo se hereda a las clases hijas, por lo que basta con un solo lugar.#[
AllowDynamicProperties] class BcTestCase extends TestCase { ... }
---
## PHP 8.4
### 1. Parámetros nullable implícitos obsoletos
`Tipo $x = null` es nullable implícito y está obsoleto. Usar explícitamente `?Tipo`.
```php
// Antes
function foo(int $contentId = null, BcAbstractDetector $agent = null)
// Después
function foo(?int $contentId = null, ?BcAbstractDetector $agent = null)
- Detección (expresión regular): Coincide con parámetros que no tienen
?al inicio y tienentipo $name = null. Ejemplo:([A-Za-z_\\][A-Za-z0-9_\\]*)\s+(\$[A-Za-z0-9_]+)\s*=\s*null(sin?inmediatamente antes). - Al cambiar la firma de una clase padre, actualizar también clases hijas/interfaces.
- Advertencia:
Implicitly marking parameter $x as nullable is deprecated, the explicit nullable type must be used instead.
2. Parámetro $escape de fgetcsv() obsoleto
Omitir el argumento $escape está obsoleto. Especificarlo explícitamente.
// Antes
$head = fgetcsv($fp, 10240);
// Después
$head = fgetcsv($fp, 10240, ',', '"', '\\');
- Las implementaciones personalizadas (funciones que analizan CSV con
fgets+regex, etc.) no usanfgetcsv, por lo que no están afectadas. Solo corregir los lugares que realmente llaman afgetcsv().
3. Llamada con un solo argumento a ReflectionProperty::setValue() obsoleta
Para propiedades no estáticas, pasar el objeto destino.
// Antes
$property->setValue($value);
// Después
$property->setValue($object, $value);
- Advertencia:
Calling ReflectionProperty::setValue() with a single argument is deprecated. - Suele esconderse en utilidades de prueba (helpers para generar solicitudes o manipular propiedades privadas).
- Para propiedades estáticas, pasar
nullcomo primer argumento:setValue(null, $value). Esto es válido en todas las versiones 8.1 a 8.5 (no rompe la compatibilidad con 8.1).
4. Pasar null al argumento string de funciones internas obsoleto (realmente desde 8.1+)
Pasar null a argumentos que no son ?string, como parse_url(null) / version_compare($a, null) / str_replace(..., null), produce Passing null to parameter #N ($x) of type string is deprecated. Convertir explícitamente con (string).
// Antes → Después
parse_url($url); → parse_url((string)$url);
version_compare($current, $required); → version_compare((string)$current, (string)$required);
- Esta obsolescencia se introdujo en 8.1, pero se manifiesta en gran cantidad durante la migración (especialmente al actualizar a 8.4/8.5), por lo que se incluye aquí.
- Solo aparece en tiempo de ejecución cuando el valor es null, por lo que no se puede detectar completamente con grep estático. Recopilar todas las apariciones de
Passing null to parameteren los registros de pruebas para identificar el origen.
PHP 8.5 (lanzamiento 2025-11-20)
Última versión menor antes de 9.0. La mayoría de los siguientes son advertencias y la ejecución continúa, pero se eliminarán en 9.0, por lo que hay que corregirlos. Se ordenan por impacto en la aplicación/framework. ※ Solo "0." es un error fatal (cambio disruptivo), no una advertencia, por lo que tiene máxima prioridad.
⚠️ Nota sobre mantener compatibilidad con PHP 8.1 (
composer.jsoncon"php": ">=8.1"). Al corregir obsolescencias de 8.5 con el "reemplazo recomendado", algunos reemplazos pueden no existir en 8.1 y provocar un error fatal en 8.1. Los elementos marcados con ❌ se posponen hasta que se elimine el soporte de 8.1 (se permiten las advertencias en 8.5). Ejecutar en 8.5 no rompe 8.1 (solo advertencias).
- ❌
str_increment()(8.3+) → en 8.1, mantener$s++- ❌
http_get_last_response_headers()(8.4+) → en 8.1, mantener$http_response_header- ❌ PDO con espacios de nombres (
Pdo\Mysql::ATTR_*, etc., 8.4+) → en 8.1, mantenerPDO::MYSQL_ATTR_*Los demás elementos (cast no estándar, offset
null→'', eliminación de funciones de liberación automática, backtick →shell_exec,case ;,__serialize) se pueden aplicar de forma segura incluso en 8.1.
0. [Cambio disruptivo, fatal] Nombres de clase reservados (ej: Array) referenciados con use causan error fatal
En 8.5, nombres como Array se reservan como nombres de clase especiales, y use ...\Array; o class Array producen un error fatal (no es una advertencia). Si ocurre durante la recopilación de pruebas (autocarga), toda la suite puede quedar inutilizable, por lo que tiene máxima prioridad.
Fatal error: Cannot use BcMail\Test\TestCase\Model\Array as Array because 'Array' is a special class name
- Experiencia en baserCMS: En
MailMessagesTableTest.phphabía restos de la era CakePHP2:use ...\Array;/use ...\ClassRegistry;(ambas importaciones muertas de clases inexistentes). Eran inofensivas hasta 8.4, pero en 8.5 causan error fatal. Eliminar las líneasusecorrespondientes para resolver (los lugares referenciados ya estaban marcados conmarkTestIncompletecomo código no ejecutado). - Detección:
grep -rnE "use [A-Za-z0-9_\\\\]+\\(Array|Object|Resource|Enum|Mixed|Never|Void|Null|False|True|Iterable|Numeric)\s*;" --include="*.php". - Nota: Es fatal, por lo que
php -lpuede detectarlo en archivos individuales. Pero limpiar con grep antes de todas las pruebas evita que la suite falle en la fase de recopilación.
1. Casts no estándar obsoletos (alto impacto)
(boolean) (integer) (double) (binary) están obsoletos. Reemplazar por los nombres estándar.
// Antes → Después
(boolean) → (bool)
(integer) → (int)
(double) → (float)
(binary) → (string)
- Detección:
grep -rnE "\((boolean|integer|double|binary)\)" --include="*.php". - Suelen quedar en código antiguo. Se pueden reemplazar mecánicamente.
2. Usar null como offset de array obsoleto
$arr[null] / array_key_exists(null, $arr) está obsoleto (se convertía implícitamente a ""). Usar explícitamente cadena vacía "".
// Antes
$arr[null] = $v; array_key_exists(null, $arr);
// Después
$arr[''] = $v; array_key_exists('', $arr);
- Tener cuidado con lugares donde una variable es la clave (
$arr[$key]y$keypuede ser null). - Difícil de detectar con grep estático (solo aparece en tiempo de ejecución cuando el valor es null). Ejecutar todas las pruebas en 8.5 y usar
grep "null as an array offset" phpunit.logpara recopilar los orígenes (on line N of <file>). - Estrategia de corrección: ① Usar
$key ?? ''para la clave de asignación/consulta (equivalente a la conversión implícita anterior; asegurar que tanto set como get usen la misma clave), ② Agregar guarda$key !== null &&antes deisset()/consulta. También es válido recibir valores null devueltos porgetData('batch')con?? ''. - Experiencia en baserCMS (total 308 casos):
BcFileUploader(set/get deuploadingFiles[$bcUploadId]con?? ''para coherencia),BcUploadBehavior(oldEntity[$alias][$entity->_bc_upload_id]con?? ''),MailMessagesTable($dists[$mailField->group_valid]),SearchIndexes/Contents/PermissionsController(getData('batch') ?? ''),BcTextHelper::arrayValue/BcUtil::decodeContent($key !== null && isset(...)).
3. Funciones de liberación de recursos para objetos obsoletas
Los objetos se liberan automáticamente por el GC, por lo que las llamadas explícitas a close/free están obsoletas. Simplemente eliminar la llamada.
curl_close()/curl_share_close()(CurlHandle / CurlShareHandle)finfo_close()(finfo)imagedestroy()(GdImage) ← común en utilidades de procesamiento de imágenesxml_parser_free()(XMLParser)- Detección:
grep -rnE "\b(curl_close|curl_share_close|finfo_close|imagedestroy|xml_parser_free)\s*\(" --include="*.php". - Experiencia en baserCMS: 4 llamadas a
imagedestroy()enImageresizer.phpyBcFileUploader.php. Se puede eliminar simplemente, pero para preservar la intención de liberación inmediata de memoria en bucles, reemplazar porunset($img)(GdImage es un objeto, al eliminar la referencia el GC lo libera. Seguro en >=8.1).
3-2. ReflectionProperty/Method::setAccessible() obsoleto (mayor número de ocurrencias)
setAccessible() es no-op desde PHP 8.1 (se puede acceder directamente a private/protected con getValue/setValue/invoke). Obsoleto en 8.5. Eliminar la línea de llamada (seguro en >=8.1).
// Antes
$property = new ReflectionProperty($obj, 'foo');
$property->setAccessible(true); // ← Eliminar esta línea
$property->setValue($obj, $v);
- Experiencia en baserCMS: 33 lugares entre src y pruebas (
BcUtil/BcTestCase/BaserCorePlugin/PreviewController/BcDatabaseServicey otros). Al estar en rutas críticas (bases de pruebas, etc.), la cantidad de ocurrencias en tiempo de ejecución es desproporcionadamente alta (aproximadamente 6,300 en todas las pruebas ≈ la mayoría de las obsolescencias de 8.5). Corregir esto primero reduce drásticamente los registros. - Eliminación masiva (limitado a sentencias individuales):
sed -i -E "/^[[:space:]]*\$[A-Za-z0-9_]+->setAccessible\(true\);[[:space:]]*$/d" <archivo>. - Nota de detección: Si el patrón de grep comienza con
-(ej:->setAccessible), grep puede interpretarlo como opción y devolver 0 resultados. Usargrep -rn "setAccessible"(evitar inicio con-) ogrep -e.
4. Incremento de cadenas no numéricas obsoleto
$str++ (cadena no numérica) está obsoleto. Usar str_increment(). ※ str_increment() es 8.3+. Mantener $str++ mientras se soporte 8.1.
// Antes
$code++; // $code es 'AZ', etc.
// Después
$code = str_increment($code);
5. __sleep() / __wakeup() suavemente obsoletos
Usar __serialize() / __unserialize() (si no se necesita compatibilidad con PHP 7).
6. $http_response_header obsoleto
La variable superglobal $http_response_header está obsoleta. Usar http_get_last_response_headers(). ※ http_get_last_response_headers() es 8.4+. Mantener $http_response_header mientras se soporte 8.1.
7. Operador backtick obsoleto
`comando` (alias de shell_exec()) está obsoleto. Usar shell_exec() directamente.
8. Terminar una sentencia case con ; obsoleto
case 1; está obsoleto. Usar case 1:.
- Detección:
grep -rnE "case\s+.+;\s*$" --include="*.php"(puede haber falsos positivos, verificar visualmente).
8-2. array_key_exists(null, ...) obsoleto (inducido por formato incorrecto de serialización)
El primer argumento null en array_key_exists() está obsoleto (usar cadena vacía). Si aparece dentro del framework (CakePHP JsonView::_dataToSerialize), la causa es un formato incorrecto de la opción serialize en código propio. Al mezclar 'clave' => valor en la lista de nombres de variables, como setOption('serialize', ['foo', 'message' => $message]), cuando $message es null, el array serialize contiene 'message' => null, lo que provoca array_key_exists(null, ...).
- Corrección:
serializedebe ser una lista de nombres de variables de la vista. Cambiar['foo', 'message' => $message]→['foo', 'message'](los valores ya están establecidos con$this->set()). Experiencia en baserCMS: acciones index/view deMailMessagesController. - Método para identificar el origen cuando ocurre dentro del framework: Instrumentar temporalmente el vendor (
if ($key === null) file_put_contents(..., var_export($serialize,true).getTraceAsString())) → ejecutar las pruebas del plugin correspondiente → identificar el controlador propio al inicio del stack trace → restaurar el vendor.
8-3. Pasar un objeto como backing array a ArrayObject obsoleto
Pasar un objeto (stdClass de json_decode(), etc.) a new ArrayObject($obj) está obsoleto. Convertir a array.
- Corrección:
new ArrayObject(json_decode($body)->devices)→new ArrayObject((array)json_decode($body)->devices). Experiencia en baserCMS:SitesControllerTest.
9. Otros (solo si aplican)
- Constantes/métodos específicos del driver PDO migrados a espacios de nombres (
PDO::MYSQL_ATTR_*→Pdo\Mysql::ATTR_*,PDO::sqliteCreateFunction()→Pdo\Sqlite::createFunction(), etc.). La mayoría son internos del framework, pero si se usan constantes de driver PDO directamente, hay que actualizar. El esquema DSN"uri:"de PDO también está obsoleto. ※ Las clases PDO con espacios de nombres son 8.4+. MantenerPDO::MYSQL_ATTR_*mientras se soporte 8.1. - Los argumentos
$key_lengthdeopenssl_pkey_derive()y$contextdefinfo_buffer()están obsoletos (se ignoran). - La directiva INI
register_argc_argven SAPI que no sea CLI está obsoleta (usar$_GET/$_SERVER['QUERY_STRING']). - Algunos bindings de clausuras estáticas están obsoletos.
- Cambio de mensaje de error para
clone null(afecta a pruebas): Hasta 8.4 era__clone method called on non-object; en 8.5 esclone(): Argument #1 ($object) must be of type object, null given. Las pruebas que verifican casos excepcionales declonecon null mediante comparación exacta de cadenas deben actualizar el valor esperado (experiencia en baserCMS: pruebastest_copyde Admin/Api enUploaderCategoriesControllerTest).
⚠️ Aislar errores en cadena durante la ejecución completa de pruebas: En esta base de código, ocasionalmente ocurre una contaminación transitoria durante la ejecución completa: una prueba anterior contamina el estado global (por ejemplo, configuración de plugins Configure), lo que provoca que casi todas las pruebas posteriores fallen en cadena con errores como
PluginCollection::create(): $config ... null givendurante el bootstrap de la aplicación. Si la suite individual de un plugin (ej:plugins/baser-core/tests/TestCase) pasa y una re-ejecución limpia el error, no es culpa del código, sino un test inestable (flaky). Siempre verificar con aislamiento y re-ejecución.
Cómo proceder con la investigación (lista de verificación)
- Usar
php -l(verificación de sintaxis) ologs/debug.logpara identificar el archivo y la línea donde ocurre la advertencia de obsolescencia. - Clasificar según el patrón de la sección de versión correspondiente y corregir. Los que se pueden reemplazar mecánicamente (cast no estándar, etc.) se pueden detectar masivamente con grep.
- Verificar de nuevo con
php -l(que la advertencia haya desaparecido). - Confirmar la prueba correspondiente de forma individual con
--filter→ finalizar con una re-ejecución completa de todas las pruebas.
Relacionado: Los problemas originados en el framework (valores de retorno de eventos, duplicación de Association, ResultSet, FormProtection, etc.) se tratan en la habilidad cakephp-migration.