ARQUITECTURA DE SOFTWARE · PHP · LEGACY · DEUDA TÉCNICA
PHP no es el problema. El problema es todo lo que hemos dejado de actualizar
Por qué migrar PHP 5.6 a PHP 8 no consiste en cambiar una versión, sino en recuperar el control de una aplicación que sigue siendo importante para el negocio.
Hay una frase que he escuchado muchas veces alrededor de aplicaciones empresariales: «No lo toques, que funciona».
Suele aparecer cuando alguien propone actualizar PHP, revisar un framework antiguo o retirar una dependencia que lleva años sin mantenimiento. Y tiene una explicación comprensible: si un sistema factura, gestiona almacenes o permite trabajar cada día a decenas de personas, nadie quiere ser quien provoque una interrupción.
Pero esa frase esconde una pregunta más incómoda: ¿funciona porque está bien mantenido o porque todavía no hemos necesitado cambiar nada importante?
Cuando nos planteamos migrar PHP 5.6 a PHP 8, rara vez encontramos solo un problema de sintaxis. Encontramos aplicaciones útiles, cargadas de conocimiento del negocio, que han ido perdiendo capacidad de evolucionar. Siguen en producción, pero dependen de un runtime sin soporte, de un framework antiguo, de extensiones eliminadas o de servidores cuya configuración nadie se atreve a modificar.
El objetivo no debería ser reescribirlas por orgullo tecnológico. Tampoco mantenerlas congeladas por miedo.
El objetivo es recuperar la capacidad de cambiarlas con seguridad.
Respuesta directa: para migrar PHP 5.6 a PHP 8 con garantías no basta con cambiar el intérprete. Primero hay que inventariar dependencias, fijar el comportamiento crítico con pruebas, reproducir el entorno, reducir los bloqueos de compatibilidad y desplegar por incrementos verificables.
TL;DR
Migrar PHP 5.6 a PHP 8 es una intervención sobre un sistema completo, no una sustitución de versión aislada. El camino más seguro suele empezar por un inventario funcional y técnico, seguir con pruebas de caracterización y contención operativa, y terminar con una migración incremental hacia versiones soportadas. Docker ayuda a reproducir el legado; no lo vuelve seguro por sí solo. La métrica decisiva es recuperar la capacidad de evolucionar sin poner el negocio en riesgo.
«Funciona» no es lo mismo que «podemos mantenerlo»

Imaginemos un ERP desarrollado hace más de diez años. La aplicación responde. Los usuarios conocen las pantallas. Las incidencias se resuelven con pequeñas modificaciones. El negocio depende de ella y cambiarla parece arriesgado.
Entonces llega una petición aparentemente sencilla: conectar un proveedor nuevo, actualizar el servidor, incorporar autenticación moderna o enviar datos a una API externa.
La tarea funcional quizá necesite unas pocas horas. Sin embargo, el equipo descubre que el cliente HTTP utiliza bibliotecas antiguas, que el entorno no negocia correctamente las conexiones actuales, que Composer no puede resolver dependencias o que la versión del framework condiciona cualquier cambio.
De repente, una mejora pequeña exige investigar una década de decisiones técnicas.
Ahí está el coste menos visible del legado: el tiempo que perdemos preparando el terreno antes de poder desarrollar la funcionalidad que realmente importa.
Hay deuda técnica razonable. Todos los equipos asumen compromisos para entregar valor. El problema aparece cuando esa deuda deja de estar identificada y se convierte en una condición permanente: cada nueva integración cuesta más, cada despliegue requiere más precauciones y cada salida de una persona del equipo aumenta la incertidumbre.
No se trata de contar líneas de código antiguas. Se trata de responder algo mucho más concreto: ¿cuánto esfuerzo nos cuesta introducir un cambio sin poner en peligro lo que ya funciona?
PHP 5.6 no vive solo: envejece todo el ecosistema

PHP 5.6 terminó su soporte oficial el 31 de diciembre de 2018. PHP 7.4 llegó al final de su vida útil el 28 de noviembre de 2022. Estar fuera de soporte no significa que una aplicación deje de ejecutarse al día siguiente: significa que el proyecto ya no proporciona correcciones oficiales para esa rama.
A fecha del 25 de octubre de 2026, PHP 8.2 recibe únicamente correcciones de seguridad hasta diciembre de 2026; PHP 8.3, hasta diciembre de 2027. PHP 8.4 dispone de soporte activo hasta diciembre de 2026 y de seguridad hasta diciembre de 2028; PHP 8.5, de soporte activo hasta diciembre de 2027 y de seguridad hasta diciembre de 2029. Por eso, al definir una nueva plataforma de producción, evaluaría primero PHP 8.4 o PHP 8.5, según la compatibilidad probada y la vida útil prevista.
Los calendarios oficiales pueden consultarse en versiones PHP soportadas y ramas PHP sin soporte.
Una aplicación no ejecuta PHP en el vacío. Ejecuta una combinación de componentes:
- Runtime: PHP, extensiones y restricciones de plataforma.
- Framework: Symfony, Laravel o una estructura propia de controladores y servicios.
- Dependencias: paquetes Composer, bibliotecas internas y código que se incorporó hace años.
- Infraestructura: sistema operativo, OpenSSL, servidor web, PHP-FPM, bases de datos y tareas programadas.
- Integraciones: correo, certificados, APIs, impresoras, lectores y formatos de intercambio.
- Reglas de negocio: cálculos, permisos, validaciones y excepciones que pueden no estar documentadas en ningún otro lugar.
Pensemos en Symfony 2.8: su soporte de seguridad finalizó en noviembre de 2019, según el calendario oficial de Symfony. Una aplicación en Symfony 2.0 puede tener todavía más obstáculos: Doctrine, Twig, bundles internos, autenticación y contratos de servicios pertenecen a un ecosistema de otra época.
Que PHP arranque no garantiza que todo eso sea compatible con un runtime moderno. Por eso migrar PHP 5.6 a PHP 8 no empieza editando una etiqueta de Docker: empieza reconstruyendo el mapa de dependencias que sostiene el producto.
Las incompatibilidades revelan supuestos que nadie documentó
Tres compatibilidades distintas
Que PHP ejecute el código no demuestra que se mantengan los resultados del negocio ni que el sistema pueda desplegarse, observarse y recuperarse con garantías.

En una aplicación moderna damos por hechas muchas cosas: autoloading, dependencias reproducibles, APIs relativamente estables, pruebas automáticas y entornos similares entre desarrollo y producción. En un proyecto antiguo, cualquiera de esas condiciones puede faltar.
La antigua familia de funciones mysql_* desapareció en PHP 7. En PHP 8 se eliminaron, entre otras, each() y create_function(). También cambiaron comparaciones no estrictas, firmas de métodos, tratamientos de errores y determinadas conversiones de tipos, como recoge la guía oficial de incompatibilidades de PHP 8.
Un fragmento heredado puede recorrer una configuración así:
// Código heredado: each() ya no existe en PHP 8.
while (list($clave, $valor) = each($configuracion)) {
registrarOpcion($clave, $valor);
}Si el código no depende de la posición interna del array, una sustitución habitual sería:
foreach ($configuracion as $clave => $valor) {
registrarOpcion($clave, $valor);
}La salvedad importa: each() avanzaba el puntero interno del array; foreach no es intercambiable en todos los programas si otros fragmentos dependen de ese estado. La migración exige pruebas, no sustituciones automáticas a ciegas.
Y muchas incompatibilidades no son sintácticas. Una comparación que antes aceptaba una conversión implícita puede devolver otro resultado. Un cálculo monetario puede verse afectado por los tipos. Una conexión TLS puede depender de certificados o clientes obsoletos. Una librería puede haber sido abandonada sin sustituto directo.
Conviene distinguir tres objetivos que a menudo confundimos:
| Dimensión | Qué debemos demostrar | Evidencia necesaria |
|---|---|---|
| Ejecución | El código carga y se ejecuta en PHP de destino | Pruebas de arranque y comandos relevantes |
| Funcional | Pedidos, facturas, inventario y permisos mantienen resultados válidos | Pruebas de caracterización y regresión aprobadas |
| Operativa | Podemos desplegar, observar y recuperar el servicio | Despliegue controlado y restauración verificada |
Superar el primero no significa haber conseguido los otros dos. A mi juicio, esta es una de las decisiones más importantes al migrar PHP 5.6 a PHP 8: definir qué significa exactamente «la migración está terminada».
El error de empezar cambiando el framework

Es tentador abrir composer.json, subir restricciones y empezar a resolver errores. En proyectos pequeños puede funcionar. En una aplicación empresarial muy antigua puede ser una forma rápida de perder el control del alcance.
Antes de actualizar Symfony necesitamos saber qué tenemos. No solo los paquetes declarados, sino las versiones bloqueadas, las extensiones realmente utilizadas, las integraciones existentes y los procesos de negocio críticos.
Yo haría un inventario en cuatro capas:
- Producto. Módulos utilizados, procesos de facturación, almacén, informes y funcionalidades que llevan años sin uso.
- Aplicación. Versiones, bundles, puntos de entrada, comandos, cron, librerías personalizadas y configuración.
- Datos e integraciones. Esquemas, codificaciones, sistemas satélite, APIs, intercambio de archivos y efectos secundarios.
- Operación. Copias de seguridad restaurables, secretos, despliegues, registros, monitorización, accesos y recuperación.
En un entorno de análisis controlado, y con una versión de Composer compatible con el proyecto, estas comprobaciones ofrecen una primera fotografía:
php -v
php -m
composer show --locked
composer check-platform-reqs --lock
composer audit --lockedcomposer show --locked consulta el fichero de bloqueo. check-platform-reqs --lock contrasta los requisitos de plataforma declarados con el entorno donde se ejecuta. audit --locked identifica avisos conocidos según las fuentes configuradas. Ninguno demuestra por sí solo que el producto funcione o sea seguro.
El proyecto puede no tener composer.lock, utilizar una versión antigua de Composer o necesitar ejecutables de auditoría separados del runtime heredado. Tampoco conviene instalar herramientas actuales directamente en producción para investigar un sistema frágil.
El resultado que busco no es una lista interminable de advertencias. Es una matriz de componentes, riesgos, bloqueos y dependencias que permita decidir por dónde empezar y qué no debemos tocar todavía.
Antes de cambiar el código hay que capturar su comportamiento

En muchos sistemas heredados apenas existen tests, pero sí reglas de negocio que llevan años ejecutándose. Proponer un porcentaje arbitrario de cobertura antes de empezar suena bien y puede bloquear la migración durante meses.
Prefiero comenzar por pruebas de caracterización: documentar el comportamiento existente para detectar cambios no deseados. No equivale a declarar correcto todo lo que hace hoy el programa. Algunas respuestas actuales son defectos conocidos; congelarlas sin criterio sería convertir un error en una especificación.
Si modernizamos un ERP, empezaría por escenarios que realmente importan:
- Crear un pedido y contrastar totales, descuentos e impuestos.
- Convertir un albarán en factura sin perder trazabilidad.
- Registrar movimientos de almacén y comprobar existencias.
- Reproducir un informe financiero con un resultado aprobado.
- Verificar roles, sesiones y autorizaciones en operaciones sensibles.
- Ejecutar procesos programados y comprobar tanto el resultado como sus efectos secundarios.
Cada escenario debe tener entradas controladas, salida esperada, datos ficticios o anonimizados, y una validación de negocio. Separaría «comportamiento aceptado» de «incidencia conocida» y asignaría responsables para resolver las discrepancias.
No todo tiene que implementarse con PHPUnit desde el primer día. Podemos comparar documentos, probar endpoints, validar consultas o recorrer flujos completos en un entorno aislado. Lo imprescindible es contar con una referencia verificable.
Para migrar PHP 5.6 a PHP 8 sin romper el negocio, las pruebas de caracterización son una inversión más útil que una falsa sensación de seguridad basada únicamente en que la página de inicio carga.
Dockerizar el legado ayuda, pero no lo moderniza
No confundas contención con modernización
Meter PHP 5.6 en una imagen de Docker ayuda a reproducir y aislar un entorno antiguo. No reanuda su soporte ni corrige por sí solo las vulnerabilidades del runtime y sus dependencias.

Docker puede ser decisivo para reproducir un sistema viejo: aislar dependencias, levantar bases de datos de prueba, automatizar el arranque y reducir diferencias entre equipos. Permite además mantener temporalmente un runtime antiguo en un laboratorio mientras avanzamos hacia su sustitución.
Pero una imagen con PHP 5.6 sigue ejecutando PHP 5.6. El contenedor no corrige una vulnerabilidad de la aplicación, una librería abandonada ni un protocolo obsoleto. Cambia cómo distribuimos y aislamos el software; no modifica su soporte oficial.
Por eso diferenciaría dos hitos:
- Contención: aplicación aislada, exposición limitada, entorno reproducible y procedimientos de copia y restauración probados.
- Modernización: dependencias renovadas, comportamiento validado, runtime con soporte y despliegue sostenible.
El primero reduce incertidumbre y facilita el segundo, pero no debería presentarse a dirección como si fueran lo mismo.
Hay, además, límites materiales: binarios antiguos, drivers, extensiones nativas o servicios externos no siempre son reproducibles. Algunas migraciones requieren sustituir antes un componente bloqueante. En entornos especialmente antiguos incluso el sistema operativo puede imponer restricciones al cliente TLS o a los certificados.
Docker es una herramienta para ganar control, no una exención de mantenimiento.
¿Actualizar, reescribir o extraer funcionalidades progresivamente?
No existe una única estrategia válida para todas las aplicaciones PHP. La elección depende del acoplamiento, la calidad del código, la criticidad, el estado de las pruebas y la vida útil esperada del producto.
Actualización incremental
Funciona cuando hay una ruta razonable de compatibilidad y el comportamiento está suficientemente caracterizado. Conservamos procesos y conocimiento, aunque quizá necesitemos varias etapas intermedias de actualización.
Reescritura
Puede justificarse si el producto cambió radicalmente, el dominio ya no se representa bien o los costes de mantener la base antigua son desproporcionados. El riesgo es subestimar las reglas que viven escondidas en informes, validaciones y casos excepcionales.
Extracción progresiva
Ofrece un camino intermedio. Podemos publicar una API, sustituir un módulo o incorporar una interfaz nueva mientras convive con el núcleo heredado. Es la idea del patrón Strangler Fig: sustituir partes del sistema sin detener el conjunto.
No significa adoptar microservicios por moda. Si el equipo no puede operar transacciones distribuidas, nuevas colas y despliegues independientes, la modularidad dentro de una aplicación puede ser mucho más razonable.
Antes de decidir, haría tres preguntas: ¿qué podemos validar?, ¿qué podemos desacoplar? y ¿qué alternativa reduce el riesgo operativo durante los próximos años?
La mejor arquitectura de destino no es la que utiliza más tecnologías recientes, sino la que el equipo podrá mantener cuando termine el proyecto de migración.
Cómo migrar PHP 5.6 a PHP 8 con una hoja de ruta verificable
El criterio de avance no es el calendario
Una fase está terminada cuando sus riesgos críticos se han comprobado con evidencias. Un despliegue exitoso sin verificación de datos, integraciones y recuperación no basta.

Prefiero planes con criterios de salida a promesas como «en tres meses estará todo migrado». El calendario depende de la aplicación; los avances deben depender de evidencias.
Fase 0. Estabilizar y proteger
Inventariar accesos y secretos, revisar la exposición de los servicios, verificar las copias de seguridad y realizar una restauración de prueba. Documentar los riesgos que exijan mitigaciones inmediatas.
Criterio de salida: podemos recuperar el servicio y trabajar sobre una copia controlada sin improvisar.
Fase 1. Reproducir
Capturar versiones, configuración y extensiones. Levantar un entorno aislado que reproduzca los flujos esenciales, con datos ficticios o anonimizados y sin publicar innecesariamente un runtime fuera de soporte.
Criterio de salida: otra persona puede levantar y comprobar la aplicación siguiendo documentación, sin depender de quien configuró el servidor original.
Fase 2. Caracterizar
Seleccionar escenarios críticos, generar pruebas y resultados de referencia, y recoger registros útiles para investigar fallos. Validar con negocio las diferencias entre resultados aceptados y errores conocidos.
Criterio de salida: podemos detectar regresiones en los procesos esenciales.
Fase 3. Desbloquear dependencias
Sustituir funciones eliminadas, bibliotecas abandonadas, componentes incompatibles y acoplamientos innecesarios. Herramientas como Rector y PHPStan pueden ayudar desde un entorno compatible, con revisión humana y pruebas de regresión.
Criterio de salida: existe una ruta probada hacia una versión soportada de PHP y del framework, o una decisión documentada para separar los módulos que impiden avanzar.
Fase 4. Migrar por incrementos
Separar, cuando sea posible, cambios de runtime, librerías, framework y lógica funcional. Probar cada avance en preproducción. Preparar despliegue controlado, plan de reversión y compatibilidad del esquema de datos: volver al código anterior no siempre revierte cambios sobre la base de datos.
Las versiones intermedias sin soporte pueden servir como escalones exclusivamente en laboratorios controlados, nunca como destino final de producción.
Criterio de salida: el sistema funciona en una rama soportada, supera las pruebas críticas y dispone de un procedimiento de despliegue y recuperación ensayado.
Fase 5. Mantener el resultado
Completar CI/CD, observabilidad, alertas, documentación de decisiones, responsables por módulo y un calendario de actualizaciones periódicas. Revisar dependencias antes de que el nuevo stack envejezca del mismo modo.
Criterio de salida: actualizar deja de ser una operación excepcional que paraliza el desarrollo de producto.
No intentaría ejecutar todas estas fases sobre todo el ERP a la vez. Empezaría por un flujo representativo, aprendería de las restricciones reales y ampliaría el alcance por bloques funcionales.
Consejos prácticos antes de migrar PHP 5.6 a PHP 8
Antes de comenzar, reduciría la decisión a ocho recomendaciones: inventariar, aislar producción, caracterizar el comportamiento, revisar extensiones, reproducir entornos, avanzar por fases, observar los resultados y ensayar la reversión. Los escalones intermedios sin soporte solo deberían utilizarse en laboratorios controlados.

- Inventariar versiones de PHP, framework, extensiones, integraciones y flujos críticos.
- Aislar el entorno heredado y mantener las pruebas fuera de producción.
- Crear pruebas de caracterización antes de modificar las reglas del negocio.
- Identificar dependencias abandonadas y sustituir APIs incompatibles.
- Reproducir el entorno de forma documentada y verificable.
- Migrar por incrementos con criterios de salida y pruebas de regresión.
- Medir errores, rendimiento, integraciones y capacidad de recuperación.
- Ensayar la reversión, especialmente ante cambios de esquema y datos.
Lo que mediría como CTO no es el porcentaje de código reescrito
Es sencillo presentar una modernización enseñando que hemos eliminado miles de líneas o sustituido un framework. Son indicadores visibles, pero no prueban que la organización esté mejor.
Me interesan señales más vinculadas a la capacidad de operar:
- Tiempo de cambio: desde una petición aprobada hasta su despliegue seguro.
- Riesgo de dependencia: componentes críticos fuera de soporte y tiempo necesario para sustituirlos.
- Confianza en las entregas: procesos esenciales con comprobaciones reproducibles.
- Capacidad de recuperación: tiempo real de restauración, medido mediante ensayos.
- Concentración de conocimiento: operaciones que dependen de una única persona.
- Coste de evolución: proporción de esfuerzo que consume resolver restricciones antiguas frente al desarrollo de valor nuevo.
No hace falta construir un cuadro de mando con cuarenta métricas. Hay que comprobar si la aplicación recupera una cualidad esencial: poder cambiar sin convertir cada modificación en una expedición arqueológica.
Modernizar requiere también una conversación honesta con dirección. No se puede prometer riesgo cero, pero sí explicar el riesgo actual, el coste de no actuar, las alternativas y los criterios que decidirán dónde seguir invirtiendo.
El error es presentar este trabajo como limpieza estética que compite con las funcionalidades de negocio. Una aplicación que no se puede actualizar termina condicionando la seguridad, el ritmo de entrega y la continuidad de esas mismas funcionalidades.
La decisión difícil no es técnica

Después de años trabajando con aplicaciones empresariales, cada vez me convence menos la división entre «software viejo» y «software nuevo».
Hay código antiguo excelente que expresa muy bien el dominio y lleva años resolviendo problemas reales. También hay productos recientes con una arquitectura difícil de mantener desde su primer despliegue.
No plantearía migrar PHP 5.6 a PHP 8 como una guerra contra PHP, Symfony o quienes tomaron decisiones hace una década. Muchas tenían sentido con los presupuestos, herramientas y necesidades de aquel momento.
Lo importante es reconocer que el contexto cambió. Hoy necesitamos integrar servicios, proteger datos, desplegar con frecuencia y seguir incorporando personas al equipo.
Para hacerlo, no basta con que una aplicación arranque. Necesitamos comprender sus dependencias, verificar su comportamiento y disponer de un camino de actualización.
PHP no es el problema. El problema aparece cuando convertimos una decisión técnica de hace diez años en una restricción que nadie puede revisar.
Y modernizar no siempre significa empezar de cero. A menudo significa algo menos espectacular y bastante más útil: recuperar el control, paso a paso, sobre un sistema que todavía tiene mucho valor.
Preguntas frecuentes sobre migrar PHP 5.6 a PHP 8
¿Merece la pena migrar PHP 5.6 a PHP 8 si la aplicación todavía funciona?
Sí, si el software sigue siendo necesario. PHP 5.6 lleva años fuera de soporte oficial; que una aplicación responda no demuestra que sea sostenible. La estrategia debe calibrarse según su exposición, criticidad, integraciones y coste de evolución.
¿Puedo ejecutar Symfony 2 con PHP 8 sin cambiar el framework?
No debe darse por supuesto. La viabilidad depende de la versión exacta del framework, las bibliotecas y el código propio. En muchos proyectos será necesario evolucionar Symfony por etapas o extraer módulos. La compatibilidad se comprueba, no se presupone.
¿Es siempre más barato reescribir una aplicación heredada?
No. Una reescritura obliga a recuperar reglas de negocio, convertir datos, reproducir integraciones y validar procesos. Debe compararse con una actualización incremental y una extracción progresiva, incluyendo el riesgo y el coste total.
¿Docker elimina los riesgos de seguridad de PHP 5.6?
No. Docker mejora aislamiento y reproducibilidad, pero no proporciona parches al runtime antiguo ni corrige automáticamente dependencias vulnerables. Es una medida de contención útil, no la meta de modernización.
¿Qué versión de PHP 8 elegiría como destino en octubre de 2026?
Evaluaría PHP 8.4 o PHP 8.5 según las dependencias y pruebas del proyecto. PHP 8.2 solo recibe correcciones de seguridad hasta diciembre de 2026; no sería mi primera elección para una modernización que aspira a durar varios años.
Documentación técnica y referencias
- PHP: versiones soportadas.
- PHP: ramas fuera de soporte.
- PHP: incompatibilidades al pasar de 5.6 a 7.0.
- PHP: incompatibilidades al pasar de 7.4 a 8.0.
- Symfony: calendario de soporte de la rama 2.8.
- Composer: opciones de auditoría y comprobación de requisitos.
- Rector y PHPStan, herramientas de ayuda a la modernización.
Una pregunta para tu próximo cambio
Si mañana tuvieras que actualizar una dependencia crítica, ¿qué necesitarías comprobar para hacerlo sin paralizar el negocio? Empieza por esa respuesta, no por una versión de PHP.


Deja una respuesta