Tema
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 --invokableEjemplo: 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
- Crea un
ContactoControllercon dos métodos:create(muestra la vistacontacto) ystore(validanombreymensajeobligatorios y redirige de regreso con el mensaje flash "Gracias por escribirnos"). - Crea un controlador invocable
HoraControllerque devuelva JSON con la fecha y hora actuales. - En un método, usa
abort_unlesspara devolver 403 si el parámetro?clave=no es igual asecreta.
Ver solución
- 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');- 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);- 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/Controllersy agrupan acciones relacionadas. php artisan make:controller Nombre --resource --model=Modelogenera un CRUD completo.- Pide
Request $requestpara 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.