Convertir código duplicado de un controlador a código limpio en Laravel

Video thumbnail

Refactorizar código en Laravel no significa reescribirlo todo desde cero, sino mejorar su estructura, claridad y reutilización. Sobre el todo el tema de la modularización es clave en cualquier aplicación y es las bases para que nuestra aplicación sea escalable.

Entendiendo cuándo es momento de refactorizar código en Laravel

Señales de que tu código necesita limpieza

Hay varios síntomas que indican que el código en Laravel necesita una refactorización:

  • Controladores con métodos demasiado largos o repetidos.

  • Lógica de negocio mezclada con la presentación.

  • Dificultad para añadir nuevas entidades sin duplicar código.

  • Tests que se vuelven frágiles o difíciles de mantener.

Si tu aplicación presenta alguno de estos signos, es probable que el código esté pidiendo una reorganización.

Riesgos de no refactorizar a tiempo

Postergar una refactorización puede ser costoso. El mantenimiento se complica, los errores aumentan y cada nueva feature requiere más esfuerzo. Laravel ofrece una estructura flexible, pero sin disciplina arquitectónica puede derivar en un caos de controladores y helpers mal usados.

Mi experiencia refactorizando controladores de pago en Laravel

El problema: dos controladores casi idénticos

Para este ejemplo, tenemos lo típico en donde tenemos dos controladores prácticamente identicos, uno para procesar pagos de productos y otro para libros. Ambos compartían el mismo flujo, lo único que cambia es el registro del pago:

$payment = Payment::create([
    'paymentable_id' => $product->id,
    'paymentable_type' => Product::class,
    ***
]);

La solución: un BasePaymentController para centralizar la lógica

Decidí extraer la lógica común a un controlador base. Creé un BasePaymentController con todos los pasos compartidos del proceso de pago, y dejé que BookPaymentController y ProductPaymentController heredaran de él. Así, cada controlador hijo solo definía lo específico de su entidad, mientras la lógica general permanecía en un solo lugar.

Por qué no usé helpers (y qué aprendí de eso)

Consideré crear un helper para reutilizar el código, pero lo descarté. En mi caso, la lógica implicaba manejar requests y responses, lo que pertenece a la capa del controlador, no a funciones globales. Este fue un punto clave: usar helpers para lógica de negocio puede generar acoplamiento y pérdida de contexto.

Patrón de herencia y clases abstractas para evitar duplicación

Cómo diseñar un controlador base reutilizable

El BasePaymentController se convirtió en el corazón del sistema de pagos. Define métodos genéricos para validar el request, procesar la orden y devolver la vista correspondiente. Los controladores hijos solo indican qué entidad utilizar.; para esto empleamos la firma de cada método junto con la inyección de dependencias de Laravel para obtener una instancia de cada objeto.

Un ejemplo simplificado podría ser:

 
class BasePaymentController extends Controller
{
    ***

    // Realiza el pago
    public function processPayment(string $orderId, AbstractProduct $item, string $type)
    {
        $orderId = request('order_id') ?: $orderId;
        
        ***

        // Crea o recupera el pago
        if (!env('APP_DEMO')) {
            $payment = Payment::create([
                + 'paymentable_id'   => $item->id,
                + 'paymentable_type' => get_class($item),
                'user_id'          => auth()->id(),
                'price'            => $this->price,
                'ordenid'          => $this->idAPI,
                'trace'            => $this->responseAPI,
                'payments'         => $this->payment,
                'coupon'           => $this->coupon,
            ]);
        } else {
            $payment = Payment::first(1);
        }

        ***
    }

Implementando una clase abstracta AbstractProduct

Además, definí una clase AbstractProduct para asegurar que solo entidades “comprables” se pasaran al controlador. Los modelos concretos (Book, Product, etc.) heredan de esta clase y garantizan la compatibilidad tipada.

Ahora, las clases que heredan de la clase abstracta anterior:

<?php

namespace App\Http\Controllers\Store\Payments;

use App\Models\Book;

class BookPaymentController extends BasePaymentController
{
    public function payment(string $orderId, Book $product, string $type)
    {
        return $this->processPayment($orderId, $product, $type);
    }
}

Y

<?php

namespace App\Http\Controllers\Store\Payments;

use App\Models\Product;
use App\Models\Payment;

class ProductPaymentController extends BasePaymentController
{

    public function payment(string $orderId, Product $product, string $type)
    {
        return $this->processPayment($orderId, $product, $type);
    }
}
 
 

Tipado y validaciones: evitar errores al pasar entidades

El uso de clases abstractas y tipado fuerte reduce el riesgo de pasar objetos incorrectos. Si alguien intenta procesar una entidad no válida, PHP y Laravel lo detectan antes de ejecutar el flujo, mejorando la robustez.

Buenas prácticas para refactorizar código en Laravel

Aplicando el principio DRY y SOLID

El principio DRY (Don’t Repeat Yourself) fue la base de esta refactorización. En lugar de repetir la misma lógica en múltiples controladores, centralicé todo en un único lugar. Además, apliqué los principios SOLID para separar responsabilidades y mantener independencia entre capas.

Separación de responsabilidades con Service Layer y Repositories

En algunos casos, puedes llevar la lógica del pago a un servicio o repositorio dedicado. Esto mantiene los controladores delgados y facilita el testing. La idea es que el controlador solo orqueste, no procese.

Ejemplo completo de refactorización paso a paso

  1. Identifica código duplicado entre controladores.

  2. Crea un controlador base con la lógica común.

  3. Define métodos abstractos para los comportamientos específicos.

  4. Haz que los controladores concretos hereden del base.

  5. Refuerza la estructura con clases abstractas y tipado.

  6. Testea los flujos refactorizados.

Beneficios de un código refactorizado y limpio

Escalabilidad y mantenimiento

Agregar una nueva entidad comprable (por ejemplo, “Zapato”) se volvió cuestión de minutos. Solo tuve que crear un nuevo modelo que herede de AbstractProduct, sin tocar la lógica del pago, entiendase que esta nueva entidad es un producto comprable como la de productos y libros mostrados anteriormente.

Reducción de errores y claridad del flujo

El flujo de pago es ahora más claro y predecible. Cada clase tiene una función única, lo que facilita entender y modificar el código.

Preparar la base para testing y nuevas features

La estructura modular permite probar cada parte del flujo por separado. Al tener métodos bien definidos, los tests unitarios se vuelven simples y confiables.

Errores comunes al refactorizar en Laravel (y cómo evitarlos)

Sobrecargar el controlador base

No conviertas el BaseController en un “controlador dios”. Manténlo genérico, pero liviano. Si empieza a crecer demasiado, extrae partes a servicios.

No aprovechar la tipificación

Laravel permite combinar PHP moderno con tipado fuerte. Ignorar esta posibilidad puede anular los beneficios de la refactorización.

Usar helpers en lugar de clases

Los helpers son útiles para funciones muy específicas, pero abusar de ellos lleva al desorden. Prefiere controladores base o servicios para mantener la cohesión.

Conclusión: refactorizar no es rehacer, es evolucionar tu código

Refactorizar código en Laravel es un proceso continuo que mejora la salud de tu aplicación. No se trata de escribir más código, sino de escribirlo mejor. En mi caso, pasar de varios controladores duplicados a un flujo centralizado con herencia cambió completamente la mantenibilidad del proyecto.

Refactorizar no rompe tu aplicación; la fortalece. Te permite crecer con confianza, añadir nuevas entidades y mantener un código limpio, predecible y escalable.

Preguntas frecuentes

¿Cuándo es mejor crear un Service que heredar de un controlador base?
Cuando la lógica deja de pertenecer al flujo HTTP y pasa a ser una operación de negocio (por ejemplo, procesar pagos, generar reportes o enviar notificaciones).

¿Cómo probar un código refactorizado en Laravel?
Usa tests unitarios para los métodos del controlador base y tests funcionales para verificar el flujo completo. PHPUnit y Pest funcionan perfectamente para esto.

¿Qué beneficios tiene usar clases abstractas en Laravel?
Permiten definir contratos claros entre entidades, asegurando que cada una implemente los métodos necesarios sin duplicar código.

Acepto recibir anuncios de interes sobre este Blog.

Te muestro una demo de como podemos convertir un código duplicado en Laravel correspondiente a un controlador a reutilizarlo mediante la herencia.

| 👤 Andrés Cruz

🇺🇸 In english