🔎 Buscar

🚀 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.

Wiki / Apuntes📖 Contenido

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 serve es 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 columna slug. El route model binding resuelve el modelo; si no existe lanza ModelNotFoundException (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-debugbar en 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(), @can o el middleware can. 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 StorePostRequest encapsula reglas en rules() y authorize().

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 --tries y --backoff para 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, luego php artisan filament:install --panels y php 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

Estudio · Recursos de todo el mundo (inglés, chino, japonés, español, francés, ruso…) curados y traducidos al español.