Curso de Laravel

Plantillas Blade en Laravel: vistas, layouts y directivas

Por Víctor Peña · Publicado el

Hola, ¿cómo están? Continuando con el curso de Laravel, hoy veremos Blade, el sistema de plantillas del framework.

Es donde se construye lo que el usuario finalmente ve.

¡Empecemos!

Qué es Blade

Blade es un motor de plantillas que permite escribir HTML con una sintaxis más limpia que PHP puro.

Los archivos van en resources/views/ y terminan en .blade.php:

resources/views/
├── layouts/
│   └── app.blade.php
├── pacientes/
│   ├── index.blade.php
│   ├── create.blade.php
│   └── show.blade.php
└── welcome.blade.php

Y se referencian con puntos, sin la extensión:

return view('pacientes.index');   // resources/views/pacientes/index.blade.php

Un detalle importante: Blade se compila a PHP puro y el resultado se cachea. No hay penalización de rendimiento por usarlo.

Mostrar datos

<h1>{{ $paciente->nombre }}</h1>
<p>Edad: {{ $paciente->fecha_nacimiento->age }} años</p>
<p>Total: {{ number_format($cita->costo, 2) }} Bs</p>

Las llaves dobles imprimen el valor escapando el HTML, que es la protección contra XSS que vimos en el curso de PHP. Si un usuario guardó <script> en su nombre, se muestra como texto y no se ejecuta.

Esa protección es automática y es una de las mejores cosas de Blade.

Si necesitas mostrar HTML sin escapar:

{!! $contenido !!}

Úsalo solo con contenido que tú controlas. Con texto escrito por usuarios, nunca: es exactamente la puerta al XSS.

Y para valores que pueden no existir:

{{ $paciente->telefono ?? 'Sin teléfono' }}
{{ $paciente->telefono ?: 'Sin teléfono' }}

Directivas de control

@if ($cita->estado === 'programada')
    <span class="verde">Programada</span>
@elseif ($cita->estado === 'atendida')
    <span class="azul">Atendida</span>
@else
    <span class="rojo">Cancelada</span>
@endif

@unless ($paciente->activo)
    <p>Esta cuenta está desactivada</p>
@endunless

@isset($paciente->correo)
    <a href="mailto:{{ $paciente->correo }}">Escribir</a>
@endisset

@empty($citas)
    <p>No hay citas registradas</p>
@endempty

Bucles

@foreach ($pacientes as $paciente)
    <tr>
        <td>{{ $paciente->nombre }}</td>
        <td>{{ $paciente->telefono }}</td>
    </tr>
@endforeach

@for ($i = 1; $i <= 5; $i++)
    <span>{{ $i }}</span>
@endfor

@while ($condicion)
    ...
@endwhile

Y una directiva muy práctica que combina el bucle con el caso vacío:

@forelse ($citas as $cita)
    <li>{{ $cita->fecha }}</li>
@empty
    <li>No hay citas para mostrar</li>
@endforelse

Eso ahorra el @if (count(...)) alrededor del bucle.

La variable $loop

Dentro de un @foreach tienes información sobre la iteración:

@foreach ($pacientes as $paciente)
    <tr class="{{ $loop->even ? 'fila-gris' : '' }}">
        <td>{{ $loop->iteration }}</td>
        <td>{{ $paciente->nombre }}</td>
    </tr>
@endforeach

Las más útiles:

Propiedad Devuelve
$loop->index Empieza en 0
$loop->iteration Empieza en 1
$loop->first ¿Es el primero?
$loop->last ¿Es el último?
$loop->even / $loop->odd Par o impar
$loop->count Total de elementos
$loop->parent En bucles anidados

Herencia de plantillas

Aquí está lo que evita repetir la cabecera y el pie en cada pantalla.

El layout, en resources/views/layouts/app.blade.php:

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@yield('titulo', 'Sistema Clínica')</title>
    @vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body>
    <header>
        <nav>
            <a href="{{ route('pacientes.index') }}">Pacientes</a>
            <a href="{{ route('citas.index') }}">Citas</a>
        </nav>
    </header>

    <main>
        @if (session('exito'))
            <div class="alerta-exito">{{ session('exito') }}</div>
        @endif

        @yield('contenido')
    </main>

    <footer>
        <p>&copy; {{ date('Y') }} Sistema Clínica</p>
    </footer>
</body>
</html>

Y una vista que lo extiende:

@extends('layouts.app')

@section('titulo', 'Listado de pacientes')

@section('contenido')
    <h1>Pacientes</h1>

    <table>
        @forelse ($pacientes as $paciente)
            <tr>
                <td>{{ $paciente->nombre }} {{ $paciente->apellido }}</td>
                <td>
                    <a href="{{ route('pacientes.show', $paciente) }}">Ver</a>
                </td>
            </tr>
        @empty
            <tr><td>No hay pacientes registrados</td></tr>
        @endforelse
    </table>

    {{ $pacientes->links() }}
@endsection

@yield define el hueco en el layout; @section lo llena desde la vista.

Fíjate en el mensaje de sesión: al estar en el layout, cualquier redirección con with('exito', ...) lo muestra automáticamente, sin repetirlo en cada pantalla.

Incluir fragmentos

@include('pacientes.partials.formulario')
@include('pacientes.partials.tarjeta', ['paciente' => $p])
@includeWhen($esAdmin, 'admin.panel')
@includeIf('opcional.banner')

Se usa mucho para compartir el formulario entre crear y editar:

{{-- pacientes/create.blade.php --}}
<form method="POST" action="{{ route('pacientes.store') }}">
    @csrf
    @include('pacientes.partials.campos')
    <button type="submit">Guardar</button>
</form>
{{-- pacientes/edit.blade.php --}}
<form method="POST" action="{{ route('pacientes.update', $paciente) }}">
    @csrf
    @method('PUT')
    @include('pacientes.partials.campos')
    <button type="submit">Actualizar</button>
</form>

Un solo archivo con los campos, usado en los dos sitios.

Formularios: @csrf y @method

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

@csrf es obligatorio en todo formulario POST. Genera el token que protege contra el ataque CSRF que vimos en el curso de PHP. Sin él, Laravel rechaza la petición con un error 419.

Y como los navegadores solo envían GET y POST:

@method('PUT')
@method('DELETE')

Eso le dice a Laravel que trate la petición como PUT o DELETE, que es lo que esperan las rutas de recurso.

Directivas útiles

{{-- Esto es un comentario, no llega al HTML --}}

@php
    $total = $citas->sum('costo');
@endphp

@auth
    <p>Hola, {{ auth()->user()->name }}</p>
@endauth

@guest
    <a href="{{ route('login') }}">Iniciar sesión</a>
@endguest

@can('editar', $paciente)
    <a href="{{ route('pacientes.edit', $paciente) }}">Editar</a>
@endcan

@error('nombre')
    <span class="error">{{ $message }}</span>
@enderror

<input value="{{ old('nombre', $paciente->nombre ?? '') }}">

@class(['fila', 'destacada' => $cita->urgente])
@checked($paciente->activo)
@selected($doctor->id === $cita->doctor_id)
@disabled($bloqueado)

Sobre @php: úsala lo menos posible. Si necesitas calcular algo en la vista, casi siempre eso pertenece al controlador o al modelo.

Y old() es el que conserva lo escrito cuando la validación falla, combinado con el withInput() del controlador.

Vite: los estilos y el JavaScript

@vite(['resources/css/app.css', 'resources/js/app.js'])

Esa directiva genera las etiquetas correctas según el entorno: en desarrollo apunta al servidor de Vite con recarga en caliente, y en producción a los archivos compilados con su versión.

npm run dev     # desarrollo
npm run build   # producción

Errores comunes

  • Usar {!! !!} con contenido de usuarios. Es una vulnerabilidad XSS.
  • Olvidar @csrf y recibir un error 419.
  • Olvidar @method('PUT') en el formulario de edición.
  • Lógica dentro de @php. Va en el controlador.
  • Consultas dentro de un bucle en la vista, provocando un N+1 desde la plantilla.
  • Repetir el mismo HTML en lugar de usar @include o componentes.

Para cerrar

Blade hace que escribir vistas sea cómodo, y lo más valioso que aporta pasa desapercibido: escapa el HTML por defecto, así que te protege del XSS sin que tengas que acordarte.

Los dos hábitos: el layout con @extends para no repetir la estructura, y @include para los fragmentos compartidos.

En la siguiente lección veremos los componentes, que son la evolución de los includes.

Saludos y éxitos.

Norvic Software

Desarrollamos el software que tu empresa necesita

Somos una fábrica de software en Bolivia. Construimos sistemas a medida y aplicaciones móviles, y llevamos Inteligencia Artificial a las empresas que ya tienen un sistema funcionando.

Solicitar cotizaciónVer todos los servicios

Cotización sin costo · Respuesta directa por WhatsApp