Blade и общий шаблон портала
Оглавление серии «Laravel 13 и PHP: портал «Заявка»»
Первый маршрут возвращает постоянную страницу. Теперь передадим две учебные заявки в Blade и выделим общий шаблон. Результат урока — серверное преобразование данных в HTML с безопасным текстовым выводом. Браузер получает готовый документ, поэтому для списка не требуется отдельный JavaScript-запрос. При следующем переходе сервер сформирует новый ответ из своего текущего источника.
Наши записи пока массивы PHP: «Нужен доступ к макетам» и «Обновить описание проекта». Это позволяет изучить представление отдельно от базы. У каждой записи есть id, title и status. Статус остаётся машинным значением open или closed, а шаблон выбирает русскую подпись. Хранить подпись как второй независимо меняемый статус означало бы создать возможность противоречия.
Общий HTML-каркас
Создадим полный resources/views/layouts/app.blade.php. @yield обозначает область, которую заполнит дочерняя страница. Название области content является договором двух шаблонов, поэтому должно совпадать с @section на странице списка.
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Портал «Заявка»</title>
</head>
<body>
<header><a href="{{ route('tickets.index') }}">Заявка</a></header>
<main>@yield('content')</main>
</body>
</html>
Ссылка использует имя маршрута, известное с первого шага. Каркас отвечает за документ, а дочерняя страница — за предметное содержимое. Если копировать head и header на каждую страницу, изменение языка или общей навигации потребует синхронного исправления нескольких файлов. Выделение общей оболочки уменьшает именно это дублирование, а не любую повторяющуюся строку вообще.
Ниже полный resources/views/tickets/index.blade.php. @forelse позволяет отделить обычный список от состояния отсутствия записей.
@extends('layouts.app')
@section('content')
<h1>Заявки сотрудников</h1>
<ul>
@forelse($tickets as $ticket)
<li>
<strong>{{ $ticket['title'] }}</strong>
— {{ $ticket['status'] === 'open' ? 'Открыта' : 'Закрыта' }}
</li>
@empty
<li>Учебных заявок пока нет.</li>
@endforelse
</ul>
@endsection
Полностью заменим routes/web.php, передав массив через второй аргумент view. Переменная tickets в данных становится $tickets шаблона.
<?php
use Illuminate\Support\Facades\Route;
Route::get('/', fn () => redirect()->route('tickets.index'));
Route::get('/tickets', function () {
$tickets = [
['id' => 1, 'title' => 'Нужен доступ к макетам', 'status' => 'open'],
['id' => 2, 'title' => 'Обновить описание проекта', 'status' => 'closed'],
];
return view('tickets.index', ['tickets' => $tickets]);
})->name('tickets.index');
Ожидается, что страница содержит две строки с подписями «Открыта» и «Закрыта». Если передать пустой массив, появится текст об отсутствии учебных заявок. Это успешное представление пустого набора, а не ошибка маршрута. Когда источник станет базой, важно сохранить такое различие: невозможность выполнить запрос не должна незаметно становиться сообщением «данных нет».
Экранирование пользовательского текста
Двойные фигурные скобки Blade экранируют значение для вывода HTML. Документация Blade объясняет различие между обычным и неэкранированным выводом. Для заголовка заявки всегда нужен обычный текстовый договор: сотрудник вводит название обращения, а не теги для изменения интерфейса. Разметку strong добавляет автор шаблона.
Полезный неправильный случай — заменить название на строку с HTML-тегом и перейти к {!! !!}, чтобы тег «красиво работал». Такой вывод позволит данным влиять на структуру документа. В рабочей форме заголовок приходит от пользователя, поэтому это создаёт возможность внедрения чужой разметки. Правильное исправление состоит в сохранении экранирования, а не в доверии тому, что форма выглядит обычной текстовой.
Даже если поле сейчас задаётся автором массива, договор стоит выбрать с учётом следующего источника. После подключения Eloquent то же title будет храниться в базе, а после Form Request попадёт туда из HTTP. Если шаблон изначально читает его как текст, переход не потребует опасного изменения способа вывода. Такая последовательность позволяет менять источник, сохраняя смысл представления.
Экранирование HTML не делает значение автоматически безопасным для любого другого контекста. Например, вставка пользовательского текста внутрь произвольного JavaScript-кода или построение адреса требует отдельного подхода. В нашем шаблоне title находится в текстовом содержимом, а URL создаётся route из объявленного имени. Не переносите короткое правило фигурных скобок на все возможные места страницы без уточнения контекста.
Условия в представлении
Проверка статуса в шаблоне проста, поскольку допустимы два значения. Если база позже получит неизвестный status, текущий тернарный оператор назовёт его закрытым. Это ограничение текущего вывода, которое следует учитывать при расширении модели. Миграция и серверные операции будут сохранять только open или closed. Для нескольких состояний лучше ввести явное сопоставление, чтобы неизвестное значение не получало случайную подпись.
Blade не должен выполнять собственный запрос к базе внутри цикла. Здесь данные уже переданы обработчиком, поэтому количество обращений к источнику понятно. Позже контроллер получит отношения заранее и отдаст готовую коллекцию. Такая граница помогает избежать скрытой зависимости между разметкой и дорогими запросами. Шаблон решает, как представить полученные данные, а обработчик — какие данные читатель вправе получить.
@extends не означает отправку второго запроса браузером за каркасом. Сервер объединяет шаблоны при формировании одного ответа. Это отличается от клиентской загрузки отдельных компонентов и полезно для понимания роли Blade. Браузер видит итоговый HTML, а не исходные директивы. Если на странице буквально отображается @section, скорее всего файл обслуживается напрямую или не обработан ожидаемым механизмом приложения.
При ошибке области, например @section('body') вместо content, каркас не покажет ожидаемый список. Маршрут и массив могут быть правильными, но договор шаблонов нарушен. Проверяйте имена и полный путь файла прежде, чем менять данные. На этом шаге нет кеша API и асинхронного состояния, поэтому такие причины не следует вводить в объяснение отсутствующего текста.
Переход к устойчивым данным
Массив маршрута существует только как часть исходника. Он не позволяет сотруднику добавить обращение и не хранит историю изменения. Для устойчивых записей нужна таблица со структурой, внешним ключом владельца и временем создания. Следующий урок введёт эту схему, оставив два понятных названия обращений как общие учебные данные. Представление продолжит выводить тот же предметный смысл.
Самостоятельно сравните два изменения: убрать заявку из массива и задать ей status closed. В первом случае исчезает строка, во втором меняется только подпись. Это разные предметные операции, поэтому закрытие не должно впоследствии реализовываться удалением записи из базы. Уже простой шаблон позволяет увидеть потребность в истории, которая появится при закрытии заявки транзакцией.
Одинаковое имя данных в двух слоях
Ключ tickets передаваемого массива становится переменной шаблона, но само имя не заставляет Blade искать таблицу с таким названием. В этом уроке источником остаётся массив обработчика. Если переименовать ключ в requests и оставить $tickets в шаблоне, связь нарушится. Такое небольшое изменение полезно разбирать отдельно от SQL: передача данных представлению имеет собственный договор даже до появления базы.
Для самостоятельного воспроизведения результата «Показать список учебных заявок с экранированием» выберите lesson-02 в архиве исходников. Статус выполнения этого состояния: not_run.