Урок 05

Blade: шаблоны

Что такое Blade

Blade — шаблонизатор Laravel. Пишешь HTML с вставками PHP через директивы. Blade компилируется в чистый PHP и кешируется.

Плюсы:

  • Чистый синтаксис — {{ $var }}, @if.
  • Автоэкранирование — защита от XSS.
  • Наследование — layouts и sections.
  • Компоненты — переиспользуемые блоки.
  • Компиляция в PHP — быстро.

Где лежат

resources/views/, расширение .blade.php.

Пример: resources/views/posts/index.blade.php → имя posts.index.

Точка = подпапка: posts/index.blade.php = posts.index.

Простой шаблон

<!DOCTYPE html>
<html>
<head><title>Привет</title></head>
<body>
    <h1>Привет, {{ $name }}</h1>
</body>
</html>

Рендер из контроллера:

return view('hello', ['name' => 'Мир']);
return view('hello', compact('name'));

Вывод данных

{{ }} — вывод с экранированием:

{{ $name }}
{{ $user->email }}
{{ 2 + 2 }}
{{ $items[0] }}

Что экранируется:

  • < → &lt;
  • > → &gt;
  • & → &amp;
  • " → &quot;

Защита от XSS.

{!! !!} — без экранирования:

{!! $trustedHtml !!}

⚠️ Только для доверенных данных. Иначе — XSS.

{{ $name ?? 'Гость' }} — с дефолтом.

Условия

if / elseif / else:

@if ($user->isAdmin())
    <p>Админ</p>
@elseif ($user->isModerator())
    <p>Модератор</p>
@else
    <p>Пользователь</p>
@endif

unless:

@unless ($user->isAdmin())
    <p>Не админ</p>
@endunless

isset / empty:

@isset($user)
    {{ $user->name }}
@endisset

@empty($posts)
    <p>Постов нет</p>
@endempty

auth / guest:

@auth
    <p>Привет, {{ auth()->user()->name }}</p>
@endauth

@guest
    <a href="/login">Войти</a>
@endguest

switch:

@switch($status)
    @case('active')
        <p>Активен</p>
        @break
    @case('pending')
        <p>Ожидает</p>
        @break
    @default
        <p>Неизвестно</p>
@endswitch

Циклы

foreach:

@foreach ($posts as $post)
    <article>{{ $post->title }}</article>
@endforeach

forelse:

@forelse ($posts as $post)
    <p>{{ $post->title }}</p>
@empty
    <p>Постов нет</p>
@endforelse

for:

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

while:

@while ($condition)
    <p>...</p>
@endwhile

break / continue:

@foreach ($items as $item)
    @continue($item->hidden)
    @break($item->stop)
    <p>{{ $item->name }}</p>
@endforeach

Переменная $loop

Внутри @foreach доступна $loop:

@foreach ($posts as $post)
    {{ $loop->index }}          {{-- с 0 --}}
    {{ $loop->iteration }}      {{-- с 1 --}}
    {{ $loop->remaining }}
    {{ $loop->count }}          {{-- всего --}}
    {{ $loop->first ? 'первый' : '' }}
    {{ $loop->last ? 'последний' : '' }}
    {{ $loop->even ? 'чётный' : '' }}
    {{ $loop->odd ? 'нечётный' : '' }}

    @if ($loop->first)
        <hr>
    @endif
@endforeach

Комментарии

{{-- Этот комментарий не попадёт в HTML --}}

Обычный HTML-комментарий <!-- --> попадёт.

Layouts

Layout — каркас страницы.

resources/views/layouts/app.blade.php:

<!DOCTYPE html>
<html>
<head>
    <title>@yield('title', 'Мой сайт')</title>
    @stack('styles')
</head>
<body>
    <header>
        @include('partials.header')
    </header>

    <main>
        @yield('content')
    </main>

    <footer>
        @include('partials.footer')
    </footer>

    @stack('scripts')
</body>
</html>

Директивы:

  • @yield('name') — слот для контента.
  • @include('view') — вставить другой шаблон.
  • @stack('name') — стек для push.

Дочерний шаблон:

@extends('layouts.app')

@section('title', 'Главная')

@push('styles')
    <link rel="stylesheet" href="/css/home.css">
@endpush

@section('content')
    <h1>Добро пожаловать</h1>
    <p>Контент главной</p>
@endsection

@push('scripts')
    <script src="/js/home.js"></script>
@endpush

Что делает:

  • @extends — наследует layout.
  • @section('title', 'Главная') — короткий (инлайн).
  • @section('content') ... @endsection — длинный блок.
  • @push — добавить в стек.

Компоненты

Blade-компонент — переиспользуемый кусок.

Создать:

php artisan make:component Alert

Два файла:

  • app/View/Components/Alert.php — логика.
  • resources/views/components/alert.blade.php — шаблон.

alert.blade.php:

<div {{ $attributes->merge(['class' => 'alert alert-' . $type]) }}>
    {{ $slot }}
</div>

Использование:

<x-alert type="success">
    Сохранено!
</x-alert>

Что здесь:

  • $slot — содержимое тега.
  • $attributes — атрибуты.
  • merge — слияние классов.

Компонент с данными

app/View/Components/Alert.php:

namespace App\View\Components;

use Illuminate\View\Component;

class Alert extends Component
{
    public function __construct(
        public string $type = 'info',
    ) {}

    public function render()
    {
        return view('components.alert');
    }
}

Использование:

<x-alert type="success">Готово</x-alert>

Именованные слоты

<div class="card">
    <header>{{ $header }}</header>
    <main>{{ $slot }}</main>
    <footer>{{ $footer }}</footer>
</div>

Использование:

<x-card>
    <x-slot:header>
        Заголовок
    </x-slot>

    Основной контент

    <x-slot:footer>
        Футер
    </x-slot>
</x-card>

Другие директивы

csrf:

<form method="POST">
    @csrf
    <!-- поля -->
</form>

method:

<form method="POST">
    @csrf
    @method('PUT')
    <!-- поля -->
</form>

PHP:

@php
    $total = $items->sum('price');
@endphp

<p>{{ $total }}</p>

⚠️ Не злоупотребляй — логика в контроллере.

errors:

@if ($errors->any())
    <ul>
        @foreach ($errors->all() as $error)
            <li>{{ $error }}</li>
        @endforeach
    </ul>
@endif

@error('title')
    <p>{{ $message }}</p>
@enderror

old:

<input name="title" value="{{ old('title') }}">

Практика

layouts/app.blade.php:

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>@yield('title', config('app.name'))</title>
    @stack('styles')
</head>
<body>
    @include('partials.nav')

    <main class="container">
        @if (session('success'))
            <div class="alert alert-success">{{ session('success') }}</div>
        @endif

        @yield('content')
    </main>

    @include('partials.footer')
    @stack('scripts')
</body>
</html>

posts/index.blade.php:

@extends('layouts.app')

@section('title', 'Посты')

@section('content')
    <h1>Все посты</h1>

    @forelse ($posts as $post)
        <article>
            <h2>
                <a href="{{ route('posts.show', $post) }}">
                    {{ $post->title }}
                </a>
            </h2>
            <p>{{ $post->excerpt }}</p>
            <small>
                {{ $post->created_at->format('d.m.Y') }} —
                {{ $post->user->name }}
            </small>
        </article>
    @empty
        <p>Постов нет</p>
    @endforelse

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

Частые ошибки

1. Забыли @endsection:

ParseError: unexpected end of file

2. Точка вместо слеша:

@include('partials/header')   // ok
@include('partials.header')   // ok
@include('partials::header')  // namespace

3. {{ }} в JS:

<script>
    const name = "{{ $name }}";   // опасно, если есть кавычки
</script>

Решение: @json($name).

4. @yield без default:

<title>@yield('title')</title>   // может быть пусто

Решение: @yield('title', 'Сайт').

5. @section дважды:

@section('content')
    A
@endsection

@section('content')
    B   // заменит A
@endsection

Решение: @parent.

Итог

  • {{ }} — вывод с экранированием.
  • {!! !!} — без экранирования (осторожно).
  • @if, @foreach, @forelse — управляющие.
  • $loop — информация о цикле.
  • @extends, @section, @yield — наследование.
  • <x-...> — компоненты.
  • @csrf, @method — формы.
  • old(), @error — работа с ошибками.

Дальше — миграции.