Hay un momento en la vida de cualquier aplicación PHP antigua en el que el coste de
mantenerla empieza a superar al coste de migrarla. No es un momento dramático: es una
acumulación. Un cambio que toca cinco archivos. Un bug que solo aparece en producción.
Una prueba que no se puede escribir porque la clase instancia su propia conexión a la
base de datos.
Migrar a Laravel no consiste en reescribir desde cero. Consiste en un proceso progresivo:
entender qué hace el código actual, separar la lógica de negocio del acceso a datos,
sustituir PDO por Eloquent o el Query Builder de Laravel y validar cada paso sin tocar los
datos existentes ni dejar la producción caída.
Este artículo recorre ese proceso completo. Si al terminar sigues pensando que reescribir
desde cero es más rápido, es que no has vivido todavía una migración a medias.
El punto de partida: PDO, Singleton y lógica mezclada
El escenario típico es una aplicación PHP escrita hace años, sin framework, donde
cada archivo hace varias cosas a la vez:
- Conexión a la base de datos con
PDOinstanciada dentro de cada clase o a través de un Singleton global. - Consultas SQL escritas a mano —cadenas con
SELECT,INSERToUPDATEconstruidas directamente en el código, sin ORM ni Query Builder—, muchas veces concatenando variables. - Lógica de negocio, validaciones, acceso a datos y salida HTML en el mismo archivo.
- Dependencias creadas con
newdentro de los métodos, imposibles de sustituir en pruebas. - Estado global en variables o en un contenedor hecho a mano.
Así se ve un fragmento típico de esa aplicación:
<?php
class PedidoController
{
public function cancelar($id)
{
// Conexión al vuelo, dentro del método
$pdo = new PDO('mysql:host=localhost;dbname=app', 'root', 'secret');
// SQL a mano, concatenado
$sql = "SELECT * FROM pedidos WHERE id = " . $id;
$pedido = $pdo->query($sql)->fetch(PDO::FETCH_ASSOC);
if ($pedido['estado'] !== 'pendiente') {
return 'No se puede cancelar';
}
$pdo->exec("UPDATE pedidos SET estado = 'cancelado' WHERE id = " . $id);
echo '<h1>Pedido cancelado</h1>';
}
}
En un mismo método conviven la conexión a la base de datos, una consulta escrita a mano,
una regla de negocio y la salida HTML. No hay forma de probar la regla sin una base de
datos real, ni de cambiar el motor de base de datos sin romper el archivo entero.
Funciona, pero no se puede mantener.
Una app con PDO directo, Singleton y SQL a mano puede estar en producción durante años.
El problema no es que esté rota: es que cualquier cambio obliga a tocar muchos sitios,
no hay pruebas y cada despliegue es un riesgo.
Laravel no arregla eso por sí solo. Lo que lo arregla es la reingeniería que haces
mientras migras.
Por qué migrar a Laravel y no reescribir desde cero
Reescribir desde cero es la opción más tentadora y la más peligrosa. Se pierden años de
decisiones que funcionaban —aunque no fueran bonitas— y el negocio suele necesitar la
aplicación viva durante todo el proceso.
Laravel permite una migración por capas:
- Se puede mantener la base de datos actual y apuntar Eloquent a las tablas existentes.
- Se puede empezar por las partes menos críticas y dejar lo antiguo funcionando en paralelo.
- Se pueden sustituir consultas PDO por Query Builder o Eloquent una a una.
- Se puede introducir inyección de dependencias y contenedor de servicios sin reescribir toda la app.
Antes de tocar nada: entender el flujo
No todo el código antiguo debe desaparecer.
Primero hay que localizar cada punto donde la app toca la base de datos, comprobar
qué hace, migrarlo si corresponde y volver a validar el proyecto.
Hay código viejo que sigue resolviendo un caso real, código que ya nadie ejecuta y código
que solo existe porque nadie se atrevió a mirarlo. Los tres se parecen muchísimo en el
repositorio.
Borrar sin entender el flujo convierte una migración en un problema nuevo.
Preguntas útiles antes de migrar o eliminar cualquier cosa
- ¿Quién llama a este código? ¿Rutas, cron, integraciones externas?
- ¿Qué consultas SQL ejecuta y sobre qué tablas?
- ¿Qué pasa si deja de funcionar? ¿Se pierde información o solo deja de mostrarse algo?
- ¿Está cubierto por alguna prueba, aunque sea manual?
- ¿Existe ya una alternativa en Laravel que haga lo mismo?
La frontera: lógica de negocio y acceso a datos
Casi todos los problemas de una app PHP legacy con PDO nacen del mismo sitio: la lógica
de negocio y el acceso a datos están en el mismo archivo. Separarlos es la primera
decisión de la migración.
Aplicación
Las reglas, las decisiones, las validaciones y el flujo de los casos de uso.
En Laravel: controladores delgados, servicios, acciones, form requests.
No debería saber si los datos viven en MySQL, en un archivo o en un servicio remoto.
Infraestructura
Conexiones, consultas, clientes HTTP, colas, almacenamiento, correo.
En Laravel: Eloquent, Query Builder, repositorios, adaptadores, drivers.
Es lo sustituible: si cambia, la lógica de negocio no debería enterarse.
Cuando esa frontera no existe, una decisión de negocio y un SELECT acaban
en la misma línea. Y a partir de ahí, cualquier cambio se vuelve caro.
Cómo se ve la migración en código
Antes de entrar en la estrategia paso a paso, conviene ver a qué nos referimos cuando
decimos “sustituir PDO por Eloquent”. Estos son los tres cambios que más se repiten.
Nota: los ejemplos están simplificados para centrarse en la idea. En código real añadirías
tipado estricto, tipos de retorno, readonly en las propiedades y las
convenciones actuales de Laravel.
1. De SQL a mano a Eloquent
// Antes: PDO con SQL a mano
$stmt = $pdo->prepare("SELECT * FROM pedidos WHERE cliente_id = :id AND estado = :estado");
$stmt->execute([':id' => $id, ':estado' => 'pagado']);
$pedidos = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Después: Eloquent
$pedidos = Pedido::where('cliente_id', $id)
->where('estado', 'pagado')
->get();
La tabla sigue siendo la misma. Lo que cambia es que ya no escribes SQL: el modelo declara
a qué tabla apunta y Eloquent se encarga del resto.
2. De Singleton a inyección de dependencias
// Antes: Singleton global
class PedidoService
{
public function cancelar($id)
{
$db = Database::getInstance(); // Singleton
// ...
}
}
// Después: dependencia inyectada por el contenedor
class PedidoService
{
public function __construct(
private readonly PedidoRepository $pedidos,
) {}
public function cancelar(int $id): void
{
// ...
}
}
La dependencia se declara en el constructor. El contenedor de Laravel la resuelve.
Y en una prueba puedes pasar un doble sin tocar nada más.
3. De controlador gordo a controlador delgado + servicio
// Antes: todo en el controlador
public function cancelar($id)
{
// conexión, SQL, validación, lógica, respuesta HTML...
}
// Después: controlador delgado
public function cancelar(int $id, PedidoService $service)
{
$service->cancelar($id);
return redirect()->route('pedidos.index');
}
El controlador solo recibe, delega y responde. La lógica vive en el servicio, donde
sí se puede probar de forma aislada.
Estrategia de migración, paso a paso
El orden importa. Saltarse un paso obliga a repetir los anteriores.
- Mapear el flujo real. Recorrer los casos de uso de principio a fin y anotar
cada punto donde el código toca PDO, el sistema de archivos, la red o variables globales. - Levantar Laravel junto al código antiguo. Misma base de datos, mismos datos.
Nada se migra todavía: solo convive. - Definir qué necesita cada caso de uso. Describir en términos de dominio
qué datos y operaciones necesita la lógica de negocio, sin pensar todavía en SQL ni en
tablas concretas. - Sustituir PDO por Eloquent o Query Builder. Consulta a consulta,
no de golpe. Si la tabla tiene nombres raros, se ajustan con$tabley
$fillable. - Inyectar en lugar de instanciar. Pasar los servicios por el constructor
y dejar que el contenedor de Laravel los resuelva, en vez de crearnew
por dentro o tirar del Singleton global. - Mover la lógica a servicios o acciones. Los controladores solo reciben,
validan y delegan. La lógica de negocio deja de vivir en el mismo archivo que la consulta. - Sustituir de forma progresiva. Un caso de uso a la vez. Nunca toda la
aplicación de golpe. - Validar. Comprobar que el comportamiento observable no ha cambiado y que
la aplicación sigue desplegándose en producción. - Eliminar lo obsoleto. Solo cuando el camino nuevo está probado y en uso.
Conservar los datos por encima de todo
El código se puede reescribir tantas veces como haga falta. Los datos existentes, no.
Cualquier migración que ponga en riesgo la información acumulada durante años está
mal planteada, por muy elegante que sea el resultado técnico.
En la práctica esto significa: no renombrar tablas ni columnas durante la migración,
no tocar el esquema más de lo necesario, no introducir transformaciones de datos
que no se puedan revertir y mantener siempre una copia íntegra de la base de datos
antes de cada paso.
Validación en tres niveles
- Estructural: el proyecto compila, Composer resuelve, las rutas de Laravel
responden y no quedan referencias rotas al código antiguo. - Funcional: los casos de uso siguen produciendo el mismo resultado que antes
de la migración. - Automatizada: pruebas que cubren las piezas críticas para que la próxima
modificación no vuelva a empezar desde cero.
Resultado de la migración
Lo que se consigue al terminar
- PDO sustituido por Eloquent y Query Builder.
- Dependencias inyectadas en lugar de instanciadas.
- Responsabilidades mejor separadas.
- Persistencia desacoplada de determinados casos de uso.
- Servicios para operaciones complejas.
- Conservación de los datos existentes.
- Eliminación progresiva de la arquitectura legacy.
- Validación estructural, funcional y automatizada.
- Despliegue real en producción.
Errores frecuentes
- Reescribir desde cero. Se pierden años de decisiones que ya funcionaban.
- Migrar toda la base de datos de golpe. El esquema se conserva; lo que
cambia es cómo se accede a él. - Abstraer por abstraer. Un repositorio con una sola implementación y sin
motivo real solo añade capas. - Cambiar todo a la vez. Sin pasos pequeños y verificables, no hay forma
de saber qué rompió qué. - Olvidar la validación. Una migración sin pruebas es una apuesta, no una
mejora.
Cuándo NO migrar a Laravel
Migrar no es siempre la respuesta. Hay escenarios donde la reingeniería cuesta más de lo
que aporta, y reconocerlo forma parte de hacer bien el trabajo.
- La aplicación es pequeña y estable. Si no va a crecer y apenas se toca,
migrar añade riesgo sin un retorno claro. - Está a punto de desaparecer. Invertir meses en migrar algo que se va a
retirar en un año es tirar dinero. - El equipo no conoce Laravel. Migrar sin experiencia previa multiplica los
errores y alarga el proceso sin necesidad. - No hay forma de validar. Si no existe ninguna manera de comprobar que el
comportamiento se mantiene, cada paso es una apuesta ciega.
En esos casos, una refactorización interna sin cambiar de framework suele ser suficiente.
La migración a Laravel tiene sentido cuando el mantenimiento duele más que migrar.
Conclusión
Pasar de una app PHP con PDO y Singleton a Laravel no va de reescribir. Va de separar la
lógica de negocio del acceso a datos, inyectar dependencias en lugar de instanciarlas y
sustituir cada consulta antigua por una equivalente en Eloquent o Query Builder, validando
cada paso antes de dar el siguiente.
El objetivo no es tener el código más elegante del mundo, sino poder seguir modificando la
aplicación dentro de dos años sin que cada cambio sea una aventura.
Si estás pensando en migrar tu propia aplicación y no sabes por dónde empezar, este caso
real te da el mapa completo: qué se encontró, en qué orden se atacó y cómo se validó cada
paso antes de tocar producción.
Visitas: 8
Automation & Data Specialist | Web Development | Data Processing & Integration
