Lección 23 de 45 12 min de lectura

Formularios en Blade

Los formularios son la principal forma de recibir datos del usuario. En esta lección aprenderás a crear formularios en Blade con protección CSRF, simular métodos HTTP como PUT y DELETE, y mantener los valores anteriores cuando hay errores de validación.

Formularios HTML básicos

Un formulario en Blade es HTML estándar con algunas directivas especiales de Laravel. Veamos un formulario simple para crear un post:

blade
<!-- resources/views/posts/create.blade.php -->
<form action="{{ route('posts.store') }}" method="POST">
    @csrf

    <div>
        <label for="title">Título</label>
        <input type="text" name="title" id="title">
    </div>

    <div>
        <label for="content">Contenido</label>
        <textarea name="content" id="content" rows="5"></textarea>
    </div>

    <button type="submit">Crear post</button>
</form>

Protección CSRF con @csrf

La directiva @csrf es obligatoria en todos los formularios POST, PUT, PATCH y DELETE. Genera un campo oculto con un token que Laravel verifica para proteger tu aplicación de ataques CSRF (Cross-Site Request Forgery).

blade
<!-- @csrf genera esto: -->
<input type="hidden" name="_token" value="random-token-aqui">
Sin @csrf no funciona

Si olvidas @csrf, Laravel rechazará el formulario con un error 419 "Page Expired". Siempre inclúyelo después de la etiqueta <form>.

Métodos HTTP con @method

Los navegadores solo soportan GET y POST en formularios HTML. Para usar PUT, PATCH o DELETE (necesarios en rutas RESTful), Laravel proporciona la directiva @method:

blade
<!-- Formulario para editar (PUT) -->
<form action="{{ route('posts.update', $post) }}" method="POST">
    @csrf
    @method('PUT')

    <div>
        <label for="title">Título</label>
        <input type="text" name="title" id="title" value="{{ $post->title }}">
    </div>

    <button type="submit">Actualizar</button>
</form>

<!-- Formulario para eliminar (DELETE) -->
<form action="{{ route('posts.destroy', $post) }}" method="POST">
    @csrf
    @method('DELETE')

    <button type="submit">Eliminar</button>
</form>

La directiva @method genera un campo oculto que Laravel interpreta para enrutar correctamente la petición:

blade
<!-- @method('PUT') genera: -->
<input type="hidden" name="_method" value="PUT">

Conservar valores con old()

Cuando un formulario falla por errores de validación, el usuario pierde todo lo que escribió. La función old() recupera los valores anteriores de la sesión:

blade
<!-- Formulario de creación con old() -->
<form action="{{ route('posts.store') }}" method="POST">
    @csrf

    <div>
        <label for="title">Título</label>
        <input type="text" name="title" id="title" value="{{ old('title') }}">
    </div>

    <div>
        <label for="content">Contenido</label>
        <textarea name="content" id="content">{{ old('content') }}</textarea>
    </div>

    <button type="submit">Crear</button>
</form>

Para formularios de edición, combina old() con el valor actual del modelo usando el segundo parámetro:

blade
<!-- Formulario de edición: old() con valor por defecto -->
<form action="{{ route('posts.update', $post) }}" method="POST">
    @csrf
    @method('PUT')

    <div>
        <label for="title">Título</label>
        <!-- Si hay error, muestra old(). Si no, muestra $post->title -->
        <input type="text" name="title" id="title"
               value="{{ old('title', $post->title) }}">
    </div>

    <div>
        <label for="content">Contenido</label>
        <textarea name="content" id="content">{{ old('content', $post->content) }}</textarea>
    </div>

    <button type="submit">Actualizar</button>
</form>

Mostrar errores de validación

Cuando la validación falla, Laravel almacena los errores en la variable $errors. Puedes mostrarlos de varias formas:

blade
<!-- Mostrar todos los errores al inicio -->
@if ($errors->any())
    <div class="alert alert-danger">
        <ul>
            @foreach ($errors->all() as $error)
                <li>{{ $error }}</li>
            @endforeach
        </ul>
    </div>
@endif

<form action="{{ route('posts.store') }}" method="POST">
    @csrf
    ...
</form>

También puedes mostrar errores junto a cada campo usando la directiva @error:

blade
<form action="{{ route('posts.store') }}" method="POST">
    @csrf

    <div>
        <label for="title">Título</label>
        <input type="text" name="title" id="title"
               value="{{ old('title') }}"
               class="@error('title') is-invalid @enderror">
        @error('title')
            <span class="error">{{ $message }}</span>
        @enderror
    </div>

    <div>
        <label for="content">Contenido</label>
        <textarea name="content" id="content"
                  class="@error('content') is-invalid @enderror">{{ old('content') }}</textarea>
        @error('content')
            <span class="error">{{ $message }}</span>
        @enderror
    </div>

    <button type="submit">Crear</button>
</form>
La variable $message

Dentro de @error('campo'), la variable $message contiene el mensaje de error específico para ese campo.

Campos de selección

Los campos <select> y checkboxes requieren lógica adicional para mantener el valor seleccionado:

blade
<!-- Select con categorías -->
<div>
    <label for="category_id">Categoría</label>
    <select name="category_id" id="category_id">
        <option value="">Selecciona una categoría</option>
        @foreach ($categories as $category)
            <option value="{{ $category->id }}"
                {{ old('category_id') == $category->id ? 'selected' : '' }}>
                {{ $category->name }}
            </option>
        @endforeach
    </select>
</div>

<!-- Select en edición -->
<select name="category_id" id="category_id">
    @foreach ($categories as $category)
        <option value="{{ $category->id }}"
            {{ old('category_id', $post->category_id) == $category->id ? 'selected' : '' }}>
            {{ $category->name }}
        </option>
    @endforeach
</select>
blade
<!-- Checkbox -->
<div>
    <label>
        <input type="checkbox" name="is_published" value="1"
            {{ old('is_published') ? 'checked' : '' }}>
        Publicar ahora
    </label>
</div>

<!-- Checkbox en edición -->
<div>
    <label>
        <input type="checkbox" name="is_published" value="1"
            {{ old('is_published', $post->is_published) ? 'checked' : '' }}>
        Publicado
    </label>
</div>

Ejemplo completo: formulario de creación

Veamos cómo conectar un formulario con su controlador:

php
<?php

// routes/web.php
use App\Http\Controllers\PostController;

Route::get('/posts/create', [PostController::class, 'create'])->name('posts.create');
Route::post('/posts', [PostController::class, 'store'])->name('posts.store');
php
<?php

// app/Http/Controllers/PostController.php
declare(strict_types=1);

namespace App\Http\Controllers;

use App\Models\Category;
use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;

class PostController extends Controller
{
    public function create(): View
    {
        $categories = Category::all();

        return view('posts.create', compact('categories'));
    }

    public function store(Request $request): RedirectResponse
    {
        $post = Post::create([
            'title' => $request->input('title'),
            'content' => $request->input('content'),
            'category_id' => $request->input('category_id'),
        ]);

        return redirect()->route('posts.show', $post);
    }
}
blade
<!-- resources/views/posts/create.blade.php -->
@extends('layouts.app')

@section('content')
    <h1>Crear nuevo post</h1>

    @if ($errors->any())
        <div class="alert alert-danger">
            <ul>
                @foreach ($errors->all() as $error)
                    <li>{{ $error }}</li>
                @endforeach
            </ul>
        </div>
    @endif

    <form action="{{ route('posts.store') }}" method="POST">
        @csrf

        <div class="form-group">
            <label for="title">Título</label>
            <input type="text" name="title" id="title"
                   value="{{ old('title') }}"
                   class="@error('title') is-invalid @enderror">
            @error('title')
                <span class="error">{{ $message }}</span>
            @enderror
        </div>

        <div class="form-group">
            <label for="category_id">Categoría</label>
            <select name="category_id" id="category_id"
                    class="@error('category_id') is-invalid @enderror">
                <option value="">Selecciona...</option>
                @foreach ($categories as $category)
                    <option value="{{ $category->id }}"
                        {{ old('category_id') == $category->id ? 'selected' : '' }}>
                        {{ $category->name }}
                    </option>
                @endforeach
            </select>
            @error('category_id')
                <span class="error">{{ $message }}</span>
            @enderror
        </div>

        <div class="form-group">
            <label for="content">Contenido</label>
            <textarea name="content" id="content" rows="10"
                      class="@error('content') is-invalid @enderror">{{ old('content') }}</textarea>
            @error('content')
                <span class="error">{{ $message }}</span>
            @enderror
        </div>

        <button type="submit">Crear post</button>
    </form>
@endsection

Resumen

  • @csrf es obligatorio en formularios POST, PUT, PATCH y DELETE
  • @method('PUT') simula métodos HTTP que los navegadores no soportan
  • old('campo') recupera el valor anterior tras errores de validación
  • old('campo', $default) usa un valor por defecto si no hay valor anterior
  • @error('campo') muestra errores específicos de cada campo
  • Usa route('nombre') para generar URLs de acción en formularios

Ejercicios

Ejercicio 1: Formulario de contacto

Crea un formulario de contacto con los campos: name, email y message. Incluye protección CSRF, usa old() para mantener valores y muestra errores con @error.

Ver solución
<!-- resources/views/contact.blade.php -->
<h1>Contáctanos</h1>

<form action="{{ route('contact.send') }}" method="POST">
    @csrf

    <div>
        <label for="name">Nombre</label>
        <input type="text" name="name" id="name"
               value="{{ old('name') }}"
               class="@error('name') is-invalid @enderror">
        @error('name')
            <span class="error">{{ $message }}</span>
        @enderror
    </div>

    <div>
        <label for="email">Email</label>
        <input type="email" name="email" id="email"
               value="{{ old('email') }}"
               class="@error('email') is-invalid @enderror">
        @error('email')
            <span class="error">{{ $message }}</span>
        @enderror
    </div>

    <div>
        <label for="message">Mensaje</label>
        <textarea name="message" id="message" rows="5"
                  class="@error('message') is-invalid @enderror">{{ old('message') }}</textarea>
        @error('message')
            <span class="error">{{ $message }}</span>
        @enderror
    </div>

    <button type="submit">Enviar mensaje</button>
</form>

Ejercicio 2: Formulario de edición de usuario

Crea un formulario para editar un usuario con campos: name, email, y un select de role (admin, editor, user). El formulario debe usar PUT, mostrar los valores actuales del usuario y mantener los valores con old().

Ver solución
<!-- resources/views/users/edit.blade.php -->
<h1>Editar usuario: {{ $user->name }}</h1>

<form action="{{ route('users.update', $user) }}" method="POST">
    @csrf
    @method('PUT')

    <div>
        <label for="name">Nombre</label>
        <input type="text" name="name" id="name"
               value="{{ old('name', $user->name) }}"
               class="@error('name') is-invalid @enderror">
        @error('name')
            <span class="error">{{ $message }}</span>
        @enderror
    </div>

    <div>
        <label for="email">Email</label>
        <input type="email" name="email" id="email"
               value="{{ old('email', $user->email) }}"
               class="@error('email') is-invalid @enderror">
        @error('email')
            <span class="error">{{ $message }}</span>
        @enderror
    </div>

    <div>
        <label for="role">Rol</label>
        <select name="role" id="role"
                class="@error('role') is-invalid @enderror">
            <option value="user"
                {{ old('role', $user->role) === 'user' ? 'selected' : '' }}>
                Usuario
            </option>
            <option value="editor"
                {{ old('role', $user->role) === 'editor' ? 'selected' : '' }}>
                Editor
            </option>
            <option value="admin"
                {{ old('role', $user->role) === 'admin' ? 'selected' : '' }}>
                Administrador
            </option>
        </select>
        @error('role')
            <span class="error">{{ $message }}</span>
        @enderror
    </div>

    <button type="submit">Guardar cambios</button>
</form>

Ejercicio 3: Botón de eliminar con confirmación

Crea un botón para eliminar un post que use el método DELETE. Añade un atributo onclick con JavaScript para pedir confirmación antes de enviar el formulario.

Ver solución
<!-- En la vista de detalle del post -->
<form action="{{ route('posts.destroy', $post) }}"
      method="POST"
      onsubmit="return confirm('¿Estás seguro de eliminar este post?')">
    @csrf
    @method('DELETE')

    <button type="submit" class="btn-danger">
        Eliminar post
    </button>
</form>

<!-- Alternativa con botón más estilizado -->
<form action="{{ route('posts.destroy', $post) }}"
      method="POST"
      id="delete-form-{{ $post->id }}">
    @csrf
    @method('DELETE')
</form>

<button type="button" class="btn-danger"
        onclick="if(confirm('¿Eliminar este post?')) document.getElementById('delete-form-{{ $post->id }}').submit()">
    Eliminar
</button>

¿Te está gustando el curso?

Tenemos cursos premium con proyectos reales, soporte personalizado y certificado.

Descubrir cursos premium