🚀 Laravel a fondo
Instalación, routing, Eloquent, migraciones, auth con Sanctum y policies, colas, cache, Livewire, Filament y despliegue. Incluye un CRUD completo con login.
Laravel a fondo
Laravel es el framework PHP más usado del mundo: routing elegante, Eloquent ORM, migraciones, auth lista para usar y un ecosistema enorme (Livewire, Filament, Horizon, Forge). Este artículo recorre cada pieza con código real y cierra con una app con auth y CRUD.
Instalación
composer create-project laravel/laravel mi-app
cd mi-app
php artisan serve # desarrollo en http://localhost:8000
Requisitos: PHP >= 8.2, extensiones mbstring, openssl, pdo_mysql y Composer 2. La base de datos se configura en .env:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_DATABASE=mi_app
DB_USERNAME=root
DB_PASSWORD=
💡
php artisan servees para desarrollo. En producción usarás Nginx + PHP-FPM (o Forge/Octane).
Estructura de directorios
app/
├── Http/ # Controllers, Middleware, Requests
├── Models/ # modelos Eloquent
├── Policies/ # autorización
└── Jobs/ # trabajos encolados
bootstrap/ # arranque de la app
config/ # config de la app
database/migrations/ # esquema de BD
database/seeders/ # datos de prueba
resources/views/ # plantillas Blade (.blade.php)
routes/
├── web.php # rutas con estado (cookies, sesión)
└── api.php # rutas sin estado, prefijo /api
Routing
routes/web.php define rutas con estado; routes/api.php las de API (prefijo /api, sin sesión).
use App\Http\Controllers\PostController;
Route::get('/posts', [PostController::class, 'index']);
Route::post('/posts', [PostController::class, 'store'])->middleware('auth');
// Parámetro, constraint y ruta con nombre
Route::get('/posts/{post:slug}', fn (Post $post) => $post->title)
->where('post', '[A-Za-z0-9\-]+');
Route::get('/posts/{post}/edit', [PostController::class, 'edit'])->name('posts.edit');
return redirect()->route('posts.edit', $post);
⚠️ Con
{post:slug}se busca por la columnaslug. El route model binding resuelve el modelo; si no existe lanzaModelNotFoundException(404).
Controllers
php artisan make:controller PostController --model=Post --resource
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
class PostController extends Controller
{
public function index(): View
{
return view('posts.index', ['posts' => Post::latest()->paginate(10)]);
}
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'title' => ['required', 'min:3', 'max:255'],
'body' => ['required', 'string'],
]);
$request->user()->posts()->create($validated);
return redirect()->route('posts.show');
}
}
Blade: plantillas
Blade compila .blade.php a PHP puro. Hereda layouts y usa componentes y directivas.
Layouts
{{-- resources/views/layouts/app.blade.php --}}
<!DOCTYPE html>
<html lang="es">
<head>
<title>@yield('title', 'Mi App')</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body>
<nav>@auth <span>{{ auth()->user()->name }}</span> @endauth</nav>
<main>@yield('content')</main>
</body>
</html>
💡 Las vistas hijas usan
@extends('layouts.app')y rellenan los bloques con@section('content')/@endsection.
Componentes y directivas
Crea componentes con php artisan make:component Alert:
{{-- components/alert.blade.php --}}
@props(['tipo' => 'info'])
<div {{ $attributes->merge(['class' => "alert alert-$tipo"]) }}>{{ $slot }}</div>
{{-- Uso: <x-alert tipo="success">Guardado</x-alert> --}}
@if ($post->publicado) @else @endif
{{ $usuario?->name ?? 'Anónimo' }} {{-- escape automático --}}
@forelse ($posts as $post) @empty @endforelse
@auth ... @endauth @guest ... @endguest
@error('title') <span>{{ $message }}</span> @enderror
⚠️
{!! !!}no escapa la salida: inyectar ahí contenido del usuario sin sanitizar es una XSS garantizada.
Eloquent a fondo
Eloquent es el ORM (Active Record) de Laravel: cada modelo corresponde a una tabla y cada instancia a una fila. Define campos asignables ($fillable), casts y relaciones:
// app/Models/Post.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Post extends Model
{
protected $fillable = ['title', 'body', 'user_id', 'category_id'];
protected $casts = ['published_at' => 'datetime', 'is_published' => 'boolean', 'meta' => 'array'];
public function author(): BelongsTo { return $this->belongsTo(User::class, 'user_id'); }
public function categories(): BelongsToMany
{
return $this->belongsToMany(Category::class)->withTimestamps();
}
}
Relaciones clave
| Relación | Método | Tabla intermedia |
|---|---|---|
| 1:N | hasMany / belongsTo |
no |
| 1:1 | hasOne / belongsTo |
no |
| N:N | belongsToMany |
sí (pivote) |
// User.php
public function posts(): HasMany
{
return $this->hasMany(Post::class);
}
$user->posts; // Collection
$post->categories()->sync([1, 2, 3]); // reemplaza la relación N:N
Eager loading y el problema N+1
Recorrer 100 posts y hacer 101 consultas (1 + 100 por el autor) es el clásico bug de rendimiento. Solución: eager loading con with().
// ✅ Eager loading: 1 + 1 queries, no 1 + N
$posts = Post::with('author')->get();
if (auth()->user()->esAdmin()) {
$posts->load('categories'); // lazy eager loading
}
💡 Instala
barryvdh/laravel-debugbaren desarrollo y verás el contador de queries: la mejor forma de cazar N+1.
Accessors, mutators, scopes y query builder
// Accessor (al leer) y mutator (al escribir); scope = filtro reutilizable
class Post extends Model
{
protected function slug(): Attribute
{
return Attribute::make(get: fn (string $v) => strtoupper($v), set: fn (string $v) => Str::slug($v));
}
public function scopePublicados(Builder $query): Builder
{
return $query->where('is_published', true);
}
}
Post::publicados()->get();
// Query builder: consultas crudas sin el modelo
use Illuminate\Support\Facades\DB;
DB::table('posts')->where('is_published', true)->count();
Migraciones y seeders
Las migraciones versionan el esquema; los seeders pueblan datos de prueba.
php artisan make:migration create_posts_table
php artisan make:seeder PostSeeder
php artisan make:factory PostFactory
php artisan migrate # aplica migraciones
php artisan migrate:fresh # borra y recrea todo (¡cuidado!)
php artisan db:seed
// database/migrations/xxxx_create_posts_table.php
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('title');
$table->text('body');
$table->boolean('is_published')->default(false);
$table->timestamp('published_at')->nullable();
$table->timestamps();
});
// database/seeders/PostSeeder.php
use App\Models\Post;
public function run(): void
{
Post::factory()->count(50)->create();
}
Autenticación
Laravel 11+ trae auth lista para usar: Breeze/Jetstream como starter kits y Sanctum para tokens de API.
Breeze (login y registro)
composer require laravel/breeze --dev
php artisan breeze:install blade
php artisan migrate
npm install && npm run build
Aporta login, registro, verificación de email y rutas protegidas por el middleware auth.
Sanctum: tokens para API
// routes/api.php
Route::post('/login', [AuthController::class, 'login']);
Route::middleware('auth:sanctum')->group(function () {
Route::get('/user', [AuthController::class, 'user']);
Route::apiResource('posts', PostController::class);
});
💡 El cliente envía
Authorization: Bearer <token>en cada request protegida.
Policies
// app/Policies/PostPolicy.php
class PostPolicy
{
public function update(User $user, Post $post): bool
{
return $user->id === $post->user_id || $user->is_admin;
}
}
// en PostController
public function update(Request $request, Post $post)
{
$this->authorize('update', $post);
}
⚠️ La autorización se evalúa en el servidor con
authorize(),can(),@cano el middlewarecan. Ocultar el botón en la vista no es seguridad: la ruta sigue siendo llamable.
Validación
$validated = $request->validate([
'title' => 'required|string|max:255',
'email' => ['required', 'email', Rule::unique('users')->ignore($user->id)],
'password' => ['required', 'confirmed', Password::min(8)->letters()],
'tags' => 'nullable|array',
'tags.*' => 'string|exists:tags,id',
]);
<input name="title" value="{{ old('title') }}">
@error('title') <span class="error">{{ $message }}</span> @enderror
💡 Para validaciones reutilizables usa Form Requests:
php artisan make:request StorePostRequestencapsula reglas enrules()yauthorize().
Colas y jobs
Un job es una clase que implementa ShouldQueue con un método handle(). Se genera con artisan y se encola con dispatch():
EnviarNewsletter::dispatch($post);
EnviarNewsletter::dispatch($post)->onQueue('email')->delay(now()->addMinutes(5));
php artisan make:job EnviarNewsletter
php artisan queue:work --queue=email --tries=3 --timeout=60 # QUEUE_CONNECTION=redis
Horizon (colas sobre Redis) añade un dashboard para monitorizar jobs y retries: composer require laravel/horizon y php artisan horizon.
💡 Los jobs fallidos se guardan en la tabla
failed_jobs. Activa--triesy--backoffpara retries con espera exponencial.
Caching con Redis
use Illuminate\Support\Facades\Cache;
Cache::put('clave', $valor, now()->addMinutes(10));
$posts = Cache::remember('posts.publicados', 3600, fn () => Post::publicados()->get());
Cache::forget('posts.publicados');
// Redis directo (CACHE_STORE=redis en .env)
use Illuminate\Support\Facades\Redis;
Redis::set('contador', 1);
Redis::incr('contador');
Redis::zadd('ranking', 100, 'post-1');
⚠️ Cuando cacheas datos que cambian, invalida la clave en el mutador o en el evento
saved: un cache sucio entrega datos obsoletos silenciosamente, que es peor que lento.
Testing
Con php artisan make:test PostTest y php artisan test se genera y ejecuta la suite. En los tests usa RefreshDatabase (BD en memoria) y actingAs($user) para simular autenticación.
Livewire y Filament (intro)
- Livewire: reactividad sin JavaScript. El estado vive en componentes PHP y el front se actualiza por AJAX.
- Filament: panel de administración sobre Livewire. Instala con
composer require filament/filament, luegophp artisan filament:install --panelsyphp artisan make:filament-resource Post --generate(panel en /admin).
💡 Regla práctica: app pública con Blade/Livewire, panel interno con Filament. No necesitas React para un CRUD de administración.
Despliegue
composer install --optimize-autoloader --no-dev
php artisan migrate --force
php artisan config:cache && php artisan route:cache && php artisan view:cache
php artisan storage:link
# Permisos: la web escribe en storage/ y bootstrap/cache/
Forge provisiona servidores y despliega con GitHub push. Octane (Swoole/RoadRunner) mantiene la app en memoria entre requests: se instala con composer require laravel/octane y se lanza con php artisan octane:start --workers=4.
⚠️ Octane mantiene estado entre requests: no guardes nada mutable en propiedades estáticas ni en singletons; usa el contenedor para servicios por request (auth, conexiones).
Cheatsheet
| Tarea | Comando / snippet |
|---|---|
| Crear app | composer create-project laravel/laravel app |
| Modelo + migración | php artisan make:model Post -m |
| Rutas | Route::get('/x', [C::class,'m'])->name('x') |
| Validar | $request->validate([...]) |
| Eager loading | Post::with('user')->get() |
| Cola | Job::dispatch($x) |
| Token API | $user->createToken('t')->plainTextToken |
| Cache | Cache::remember('k', 3600, fn() => ...) |
| Test | $this->actingAs($user)->post('/posts', [...]) |
Para profundizar
- Documentación oficial de Laravel: routing, Eloquent, auth, colas, todo con ejemplos.
- Laravel Bootcamp: tutorial oficial paso a paso de una app con auth y CRUD.
- Laracasts: screencasts de Laravel desde cero hasta avanzado.
- Filament Docs: panel de administración sobre Livewire.
- Laravel Octane: aplicación en memoria con Swoole y RoadRunner.
- Ruta completa: Backend con PHP.