Skip to content

Controladores ​

Escribir toda la lógica dentro de routes/web.php funciona para ejemplos pequeños, pero en un proyecto real el archivo se vuelve inmanejable. Los controladores agrupan la lógica relacionada en clases. En esta lección aprenderás a crearlos, a leer datos de la petición con Request y a devolver distintos tipos de respuesta.

Concepto ​

Si las rutas son el conmutador de una empresa, los controladores son los departamentos. El ProductoController atiende todo lo de productos; el PedidoController, todo lo de pedidos. Cada método público del controlador es una acción (listar, guardar, eliminar...).

Un controlador es una clase PHP normal dentro de app/Http/Controllers. Si repasaste POO, no hay nada nuevo: clases, métodos y namespaces.

Crear un controlador ​

powershell
# Controlador vacío
php artisan make:controller PaginaController

# Controlador de recurso: con los 7 métodos del CRUD
php artisan make:controller ProductoController --resource

# De recurso y además asociado a un modelo (usa route model binding en las firmas)
php artisan make:controller ProductoController --resource --model=Producto

# Controlador invocable: una sola acción (__invoke)
php artisan make:controller DescargarReporteController --invokable

Ejemplo: un controlador básico ​

php
<?php
// app/Http/Controllers/PaginaController.php

namespace App\Http\Controllers;

use Illuminate\View\View;

class PaginaController extends Controller
{
    // GET /
    public function inicio(): View
    {
        // Datos que enviamos a la vista
        $servicios = ['Diseño web', 'Tiendas en línea', 'Mantenimiento'];

        return view('paginas.inicio', [
            'titulo'    => 'Bienvenido',
            'servicios' => $servicios,
        ]);
    }

    // GET /nosotros
    public function nosotros(): View
    {
        $anioFundacion = 2019;
        $anios = now()->year - $anioFundacion;

        // compact('anios') equivale a ['anios' => $anios]
        return view('paginas.nosotros', compact('anios'));
    }
}

Y las rutas que lo usan:

php
<?php
// routes/web.php

use App\Http\Controllers\PaginaController;
use Illuminate\Support\Facades\Route;

Route::get('/', [PaginaController::class, 'inicio'])->name('inicio');
Route::get('/nosotros', [PaginaController::class, 'nosotros'])->name('nosotros');

Leer datos de la petición: Request ​

Laravel inyecta el objeto Illuminate\Http\Request si lo pides como parámetro. Olvídate de $_GET y $_POST:

php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class BusquedaController extends Controller
{
    // GET /buscar?q=laptop&categoria=3&pagina=2
    public function __invoke(Request $request)
    {
        $termino   = $request->input('q');             // 'laptop' (busca en query y body)
        $categoria = $request->integer('categoria');   // 3 (convertido a int)
        $pagina    = $request->query('pagina', 1);     // solo de la query string, con valor por defecto

        $request->has('q');          // ¿viene el campo?
        $request->filled('q');       // ¿viene y no está vacío?
        $request->boolean('oferta'); // "1", "true", "on", "yes" → true

        $datos  = $request->only(['q', 'categoria']);  // solo esos campos
        $resto  = $request->except(['_token']);        // todos menos esos
        $todo   = $request->all();                     // todo (úsalo con cuidado)

        // Información de la petición
        $request->method();      // 'GET'
        $request->path();        // 'buscar'
        $request->url();         // 'http://mi-app.test/buscar'
        $request->ip();          // IP del visitante
        $request->user();        // usuario autenticado o null

        return "Buscando '$termino' en la categoría $categoria, página $pagina";
    }
}

Error común

Usar $request->all() para crear registros directamente (Producto::create($request->all())). Un usuario malicioso puede añadir campos extra al formulario (como es_admin=1). Usa siempre los datos validados: $request->validate([...]) devuelve solo los campos permitidos. Lo verás a fondo en validación.

Controlador de recurso completo ​

Este es el esqueleto que genera --resource --model=Producto, ya relleno con lógica real (el modelo y la vista los construirás en las siguientes lecciones):

php
<?php
// app/Http/Controllers/ProductoController.php

namespace App\Http\Controllers;

use App\Models\Producto;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;

class ProductoController extends Controller
{
    // GET /productos → listar
    public function index(): View
    {
        $productos = Producto::latest()->paginate(10);
        return view('productos.index', compact('productos'));
    }

    // GET /productos/create → formulario de alta
    public function create(): View
    {
        return view('productos.create');
    }

    // POST /productos → guardar
    public function store(Request $request): RedirectResponse
    {
        // Valida; si falla, regresa al formulario con los errores automáticamente
        $datos = $request->validate([
            'nombre' => ['required', 'string', 'max:120'],
            'precio' => ['required', 'numeric', 'min:0'],
        ]);

        Producto::create($datos);

        // Redirige con un mensaje "flash" (dura solo una petición)
        return redirect()
            ->route('productos.index')
            ->with('exito', 'Producto creado correctamente.');
    }

    // GET /productos/{producto} → ver uno
    public function show(Producto $producto): View
    {
        return view('productos.show', compact('producto'));
    }

    // GET /productos/{producto}/edit → formulario de edición
    public function edit(Producto $producto): View
    {
        return view('productos.edit', compact('producto'));
    }

    // PUT/PATCH /productos/{producto} → actualizar
    public function update(Request $request, Producto $producto): RedirectResponse
    {
        $datos = $request->validate([
            'nombre' => ['required', 'string', 'max:120'],
            'precio' => ['required', 'numeric', 'min:0'],
        ]);

        $producto->update($datos);

        return redirect()
            ->route('productos.show', $producto)
            ->with('exito', 'Producto actualizado.');
    }

    // DELETE /productos/{producto} → eliminar
    public function destroy(Producto $producto): RedirectResponse
    {
        $producto->delete();

        return redirect()
            ->route('productos.index')
            ->with('exito', 'Producto eliminado.');
    }
}

Ruta:

php
Route::resource('productos', ProductoController::class);

Tipos de respuesta ​

Un método de controlador puede devolver muchas cosas:

php
<?php

namespace App\Http\Controllers;

use App\Models\Producto;

class EjemploRespuestasController extends Controller
{
    public function texto()
    {
        return 'Texto plano';                         // string → respuesta HTML
    }

    public function vista()
    {
        return view('inicio', ['nombre' => 'Ana']);   // HTML desde Blade
    }

    public function json()
    {
        // Arrays y modelos se convierten a JSON automáticamente
        return Producto::all();
        // o explícito, con código de estado:
        // return response()->json(['ok' => true], 201);
    }

    public function redireccion()
    {
        return redirect()->route('inicio');           // a una ruta con nombre
        // return back();                             // a la página anterior
        // return back()->withInput();                // conservando lo escrito
        // return redirect()->away('https://google.com'); // a otro dominio
    }

    public function descarga()
    {
        // Descargar un archivo de storage/app/private/reportes/ventas.pdf
        return response()->download(storage_path('app/private/reportes/ventas.pdf'), 'ventas-octubre.pdf');
    }

    public function error()
    {
        abort(403, 'No tienes permiso.');            // corta la ejecución con un código HTTP
        // abort_if($condicion, 404);
        // abort_unless($usuario->esAdmin(), 403);
    }

    public function conCabeceras()
    {
        return response('Contenido', 200)
            ->header('Content-Type', 'text/plain')
            ->header('X-Mi-Cabecera', 'valor');
    }
}

Controladores invocables ​

Cuando un controlador hace una sola cosa, usa __invoke:

php
<?php
// app/Http/Controllers/ExportarClientesController.php

namespace App\Http\Controllers;

use App\Models\Cliente;
use Symfony\Component\HttpFoundation\StreamedResponse;

class ExportarClientesController extends Controller
{
    // Exporta todos los clientes a CSV
    public function __invoke(): StreamedResponse
    {
        return response()->streamDownload(function () {
            $salida = fopen('php://output', 'w');
            fputcsv($salida, ['ID', 'Nombre', 'Correo']);   // encabezados

            // cursor() recorre registros uno a uno sin cargar todos en memoria
            foreach (Cliente::cursor() as $cliente) {
                fputcsv($salida, [$cliente->id, $cliente->nombre, $cliente->email]);
            }

            fclose($salida);
        }, 'clientes.csv');
    }
}
php
// Sin indicar método: Laravel llama a __invoke
Route::get('/clientes/exportar', ExportarClientesController::class)->name('clientes.exportar');

Inyección de dependencias ​

Además de Request, Laravel puede inyectar cualquier clase en el constructor o en los métodos. Esto te permite separar la lógica de negocio en servicios:

php
<?php
// app/Services/CalculadoraEnvio.php

namespace App\Services;

class CalculadoraEnvio
{
    // Calcula el costo de envío según el peso en kg
    public function calcular(float $pesoKg): float
    {
        $base = 99.0;
        return $pesoKg <= 1 ? $base : $base + ($pesoKg - 1) * 25;
    }
}
php
<?php
// app/Http/Controllers/CotizacionController.php

namespace App\Http\Controllers;

use App\Services\CalculadoraEnvio;
use Illuminate\Http\Request;

class CotizacionController extends Controller
{
    // Laravel crea la instancia de CalculadoraEnvio automáticamente
    public function __construct(private CalculadoraEnvio $calculadora)
    {
    }

    public function __invoke(Request $request)
    {
        $peso = (float) $request->input('peso', 1);

        return response()->json([
            'peso'  => $peso,
            'costo' => $this->calculadora->calcular($peso),
        ]);
    }
}

Buena práctica: controladores delgados

Un controlador debe coordinar, no hacer todo: recibir la petición, validar, llamar a un modelo o servicio y devolver una respuesta. Si un método pasa de ~30 líneas, probablemente parte de esa lógica debería ir a un servicio, a un Form Request o al modelo.

Error común

Olvidar el use del controlador en routes/web.php. El error es "Target class [ProductoController] does not exist". Agrega use App\Http\Controllers\ProductoController; arriba del archivo.

Ejercicios ​

  1. Crea un ContactoController con dos métodos: create (muestra la vista contacto) y store (valida nombre y mensaje obligatorios y redirige de regreso con el mensaje flash "Gracias por escribirnos").
  2. Crea un controlador invocable HoraController que devuelva JSON con la fecha y hora actuales.
  3. En un método, usa abort_unless para devolver 403 si el parámetro ?clave= no es igual a secreta.
Ver solución
  1. Contacto:
php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;

class ContactoController extends Controller
{
    public function create(): View
    {
        return view('contacto');
    }

    public function store(Request $request): RedirectResponse
    {
        $request->validate([
            'nombre'  => ['required', 'string', 'max:100'],
            'mensaje' => ['required', 'string', 'max:2000'],
        ]);

        // Aquí podrías guardar en BD o enviar un correo

        return back()->with('exito', 'Gracias por escribirnos');
    }
}
php
Route::get('/contacto', [ContactoController::class, 'create'])->name('contacto');
Route::post('/contacto', [ContactoController::class, 'store'])->name('contacto.store');
  1. Hora:
php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\JsonResponse;

class HoraController extends Controller
{
    public function __invoke(): JsonResponse
    {
        return response()->json([
            'fecha' => now()->format('Y-m-d'),
            'hora'  => now()->format('H:i:s'),
        ]);
    }
}
php
Route::get('/hora', HoraController::class);
  1. Clave:
php
public function secreto(Request $request)
{
    abort_unless($request->query('clave') === 'secreta', 403, 'Clave incorrecta');

    return 'Bienvenido a la zona secreta';
}

Resumen ​

  • Los controladores viven en app/Http/Controllers y agrupan acciones relacionadas.
  • php artisan make:controller Nombre --resource --model=Modelo genera un CRUD completo.
  • Pide Request $request para leer datos: input, query, integer, boolean, only.
  • Puedes devolver texto, vistas, JSON, redirecciones, descargas o cortar con abort().
  • Los controladores invocables (__invoke) son ideales para una sola acción.
  • Mantén los controladores delgados: mueve la lógica de negocio a servicios.

Academia Web — aprende, copia y construye.