Communitygithub.com

php-migration

Guía de migración de versiones de PHP con recetas para corregir cambios obsoletos y disruptivos, enfocada en baserCMS.

¿Qué es php-migration?

Esta habilidad está diseñada para desarrolladores que mantienen aplicaciones PHP heredadas, especialmente baserCMS, y necesitan migrar entre versiones de PHP (8.2, 8.4, 8.5). Proporciona un catálogo detallado de cambios obsoletos y disruptivos por versión, junto con recetas de corrección paso a paso. Cubre desde propiedades dinámicas (PHP 8.2) hasta cambios críticos como nombres de clase reservados (Array, Object, etc.) que causan errores fatales en PHP 8.5, además de advertencias sobre compatibilidad con PHP 8.1. Se integra con flujos de trabajo de prueba unitaria (basercms-unittest) y ofrece métodos para detección mediante grep, Rector y análisis de registros de pruebas. Es compatible con Claude Code, Cursor y otros asistentes de IA para automatizar correcciones, y su enfoque práctico ayuda a priorizar cambios según el impacto y la necesidad de mantener compatibilidad hacia atrás.

Compatible con~Claude Code~Codex CLI~Cursor
npx skills add baserproject/baser-skills

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

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.log con debug: 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-unittest para 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(...).
  • Solución B: Para clases base que afectan a muchas clases hijas (ej: clase base de pruebas BcTestCase con 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 tienen tipo $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 usan fgetcsv, por lo que no están afectadas. Solo corregir los lugares que realmente llaman a fgetcsv().

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 null como 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 parameter en 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.json con "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, mantener PDO::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.php habí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íneas use correspondientes para resolver (los lugares referenciados ya estaban marcados con markTestIncomplete como 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 -l puede 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 $key puede 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.log para 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 de isset()/consulta. También es válido recibir valores null devueltos por getData('batch') con ?? ''.
  • Experiencia en baserCMS (total 308 casos): BcFileUploader (set/get de uploadingFiles[$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ágenes
  • xml_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() en Imageresizer.php y BcFileUploader.php. Se puede eliminar simplemente, pero para preservar la intención de liberación inmediata de memoria en bucles, reemplazar por unset($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/BcDatabaseService y 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. Usar grep -rn "setAccessible" (evitar inicio con -) o grep -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: serialize debe 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 de MailMessagesController.
  • 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+. Mantener PDO::MYSQL_ATTR_* mientras se soporte 8.1.
  • Los argumentos $key_length de openssl_pkey_derive() y $context de finfo_buffer() están obsoletos (se ignoran).
  • La directiva INI register_argc_argv en 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 es clone(): Argument #1 ($object) must be of type object, null given. Las pruebas que verifican casos excepcionales de clone con null mediante comparación exacta de cadenas deben actualizar el valor esperado (experiencia en baserCMS: pruebas test_copy de Admin/Api en UploaderCategoriesControllerTest).

⚠️ 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 given durante 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)

  1. Usar php -l (verificación de sintaxis) o logs/debug.log para identificar el archivo y la línea donde ocurre la advertencia de obsolescencia.
  2. 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.
  3. Verificar de nuevo con php -l (que la advertencia haya desaparecido).
  4. 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.


Referencias

Skills relacionados