De PDO y Singleton a Laravel: la reingeniería completa de una aplicación PHP legacy
De PHP + PDO + Singleton a Laravel: reingeniería de una aplicación legacy
Migrar una aplicación PHP legacy a Laravel no consiste simplemente en cambiar unas cuantas consultas SQL, instalar un framework y mover archivos.
En este artículo documento la reingeniería completa de una aplicación que durante años había crecido alrededor de una arquitectura tradicional basada en PHP, PDO y Singleton, junto con una mezcla de responsabilidades que hacía cada vez más difícil mantener y evolucionar el proyecto.
El punto de partida era una aplicación funcional, pero con una arquitectura heredada: acceso a base de datos
mediante PDO, un Core utilizado como Singleton y Service Locator,
lógica de negocio mezclada con infraestructura y dependencias obtenidas directamente desde diferentes puntos
de la aplicación.
El objetivo no fue realizar una migración cosmética, sino reconstruir la arquitectura. La nueva versión utiliza Laravel como base, inyección de dependencias, separación de responsabilidades, modelos y repositorios definidos de forma explícita, servicios para la lógica de aplicación y una estructura mucho más clara entre dominio, aplicación e infraestructura.
El proceso también implicó revisar la compatibilidad con la base de datos existente, conservar los datos reales de la aplicación, adaptar las consultas y relaciones necesarias y eliminar progresivamente las dependencias del antiguo Singleton.
Finalmente, la nueva aplicación fue validada en desarrollo y desplegada en producción con Laravel, comprobando que la reingeniería no se quedara únicamente en una reorganización del código, sino que terminara en una aplicación funcional y preparada para continuar creciendo.
El artículo recorre las decisiones técnicas más importantes, los problemas encontrados durante el proceso y las diferencias entre mantener una aplicación legacy funcional y reconstruirla sobre una arquitectura más modular y mantenible.
No es una guía teórica sobre Laravel. Es el recorrido de una aplicación PHP real desde una arquitectura legacy basada en PDO + Singleton hasta una aplicación Laravel desplegada en producción.
1. El problema no era que la aplicación no funcionara
Esta es probablemente la primera distinción importante.
La aplicación original funcionaba. Tenía usuarios, productos, pedidos, blog, carrito, administración y datos históricos. Había sido utilizada durante años y contenía información que no podía simplemente descartarse durante una migración.
El problema era otro.
La aplicación había ido creciendo alrededor de decisiones arquitectónicas que tenían sentido en su contexto original, pero que con el tiempo habían generado demasiado acoplamiento.
El acceso a la base de datos dependía de una instancia global. La lógica de diferentes partes de la aplicación conocía directamente detalles de infraestructura. Las dependencias no siempre estaban declaradas explícitamente.
Y el antiguo Core había terminado desempeñando demasiadas funciones.
2. El punto de partida: PHP, PDO y Singleton
La aplicación original utilizaba PDO para comunicarse con la base de datos y
centralizaba buena parte del acceso mediante una clase Core.
El patrón utilizado era esencialmente un Singleton:
$core = Core::instance();
$db = $core->getDb();
Desde diferentes partes de la aplicación se obtenía esa instancia y, a partir de ella, la conexión.
Sobre el papel parecía sencillo. En la práctica, esto convertía la base de datos en una dependencia global.
class Pedido
{
public function guardar()
{
$db = Core::instance()->getDb();
//
}
}
La dependencia real de la clase no aparecía en su constructor ni en su interfaz. Estaba escondida dentro de la implementación.
Cuando una clase recibe sus dependencias explícitamente, resulta sencillo saber qué necesita para funcionar. Cuando las obtiene desde un Singleton global, las dependencias quedan dispersas por el código.
3. El Singleton no era el único problema
Eliminar el Singleton por sí solo no habría solucionado la arquitectura. El problema real era el acoplamiento entre responsabilidades.
- Acceso a base de datos.
- Consultas SQL.
- Reglas de negocio.
- Validaciones.
- Gestión de sesiones.
- Operaciones relacionadas con usuarios.
- Lógica de pedidos.
- Carrito.
- Productos.
- Blog.
- Administración.
- Infraestructura.
La consecuencia era una aplicación difícil de modificar sin conocer previamente una cantidad considerable de código.
Por eso la migración tenía que abordar el problema desde varios ángulos.
4. Antes de migrar: entender lo que realmente existía
Una migración de este tipo no empieza instalando Laravel. Empieza haciendo inventario.
Había que localizar:
- Clases antiguas.
- Referencias al
Core. - Llamadas a
Core::instance(). - Accesos directos a
PDO. - Propiedades
$db. includeyrequire.- Conexiones creadas manualmente.
- Consultas SQL.
- Dependencias ocultas.
- Posibles duplicados.
- Archivos que todavía pertenecían a la arquitectura anterior.
En un proyecto que ha evolucionado durante años, la estructura aparente y la estructura real no siempre coinciden.
No basta con mirar los directorios principales. Hay que buscar las dependencias que permanecen ocultas dentro del código.
5. La base de datos también forma parte de la migración
Otro error habitual consiste en tratar la migración de Laravel como si la aplicación y la base de datos fueran dos problemas independientes.
No lo eran.
La aplicación tenía datos reales que debían conservarse. Por tanto, no se podía plantear una reconstrucción desde cero simplemente creando tablas nuevas y dando por terminado el trabajo.
Había que conocer primero el esquema existente:
- Tablas.
- Claves primarias.
- Relaciones.
- Nombres históricos.
- Tipos de datos.
- Campos heredados.
- Relaciones incompletas.
- Inconsistencias.
- Datos reales que seguían siendo necesarios.
La migración debía respetar ese patrimonio de información.
Esto también obligó a revisar determinadas relaciones y consultas en lugar de asumir que el esquema legacy seguía exactamente las convenciones que Laravel esperaba.
6. Laravel no se utilizó como una capa de pintura
Una migración superficial habría producido algo parecido a esto:
Aplicación antigua
↓
copiar archivos
↓
instalar Laravel
↓
seguir utilizando el mismo Core
↓
seguir obteniendo PDO globalmente Eso no habría resuelto el problema. Habríamos cambiado el framework, pero no la arquitectura.
La decisión fue diferente:
LEGACY
PHP + PDO + Singleton
│
▼
auditoría
│
▼
reconstrucción
│
▼
LARAVEL
┌────────────┼────────────┐
│ │ │
Dominio Aplicación Infraestructura
│ │ │
modelos servicios persistencia
reglas casos de conexión DB
entidades uso repositorios Laravel pasó a ser la base de la nueva aplicación, pero la arquitectura se diseñó alrededor de responsabilidades claras.
7. Inyección de dependencias en lugar de dependencias globales
Uno de los cambios fundamentales fue sustituir el acceso global por inyección de dependencias.
En lugar de esto:
$db = Core::instance()->getDb();
la clase declara lo que necesita:
class PedidoService
{
public function __construct(
private PedidoRepository $pedidos
) {
}
}
Ahora la dependencia forma parte de la propia definición de la clase.
Esto proporciona varias ventajas: la clase es más fácil de entender, es más fácil de probar y es más fácil de sustituir. Sobre todo, deja de depender de un estado global.
Laravel puede resolver estas dependencias mediante su contenedor de servicios, mientras que la clase permanece centrada en su responsabilidad.
8. Del Service Locator al contenedor de Laravel
El antiguo Core cumplía, entre otras funciones, el papel de un
Service Locator.
El problema de este patrón no es únicamente la existencia de una clase llamada
Core. El problema aparece cuando cualquier parte de la aplicación puede acceder
a un objeto global y obtener desde él prácticamente cualquier cosa.
Eso hace que las dependencias reales sean difíciles de detectar.
La alternativa consiste en utilizar el contenedor de Laravel como mecanismo de resolución y, siempre que sea posible, recibir las dependencias mediante constructor.
9. Separar dominio, aplicación e infraestructura
La nueva estructura se diseñó alrededor de responsabilidades diferentes.
Dominio
El dominio representa las reglas y conceptos propios de la aplicación. Aquí deben vivir las decisiones relacionadas con el negocio, no detalles específicos de cómo se conecta la aplicación a MySQL o cómo se ejecuta una petición HTTP.
Aplicación
La capa de aplicación coordina los casos de uso. Aquí aparecen servicios relacionados con pedidos, checkout, usuarios o determinadas operaciones de negocio.
La aplicación indica qué hay que hacer, sin convertirse necesariamente en la implementación concreta de la persistencia.
Infraestructura
La infraestructura contiene los detalles técnicos:
- Persistencia.
- Conexiones.
- Repositorios concretos.
- Integración con servicios externos.
- Implementación de mecanismos técnicos.
Esta separación permite que cada parte tenga una responsabilidad más clara.
10. Repositorios: separar persistencia de lógica de negocio
Otra decisión importante fue utilizar repositorios explícitos para determinadas áreas de la aplicación.
PedidoRepository
ProductoRepository
UsuarioRepository
CarritoRepository
BlogRepository
AdminRepository La idea no consiste en crear una interfaz para absolutamente todo porque sí. El objetivo es evitar que la lógica de aplicación tenga que conocer directamente los detalles de persistencia cuando no necesita conocerlos.
class CheckoutService
{
public function __construct(
private PedidoRepository $pedidos
) {
}
}
La implementación concreta puede encargarse de las consultas necesarias.
Esto también facilita las pruebas porque la persistencia puede sustituirse por una implementación controlada durante un test.
11. Servicios para los casos de uso
La lógica de aplicación tampoco debía terminar repartida por controladores gigantes.
Un controlador debería encargarse principalmente de coordinar la petición HTTP y devolver la respuesta correspondiente.
Cuando existe una operación con varias reglas y pasos, esa responsabilidad pertenece a un servicio de aplicación.
Checkout
│
├── validar carrito
├── comprobar productos
├── calcular importes
├── crear pedido
├── guardar detalles
└── completar operación
En lugar de convertir un controlador en cientos de líneas de lógica, esa operación puede
quedar encapsulada en un CheckoutService.
La consecuencia es una aplicación más fácil de leer y también más fácil de probar.
12. Migrar los datos reales sin destruir el legado
Uno de los puntos delicados fue conservar los datos existentes.
No se trataba de crear una aplicación vacía y comprobar que Laravel funcionaba. Había que demostrar que la nueva aplicación podía trabajar con la información real que ya existía.
Esto obligó a revisar diferencias entre las convenciones del proyecto antiguo y las convenciones de Laravel.
- Nombres de tablas.
- Nombres de columnas.
- Claves primarias no convencionales.
- Relaciones históricas.
- Tipos de datos.
- Codificaciones.
- Consultas antiguas.
- Campos que todavía eran necesarios.
- Relaciones que necesitaban ser adaptadas.
En algunos casos, la solución correcta no era modificar inmediatamente la base de datos. Era adaptar el modelo Laravel al esquema existente.
Eso permitió avanzar con la aplicación sin convertir la migración en una reescritura destructiva de los datos.
13. Las consultas también tuvieron que revisarse
Una migración arquitectónica no consiste en copiar consultas SQL sin examinarlas.
Algunas consultas procedían de una arquitectura antigua y tenían que revisarse dentro del nuevo contexto.
También aparecieron decisiones relacionadas con consultas de existencia, relaciones y condiciones que debían conservar exactamente su semántica.
Por ejemplo, en determinadas comprobaciones fue necesario utilizar NOT EXISTS
en lugar de sustituirlo alegremente por NOT IN.
Por eso la validación no podía limitarse a comprobar que la página cargaba. Había que comprobar los resultados.
14. Las peculiaridades del código legacy
Una reingeniería real también significa encontrarse cosas que no estaban en el diseño inicial.
Durante la limpieza aparecieron diferentes restos de la arquitectura anterior:
- Referencias al Singleton.
- Propiedades relacionadas con
$db. - Conexiones PDO heredadas.
includeyrequireantiguos.- Archivos que ya no formaban parte de la arquitectura.
- Problemas de codificación.
- Diferencias de nombres y mayúsculas.
- Código que había sobrevivido a diferentes etapas del proyecto.
Uno de los errores más simples y más molestos fue un BOM antes de
<?php en un modelo.
No era un problema conceptual de Laravel. Era simplemente un problema real de un archivo PHP antiguo.
Y precisamente por eso estas migraciones necesitan auditoría. La arquitectura nueva puede ser correcta y aun así un único archivo heredado puede provocar un fallo de ejecución.
15. Limpiar no significa borrar a ciegas
Otro principio importante fue diferenciar entre eliminar código antiguo y eliminar código antiguo sin comprobar sus dependencias.
No tenía sentido borrar cualquier referencia a Core, PDO o
include simplemente porque apareciera en una búsqueda.
Primero había que determinar si la referencia pertenecía todavía a la aplicación.
- Identificar la dependencia.
- Sustituirla por la nueva arquitectura.
- Comprobar el comportamiento.
- Volver a buscar referencias antiguas.
- Repetir el proceso.
La limpieza debía ser reproducible. No debía depender de que alguien recordara qué archivos había tocado manualmente.
16. Pruebas: comprobar algo más que el HTTP 200
Una aplicación que devuelve 200 OK no necesariamente está funcionando correctamente.
Por eso la validación tuvo varias capas.
Primero, comprobaciones estructurales:
- Singleton eliminado.
- PDO legacy eliminado.
- Core legacy eliminado.
include/requireheredados revisados.- BOM revisados.
- Dependencias antiguas revisadas.
Después, comprobaciones funcionales:
Inicio
Tienda
Productos
Usuarios
Pedidos
Carrito
Blog
Administración Y finalmente pruebas automatizadas para comprobar comportamientos concretos.
La finalidad era detectar tanto errores arquitectónicos como errores funcionales.
17. La importancia de validar después de limpiar
Una limpieza puede introducir errores. Por eso cada modificación importante tenía que ir acompañada de una segunda comprobación.
buscar
↓
clasificar
↓
modificar
↓
ejecutar
↓
probar
↓
buscar de nuevo
↓
validar Este ciclo es especialmente importante cuando se eliminan dependencias globales.
Una búsqueda inicial puede indicar que existen veinte referencias a Core.
Después de migrarlas deberían quedar cero, salvo las que hayan sido justificadas expresamente.
La validación final debe demostrarlo.
18. Del entorno local a producción
Una aplicación puede funcionar perfectamente en local y fallar al desplegarla. Por eso el despliegue también formó parte de la reingeniería.
No bastaba con ejecutar php artisan serve en el entorno de desarrollo.
Había que comprobar:
- Versión de PHP.
- Extensiones.
- Composer.
- Variables de entorno.
- Conexión a base de datos.
- Permisos.
- Rutas públicas.
- Almacenamiento.
- Cachés.
- Configuración.
- Servidor web.
- SSL.
- DNS.
- Comportamiento real de la aplicación.
El objetivo era comprobar que la aplicación no dependiera accidentalmente de las condiciones específicas de la máquina de desarrollo.
19. Laravel en producción
Finalmente, la nueva aplicación llegó a producción.
Eso permitió comprobar algo fundamental: la migración no había terminado cuando el código compilaba ni cuando los tests pasaban.
Terminó cuando la aplicación reconstruida era capaz de funcionar con datos reales en un entorno real.
El despliegue también sirvió para descubrir problemas que no aparecían en local. Y eso forma parte del proceso.
20. El resultado: no solamente otro framework
Al terminar la migración, el cambio más importante no fue que la aplicación utilizara Laravel.
Fue que las responsabilidades habían dejado de estar concentradas alrededor de una dependencia global.
LEGACY
Core
│
┌───────┼────────┐
│ │ │
PDO lógica servicios
│ │ │
└───────┴────────┘
La arquitectura reconstruida quedó conceptualmente más próxima a:
Laravel
│
┌─────────┼─────────┐
│ │ │
Dominio Aplicación Infraestructura
│ │
Servicios Repositorios
│ │
└──────┬──────┘
│
Base de
datos
La diferencia no está solamente en los directorios. Está en dónde vive cada responsabilidad y quién conoce a quién.
21. Lo que realmente se ganó con la reingeniería
El beneficio principal no es que el código nuevo sea más moderno. Es que ahora resulta más sencillo razonar sobre él.
- Una dependencia puede localizarse.
- Un servicio puede probarse.
- Un repositorio puede sustituirse.
- Una consulta puede localizarse.
- Una responsabilidad puede modificarse sin recorrer medio proyecto.
Eso es mantenibilidad.
Y esa era la razón principal de la migración.
22. El siguiente problema: la infraestructura
Una vez reconstruida la aplicación aparece una conclusión interesante.
La arquitectura de software puede estar preparada para determinadas capacidades que el entorno de alojamiento no necesariamente permite utilizar.
Esto resulta especialmente relevante cuando una aplicación empieza a utilizar procesos persistentes y comunicación en tiempo real.
En desarrollo, por ejemplo, Laravel Reverb permite ejecutar un servidor WebSocket para las comunicaciones en tiempo real.
Eso abre la puerta a funcionalidades como:
- Mensajería instantánea.
- Notificaciones en tiempo real.
- Actualización de estados.
- Eventos enviados directamente al navegador.
- Comunicación entre administradores.
Pero estas capacidades requieren un entorno de producción donde esos procesos puedan mantenerse ejecutándose y administrarse correctamente.
Ahí aparece una diferencia fundamental entre una aplicación y su infraestructura.
23. Del hosting compartido a una infraestructura administrable
El siguiente paso natural de la aplicación no tiene por qué ser cambiar de framework. Puede ser cambiar el nivel de control sobre el entorno donde se ejecuta.
Un hosting compartido resulta cómodo porque muchas decisiones de infraestructura las toma el proveedor.
Un VPS plantea el modelo contrario: proporciona una máquina virtual sobre la que el administrador controla el sistema operativo, los servicios y la configuración.
Eso permite construir un entorno donde puedan convivir, por ejemplo:
Nginx
PHP-FPM
Laravel
MariaDB
PostgreSQL
Redis
Reverb
Supervisor
Node.js
Docker siempre que los recursos del servidor y las necesidades de la aplicación lo permitan.
En ese escenario, capacidades como PostgreSQL, pgvector o un proceso persistente
de Reverb dejan de depender de que el catálogo de un hosting compartido las ofrezca como servicio.
El coste de esa libertad es evidente: la administración del servidor pasa a formar parte del trabajo del proyecto.
Seguridad, actualizaciones, firewall, SSH, copias de seguridad, monitorización y recuperación dejan de ser problemas que pueda ignorar completamente.
Y para una aplicación que continúa creciendo, esa decisión forma parte de la arquitectura tanto como la elección del framework.
24. Qué aprendí de la migración
La principal conclusión no es que Laravel sea mejor que PHP legacy. Eso sería una simplificación.
Una aplicación legacy puede funcionar durante muchos años. El problema aparece cuando la estructura que la sostiene empieza a dificultar más el cambio de lo que facilita el mantenimiento.
En ese momento, cambiar únicamente herramientas no es suficiente.
Hay que:
- Revisar las responsabilidades.
- Conocer las dependencias.
- Proteger los datos existentes.
- Probar el comportamiento.
- Validar el resultado en producción.
La migración de esta aplicación fue, en realidad, una reingeniería progresiva.
No se trató de borrar todo y empezar de cero. Tampoco de envolver el código antiguo dentro de Laravel.
Se trató de entender qué existía, conservar lo que tenía valor, eliminar el acoplamiento innecesario y reconstruir las partes que necesitaban una arquitectura diferente.
25. Conclusión
Migrar una aplicación PHP legacy a Laravel es relativamente sencillo si el objetivo es únicamente conseguir que Laravel arranque.
Lo difícil empieza cuando el objetivo es mejorar realmente la aplicación sin perder lo que ya funciona.
En este proyecto, el cambio importante fue pasar de una arquitectura basada en dependencias globales, PDO y Singleton a una estructura donde las dependencias son explícitas y las responsabilidades están separadas.
La aplicación conserva sus datos reales, mantiene sus funcionalidades y funciona en producción, pero ahora dispone de una base arquitectónica mucho más preparada para continuar evolucionando.
Y esa última parte es probablemente la más importante.
Una migración no debería terminar cuando desaparece el código antiguo. Debería terminar cuando la aplicación queda en una posición mejor para el siguiente cambio.
En este caso, ese siguiente cambio ya no es solamente de código. También es de infraestructura.
Porque cuando una aplicación empieza a necesitar procesos persistentes, comunicación WebSocket, diferentes motores de base de datos o servicios adicionales, la arquitectura deja de estar limitada al código fuente.
La aplicación y el entorno donde se ejecuta forman parte del mismo sistema.
Y después de reconstruir la aplicación, el siguiente paso lógico es conseguir que esa infraestructura esté a la altura de la arquitectura que acabamos de construir.
Vic
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero en opinar!
Deja un comentario