Laravel · Часть 3 — Контроллеры, Blade и список пользователей из массива
1 / 1
lesson.laravel · часть 3
Часть 3 — Контроллеры, Blade и список пользователей из массива
Вынесем логику из маршрута-замыкания в контроллер, соберём страницу списка
пользователей и проследим, как данные проходят три слоя — от адреса в адресной
строке до готового HTML.
Три слоя и результат: путь данных в этой части
Сделаем: контроллер UserController с действием index(), макет
layouts/app.blade.php, стили public/css/app.css, шаблон users/index.blade.php
и маршрут /users. Четыре новых файла и одна правка.
Понадобится: проект project-laravel из части 2 — он уже работает через nginx по
адресу http://project-laravel.rs, — терминал в корне проекта и браузер.
Данные пока лежат в PHP-массиве. Базу подключим в части 4, когда три слоя
станут привычными: так мы разделяем две задачи вместо того, чтобы решать их
одновременно.
4 новых файла2 маршрута3 записи в списке
1листайте
Шаг 0 · Где мы
Что уже работает и что добавим
К концу части 2 у вас есть проект project-laravel в
~/recoursia/project-laravel, домен project-laravel.rs
в локальной зоне .rs и vhost nginx с root в public/. Запрос доходит до
public/index.php, роутер находит маршрут, ответ уходит обратно через буфер
вывода — весь путь из урока lesson.http.i.01.
До сих пор действия жили прямо в маршрутах: Route::get('/hello/{name}', …).
Для одной страницы-приветствия этого хватает. Для приложения, где будут список,
форма добавления, редактирование и удаление, — нет.
Последний шаг — проверка тремя способами: curl (HTML), route:list (маршруты)
и снимок браузера.
2листайте
Шаг 1 · Зачем контроллер
Маршрут-замыкание упирается в потолок
Замыкание — это функция, объявленная прямо внутри вызова Route::get(). У неё
нет имени и нет дома, кроме файла маршрутов.
Что начинает мешать, когда действие перестаёт быть одним:
routes/web.php превращается в свалку. Десять замыканий по 30 строк — это
300 строк логики в файле, чья работа — быть таблицей соответствия «адрес →
действие». Такую таблицу невозможно прочитать глазами.
Безымянное замыкание нельзя позвать откуда-то ещё. Ни из команды artisan,
ни из теста, ни из соседнего маршрута: на него нечем сослаться.
Нет навигации. IDE не ответит на вопрос «кто обрабатывает /users», если
у действия нет имени.
Контроллер решает это способом, который вы уже прошли в блоке ООП: действие
становится методом класса.
класс UserController — всё, что приложение делает с пользователями;
метод index() — одно действие: показать список;
рядом встанут create(), store(), update(), destroy() — это часть 5.
Вспомните задачу «Герои»: вы переносили поведение из процедурного кода в методы,
чтобы каждый объект отвечал за своё и не тянул всё в одну функцию. Здесь тот же
приём: маршрут отвечает за адрес, контроллер — за действие, шаблон — за разметку.
3листайте
Шаг 2 · Генерация
artisan создаёт контроллер сам
student@mint: ~/recoursia/project-laravel695 мс
student@mint:~/recoursia/project-laravel$
INFO Controller [app/Http/Controllers/UserController.php] created successfully.
Генерируем контроллер через artisan — artisan создаёт файл с готовым namespace — руками его писать не нужно.
Разбор команды
Маршрут из части 3 ссылается на класс и метод, поэтому сначала нужен сам класс. Файл создаёт artisan: он сам сводит путь и namespace, которые обязаны совпадать.
php artisan make:controller UserController
Токен
Зачем он и что меняет
php
запуск интерпретатора из PATH; на стенде PHP 8.3 — его мы проверили в части 1 командой php -v
artisan
не команда системы, а файл в корне проекта (18 строк, 424 байта): он подключает vendor/autoload.php, поднимает приложение и передаёт ему аргументы. Отсюда требование запускать команду из корня проекта
make:controller
имя artisan-команды; двоеточие разделяет группу make: (генераторы кода) и создаваемую сущность controller
UserController
аргумент — имя класса. Из него artisan выводит путь app/Http/Controllers/UserController.php и namespace App\Http\Controllers
Что произойдёт
Создаёт файл app/Http/Controllers/UserController.php — заготовку на 10 строк и 120 байт: namespace, use Illuminate\Http\Request;, class UserController extends Controller и пустое тело с комментарием. Печатает одну строку INFO с путём к файлу. Код возврата 0, время 695 мс.
На что смотреть
Смотрим на путь в квадратных скобках: он обязан совпадать с namespace класса — на этом совпадении держится автозагрузка Composer (PSR-4). 695 мс — это не запись файла (она мгновенная), а подъём приложения: автозагрузка, конфиги, сервисы.
Частые ошибки
Запуск не из корня проекта: php не найдёт файл и ответит «Could not open input file: artisan».
Файл переложили в другой каталог, не поправив namespace: путь перестаёт совпадать с именем класса, автозагрузчик не находит файл, и PHP сообщит, что класс не найден, — в момент первого обращения к классу, а не при сохранении файла.
Метода index() в заготовке нет: artisan не угадывает, какие действия понадобятся, их дописываем руками.
Что важно в этом выводе:
artisan печатает путь, а не «сделано вообще»:
app/Http/Controllers/UserController.php. Путь не выбран произвольно — он
следует из пространства имён класса, об этом следующий слайд.
Код возврата 0, одна строка INFO, 695 мс. Столько занимает не создание файла
(это микросекунды), а подъём приложения: автозагрузка, конфиги, сервисы.
Поэтому artisan-команды заметно тяжелее, чем touch, — и поэтому их не
вызывают на каждый запрос.
Почему генератор, а не файл руками: Composer-автозагрузка работает по PSR-4, где
пространству имён App\ соответствует каталог app/ (мы видели эту привязку в
composer.json в части 1). Значит, обязаны совпасть две вещи: файл лежит в
app/Http/Controllers/, и в нём написано namespace App\Http\Controllers;.
make:controller гарантирует совпадение и заодно подставляет базовый класс.
namespace App\Http\Controllers; — то самое совпадение с путём. Класс
App\Http\Controllers\UserController лежит в
app/Http/Controllers/UserController.php, автозагрузчик находит файл по имени
класса. Поэтому ни в маршрутах, ни в контроллере нет ни одного require.
use Illuminate\Http\Request; — импорт класса запроса. В заготовке он есть,
потому что большинство действий контроллера принимают запрос и читают из него
данные. Нашему index() запрос не нужен: список не зависит от параметров
адреса. В итоговом файле этой строки нет — сравните со следующим слайдом.
class UserController extends Controller — наследование базового класса.
Импорта для Controller в файле нет, значит PHP ищет его в текущем
пространстве имён — App\Http\Controllers, там же, где лежит сам файл.
// — пустое тело: artisan не угадывает, какие методы вам понадобятся.
Имя класса совпадает с именем файла. Это не соглашение проекта, а требование
автозагрузки: при несовпадении автозагрузчик не найдёт файл, и PHP сообщит, что
класс не найден, — в момент первого обращения к классу, а не при сохранении файла.
5листайте
Шаг 3 · Данные
Действие index(): данные пока в массиве
app/Http/Controllers/UserController.php
1<?php23namespace App\Http\Controllers;45classUserControllerextends Controller6{7publicfunctionindex()8{9$users=[10['name'=>'Иванов Иван Иванович','phone'=>'+7 (912) 345-67-89','email'=>'ivanov@example.com'],11['name'=>'Петрова Анна Сергеевна','phone'=>'+7 (903) 111-22-33','email'=>'petrova@example.com'],12['name'=>'Сидоров Пётр Алексеевич','phone'=>'+7 (926) 555-44-33','email'=>'sidorov@example.com'],13];1415return view('users.index',['users'=>$users]);16}17}
Семнадцать строк, 619 байт: к заготовке добавился один метод и данные.
public function index() — публичный метод без аргументов. Публичный потому,
что вызывает его не наш код, а фреймворк: роутер нашёл маршрут, создал объект
контроллера и позвал метод по имени, записанному в маршруте.
$users — двумерный массив: три элемента, каждый сам является массивом с
ключами name, phone, email. Ровно та структура, которую вы собирали в
блоке «Циклы и массивы» (lesson.06.03 — обращение к вложенному элементу) и в
ООП (lesson.oop.heroes.02 — герой как ассоциативный массив с ключами
health и strength).
Ключи у всех трёх элементов одинаковые. Это не мелочь: шаблон перебирает массив
и обращается к одним и тем же ключам. Если в одной строке ключа не окажется,
PHP не упадёт — страница просто выведет пустую ячейку.
return view(…) — действие возвращает ответ, а не печатает его. Никаких
echo: разметку соберёт фреймворк и отдаст в буфер вывода, дальше путь вам
знаком — PHP-FPM → nginx → браузер.
6листайте
Шаг 3 · Данные
view(): как данные доезжают до шаблона
Последняя строка index() — return view('users.index', ['users' => $users]);
(строка 15 файла, он показан на предыдущем слайде).
Два аргумента view(): имя шаблона и данные
Два аргумента — две разные обязанности:
'users.index' — имя шаблона. Точки работают как разделители каталогов,
корень — resources/views/, расширение .blade.php Blade подставляет сам.
Значит, будет открыт файл resources/views/users/index.blade.php.
['users' => $users] — данные для шаблона. Ключ массива становится
именем переменной: внутри шаблона появится $users.
Имя переменной в контроллере и ключ во втором массиве — вещи независимые.
Переменную можно назвать $rows или $list: шаблон увидит переменную с именем
ключа, а не с именем переменной контроллера.
В блоке HTTP вы делали то же самое руками (lesson.http.ii.03): класс booking
наследовал layout, переопределял content() и передавал данные в render().
Blade заменяет наследование классов директивами, а массив в render() — вторым
аргументом view(). Механика та же, записи меньше.
Восемь строк, 189 байт — и оба маршрута приложения.
use App\Http\Controllers\UserController; — импорт в начале файла. Без него
короткое имя UserController означало бы класс в пространстве имён самого
файла маршрутов, и роутер его бы не нашёл.
[UserController::class, 'index'] — массив из двух элементов: класс и
имя метода. Для роутера это и есть «действие».
::class — константа класса из ООП: она разворачивается в полную строку
App\Http\Controllers\UserController. Смысл ровно тот же, что у строки
'App\Http\Controllers\UserController@index', но опечатку редактор подсветит
сразу, а переименование класса пройдёт по всему проекту. Со строкой вы узнаете
об ошибке только в момент запроса.
Route::get('/', fn () => redirect('/users')); — корень домена не показывает
вторую копию списка, а отправляет на /users. Причина: страница должна жить по
одному адресу, иначе закладки, ссылки и будущая пагинация начнут разъезжаться.
fn () => — стрелочная функция: здесь она уместна, потому что тело состоит из
одного выражения.
Маршрута /hello/{name} из части 2 в файле больше нет: routes/web.php мы
переписали целиком.
8листайте
Шаг 5 · Макет
Один каркас страницы на все шаблоны
Список — не единственная страница. Дальше появятся форма добавления, форма
редактирования и страница с ошибками валидации (часть 5). Общее у всех:
<!DOCTYPE>, <head>, шапка с меню, подключение стилей.
Что будет без общего каркаса:
четыре копии <head> и четыре копии меню;
правка меню — это правка четырёх файлов, и один из них обязательно забудут;
подключение CSS в каждом шаблоне, со своим путём и своей версией.
Решение — макет: файл с каркасом и именованными точками, куда дочерние
шаблоны подставляют своё содержимое.
Такую схему вы уже строили в чистом PHP (lesson.http.ii.03): класс booking
наследовал layout, переопределял метод content(), а родительский render()
вызывал content() в нужном месте. Blade делает то же самое директивами:
Чистый PHP из блока HTTP
Blade
Роль
class booking extends layout
@extends('layouts.app')
кто наследует каркас
переопределённый content()
@section('content') … @endsection
что подставляем
вызов content() внутри render()
@yield('content')
куда подставляем
Принципиальная разница одна: Blade-шаблон компилируется в обычный PHP и
кэшируется в storage/framework/views/ — эти файлы мы смотрели в части 2.
@yield('title', 'Пользователи') — точка подстановки со значением по
умолчанию: если дочерний шаблон не объявит секцию title, в <title>
попадёт Пользователи.
<link rel="stylesheet" href="/css/app.css"> — слэш в начале означает «от
корня сайта», а не от текущего адреса. Корень сайта — public/ (так настроен
vhost в части 2), поэтому браузер запросит /css/app.css, а nginx отдаст файл
public/css/app.css как статику, не доходя до PHP. Тот же принцип «путь со
слэша — это адрес, а не серверная директория» вы разбирали в lesson.http.i.04.
<header class="topbar"> с меню — общий для всех страниц. Ссылка «Добавить»
ведёт на /users/create: такого маршрута пока нет, он появится в части 5.
@if (session('status')) — заготовка под flash-сообщение: текст, который
показывается один раз, сразу после редиректа. Сейчас в сессии пусто, поэтому
блок не выводится вовсе — это будет видно в HTML, который вернёт curl.
@yield('content') внутри <main class="page"> — сюда встанет список.
Порядок частей в файле и две точки подстановки — на схеме; номера строк те же,
что в листинге выше.
Файл целиком — 47 строк, 2783 байта; окно кода длиннее 40 строк ограничено по
высоте и листается внутри. Подсвечены две группы: строки 2–9 — переменные темы в
:root, строки 28–31 — правила таблицы. Рядом с ними .page (строка 25) задаёт
ширину контента max-width: 960px, а .muted (строка 27) приглушает счётчик в
заголовке.
Почему просто CSS, а не Vite:
файл лежит в public/, то есть внутри корня сайта — nginx отдаёт его
напрямую, PHP в этом запросе не участвует;
собирать нечего: ни препроцессора, ни JS-бандла в учебном приложении нет,
значит, не нужны ни npm run build, ни манифест сборки;
правка видна после обычного обновления страницы.
Переменные в :root — одна точка правки палитры. Обратите внимание: --accent
и --err заданы одним и тем же значением #b91c1c, поэтому смена акцентного
цвета не требует обхода всех правил.
В файле уже есть классы на вырост: .form, .field, .btn, .btn--danger
(формы и удаление — часть 5), .flash--ok и .flash--err (сообщения и ошибки
валидации), .pager (пагинация — часть 4).
Тридцать строк, 772 байта: подключение макета, две секции и таблица с перебором
массива. Дальше разберём шаблон по двум линиям — как он соединяется с макетом и
как выводит данные.
12листайте
Шаг 7 · Шаблон
@extends, @section, @yield: как шаблон встречается с макетом
@extends('layouts.app') — «этот шаблон дочерний, каркас взять из
resources/views/layouts/app.blade.php». Правило точек то же, что у view():
layouts.app → layouts/app.blade.php.
@section('title', 'Список пользователей') — короткая форма: значение задано
вторым аргументом, поэтому @endsection не нужен. Именно оно попадёт в
<title> и переопределит значение по умолчанию из макета.
@section('content') … @endsection — блочная форма: всё, что между
директивами, подставится в @yield('content').
В шаблоне нет ни <html>, ни <head>, ни шапки с меню — их добавляет
макет. Дублировать каркас в дочернем шаблоне не нужно и вредно: получится два
<html> в одном ответе.
Порядок работы: Blade собирает секции дочернего шаблона, затем подставляет их в
точки @yield макета. В итоговом HTML сначала идёт всё, что в макете стоит до
@yield('content'), затем содержимое секции, затем остаток макета.
Сборка страницы: секции шаблона встают в точки макета
Один нюанс, который видно в ответе curl: Blade не выравнивает отступы. На
место директив подставляется текст как есть, поэтому <h1> в готовом HTML
начинается с лишними пробелами. На отображение это не влияет — браузер
схлопывает пробелы, — но в исходнике страницы заметно.
13листайте
Шаг 7 · Шаблон
@forelse и {{ }}: перебор и экранирование
@forelse ($users as $user) — foreach со встроенной проверкой на пустоту.
пока в массиве есть элементы, выполняется тело между @forelse и @empty;
Зачем это нужно: пустой список — нормальное состояние страницы. С обычным
@foreach вы получили бы <tbody> без единой строки, то есть пустое место
вместо внятного сообщения. colspan="3" объединяет три ячейки — по числу колонок
(ФИО, Телефон, Email); сам приём вы разбирали в lesson.http.i.04.
{{ count($users) }} — обычная PHP-функция внутри {{ }}. В заголовке
появится число записей: сейчас 3. В блоке HTTP count() считал записи для
пагинации (lesson.http.ii.05) — здесь та же роль, только считает PHP, а не SQL.
{{ $user['name'] }} — обращение по ключу, потому что $user — элемент
двумерного массива, а не объект. Когда данные приедут из базы (часть 4), $user
станет объектом модели, и к полям мы будем обращаться через -> — как к
свойствам класса из блока ООП.
{{ }} экранирует вывод. В части 2 мы смотрели, что значение внутри {{ }}
проходит через e(), то есть через htmlspecialchars(), и угловые скобки
превращаются в < и >.
Почему это важно именно здесь: сейчас данные в массив написали мы сами. В части 5
в эти же ячейки попадут значения из формы — текст, который ввёл пользователь.
Экранирование вывода — сестра prepared statements из блока «SQL-инъекции»: та же
идея «данные не становятся командой», только на стороне HTML. Prepared statements
защищают запрос, {{ }} защищает разметку.
Для вставки готового HTML существует {!! !!} — без экранирования. В этом
шаблоне таких мест нет, и это правильно.
HTML списка пользователей — Видно, как Blade собрал таблицу из массива.
Разбор команды
Страницу надо проверить без браузера: curl показывает ровно тот HTML, который собрало приложение. sed обрезает вывод до первых 40 строк, иначе ответ не влезет в окно терминала.
curl -sS http://project-laravel.rs/users | sed -n "1,40p"
Токен
Зачем он и что меняет
curl
клиент HTTP: отправляет запрос и печатает тело ответа в stdout
-sS
два флага слитно: -s (silent) убирает прогресс-бар и статистику, -S (show-error) возвращает сообщения об ошибках, которые -s подавляет вместе с ними. Итог — чистый ответ, но проблему с соединением всё равно видно
http://project-laravel.rs/users
адрес запроса: домен project-laravel.rs из локальной зоны .rs (dnsmasq отдаёт 127.0.0.1, проверяли в части 1) и URI /users — маршрут из routes/web.php
|
пайп: stdout curl становится stdin следующей команды, тело ответа не печатается на экран целиком
sed
потоковый редактор; здесь работает только фильтром строк и ничего не правит
-n
отключает автоматическую печать каждой строки: без него ответ вывелся бы дважды — сам по себе и по команде p
"1,40p"
адрес «строки 1–40» и команда p — напечатать их. Полный ответ длиннее: дальше идут закрывающие теги таблицы и документа
кавычки
оболочка передаёт 1,40p одним аргументом; запятая и p не специальные символы shell, но кавычки защищают всё sed-выражение целиком
Что произойдёт
Запрос проходит dnsmasq → nginx → PHP-FPM → роутер → UserController@index → Blade. На экран попадают первые 40 строк готового HTML (1275 байт в захвате), замер — 433 мс на весь путь от curl до собранной страницы. Код возврата 0.
На что смотреть
В ответе нет ни одного PHP-тега и ни одной директивы Blade: массив из контроллера превратился в строки таблицы. Строка 6 —
Список пользователей (секция шаблона переопределила значение по умолчанию из макета), строка 7 — подключение /css/app.css из макета, строка 18 — счётчик (3) из count($users), строки 29–33 и 34–38 — первые два
, строки 39–40 — третий, обрезанный sed. Между (строка 17) и
(строка 18) нет блока flash: условие @if (session('status')) в макете не сработало. Строка 18 начинается с отступа в 12 пробелов — Blade не выравнивает отступы за директивами.
Частые ошибки
Пустой вывод и ни одной ошибки: забыли -S, и curl промолчал о проблеме. Если dnsmasq с зоной .rs не запущен, имя не резолвится и curl завершается с кодом 6 — проверяется echo $?.
404: маршрут не зарегистрирован или в URI опечатка. Первым делом смотрим php artisan route:list --except-vendor.
500, а в storage/logs/laravel.log строка «tempnam(): file created in the system's temporary directory»: папка storage/framework/views закрыта на запись, Blade не может сохранить скомпилированный шаблон (воспроизведено в части 6 на GET /users).
500 и в теле ответа «No application encryption key has been specified.»: в .env пустой APP_KEY (воспроизведено в части 6 на GET /users).
Страница без стилей: в HTML есть <link rel="stylesheet" href="/css/app.css">, но файла нет в public/css/ — тогда 404 на статику отдаст nginx, PHP в этом запросе не участвует.
Команда: curl -sS http://project-laravel.rs/users | sed -n "1,40p".
-s убирает прогресс-бар, -S возвращает показ ошибок — вместе они дают
чистый ответ без служебных строк;
project-laravel.rs резолвится dnsmasq в 127.0.0.1 (зона .rs из части 1), запрос
принимает nginx и передаёт в PHP-FPM через сокет;
sed -n "1,40p" печатает первые 40 строк: полный ответ длиннее, дальше идут
закрывающие теги таблицы и документа;
замер команды — 433 мс за весь путь от curl до готового HTML.
Главное: в ответе нет ни одного PHP-тега. Массив из контроллера превратился в
строки таблицы, директив Blade в разметке не осталось. Значит, сработали все три
слоя: роутер нашёл маршрут, контроллер отдал данные, Blade собрал HTML.
15листайте
Шаг 8 · Проверка
Что именно мы видим в ответе
Номера строк — те, что напечатал curl выше.
Строки
Что там
Откуда взялось
6
<title>Список пользователей</title>
@section('title', …) шаблона переопределил @yield('title', 'Пользователи') из макета
между <main class="page"> (строка 17) и <h1> (строка 18) нет блока
flash — условие @if (session('status')) в макете не сработало, в сессии
пусто. Директива не оставила после себя ни комментария, ни пустого <div>;
строка 18 начинается с отступа в 12 пробелов: Blade подставил текст на место
директив, не пересобирая отступы.
Три элемента массива дали ровно три <tr> — ни одного лишнего и ни одного
потерянного. Это и есть проверка того, что @forelse отработал по массиву.
Файл routes/web.php переписан целиком: корень отправляет на /users, а /users ведёт в контроллер. Команда показывает, что роутер действительно увидел оба маршрута.
php artisan route:list --except-vendor
Токен
Зачем он и что меняет
php
запуск интерпретатора из PATH; на стенде PHP 8.3 — его мы проверили в части 1 командой php -v
artisan
файл в корне проекта: поднимает приложение и передаёт ему команду. Таблица маршрутов собирается из уже загруженного приложения, поэтому команда такая же тяжёлая, как и остальные artisan-команды (здесь 681 мс)
route:list
имя команды: напечатать зарегистрированные маршруты — HTTP-методы, URI и действие
--except-vendor
флаг-фильтр: не показывать маршруты, которые зарегистрировали пакеты и сам каркас. Без него своих двух строк не найти в длинной таблице
Что произойдёт
Печатает таблицу маршрутов приложения: две строки и итог «Showing [2] routes». Оба маршрута отвечают на GET и HEAD. Код возврата 0, время 681 мс.
На что смотреть
Колонка действия показывает, чем обслуживается адрес. «/» — замыкание, поэтому указан файл и строка routes/web.php:6. «users» — контроллер, поэтому указан класс и метод UserController@index. URI users напечатан без ведущего слэша, тогда как корень — как «/».
Частые ошибки
Адреса нет в таблице: маршрут не зарегистрирован, и запрос к нему вернёт 404. Проверка списка — первое действие при 404.
404 с подписью nginx/1.24.0 (Ubuntu) — это ответил веб-сервер, а не Laravel: запрос не дошёл до index.php, в vhost нет try_files (разобрано в части 2). У Laravel страница ошибки своя.
Опечатка в имени метода в маршруте ([UserController::class, 'indx']): строка в таблице выглядит нормально, а ошибка всплывает только в момент запроса.
/ обслуживает замыкание, поэтому в колонке действия стоит файл и строка:
routes/web.php:6. Строка 6 — это как раз
Route::get('/', fn () => redirect('/users'));.
users обслуживает контроллер, поэтому Laravel показывает класс и метод:
UserController@index. Обратите внимание: URI напечатан без ведущего слэша,
тогда как корень — как /.
GET|HEAD — один маршрут отвечает на оба метода: GET запрашивает страницу
целиком, HEAD — только заголовки.
Showing [2] routes — маршрута два, и оба наши: флаг --except-vendor прячет
маршруты, которые зарегистрировали пакеты и сам каркас.
Эта команда — карта приложения. К ней обращаются в двух случаях: когда нужно
понять, кто обрабатывает адрес, и когда адрес отвечает 404 — тогда первым делом
смотрят, есть ли он в списке вообще.
17листайте
Шаг 8 · Проверка
Как страница выглядит в браузере
http://project-laravel.rs/users
Таблица собрана Blade из PHP-массива: три строки, шапка из макета.
Адрес http://project-laravel.rs/users, окно браузера 1440×900. На снимке — тот же HTML,
который мы разобрали по строкам, только отрендеренный: шапка из макета и таблица
из трёх строк, собранная Blade из PHP-массива.
Сверьте два числа: 3 в скобках после «Пользователи» и три строки таблицы. Оба
берутся из одного источника — массива в index(). Это легко проверить руками:
допишите в массив четвёртый элемент и обновите страницу — появится четвёртая
строка, а счётчик в заголовке станет 4. Править шаблон для этого не нужно.
18листайте
Шаг 9 · Три слоя
Данные прошли через три слоя
Карта MVC из части 1 — теперь не схема, а четыре файла нашего проекта.
Путь данных: маршрут → контроллер → шаблон → ответ
Что знает каждый слой и чего он намеренно не знает:
Маршрут (routes/web.php) знает, какому адресу соответствует какое
действие, — и не знает, где данные и как выглядит страница.
Контроллер (app/Http/Controllers/UserController.php) знает, где взять
данные и какой шаблон позвать, — и не знает, как свёрстана страница.
Шаблон (resources/views/users/index.blade.php) знает, как вывести
$users, — и не знает, откуда они взялись.
Макет (resources/views/layouts/app.blade.php) знает общий каркас и меню —
и не знает, что именно будет внутри.
Буква M — модель — в этой схеме пока не участвует: её роль играет массив
внутри контроллера. В части 4 на этом месте появится модель User и запрос к
базе. Адрес /users при этом продолжит вести в UserController@index: изменится
содержимое метода, а не маршрут.
Что разделение даёт на практике:
источник данных меняется в одном месте — в контроллере; шаблон при этом
правится точечно ($user['name'] станет $user->name);
разметка меняется в макете и шаблоне, не задевая логику;
у одного класса появляется несколько действий: список, форма, сохранение —
разные методы, один контроллер.
19листайте
Шаг 10 · Предел массива
Почему массив — тупик
Страница работает, запрос занимает 433 мс. Но данные в массиве не живут:
Между запросами ничего не сохраняется. PHP-процесс обрабатывает один запрос
и завершается: переменная $users исчезает вместе с ним. Следующий запрос
снова выполняет index() с начала и получает тот же массив, что записан в
коде.
Добавить запись из формы нельзя. Даже приняв POST и дописав элемент в
массив, мы потеряем изменение вместе с процессом. Чтобы «добавить» работало,
нужно хранилище, которое переживает запрос.
Источник данных — исходный код. Чтобы изменить список, нужно править
PHP-файл. Ни другой скрипт, ни другой человек данные изменить не могут.
На объёме нечего делать. В блоке HTTP вы считали страницы через count() и
ceil() по результатам запроса к базе (lesson.http.ii.05). Массив из трёх
элементов листать незачем, а отдавать тысячу записей одним куском уже нельзя:
ни сортировки, ни фильтра, ни пагинации.
Копии разъезжаются. Когда страниц станет несколько, каждой нужен один и тот
же список. Массив, скопированный в два контроллера, — это две версии правды.
Вывод: нужен носитель, который живёт дольше запроса. Это база данных — MariaDB,
которую мы проверили в части 1.
Мост к части 4: работать с базой вы уже умеете — PDO и подготовленные запросы из
блоков «Базы данных» и «SQL-инъекции». В Laravel тот же запрос описывается на
уровне модели: не SQL-строка и не ручная привязка параметров, а вызов метода.
Под капотом останется тот же prepared statement.
20листайте
Итог
Итог части
Файл
Роль
Строк
Байт
app/Http/Controllers/UserController.php
действие index() и данные
17
619
routes/web.php
два маршрута: редирект с / и /users
8
189
resources/views/layouts/app.blade.php
каркас страницы
24
670
resources/views/users/index.blade.php
список и перебор массива
30
772
public/css/app.css
стили без сборщика
47
2783
Итого 126 строк, 5033 байта — и рабочая страница списка, проверенная тремя
способами: curl (HTML), route:list (маршруты) и снимок браузера.
Три вещи, которые стоит унести из части:
Действие — это метод класса. Маршрут хранит пару [Класс::class, 'метод'],
а не логику.
Шаблон ничего не знает о данных. Он получает переменные вторым аргументом
view() и выводит их через {{ }} с экранированием.
Макет один, страниц много.@extends + @section/@yield — это
наследование, которое вы уже писали руками на классах.
21листайте
Итог
Проверьте себя
Почему make:controller кладёт файл именно в app/Http/Controllers/, и что
сломается, если переложить его в другой каталог, не меняя namespace?
Что означает запись [UserController::class, 'index'] в маршруте и чем она
лучше строки 'App\Http\Controllers\UserController@index'?
Как view('users.index', ['users' => $users]) превращается в конкретный файл
на диске и в переменную внутри шаблона? Что увидит шаблон, если переменную в
контроллере переименовать в $rows?
Чем @forelse отличается от @foreach и при каком условии сработает ветка
@empty?
В макете есть блок <div class="flash flash--ok">, но в HTML, который вернул
curl, его нет. Почему?
22листайте
Итог
Ответы
Автозагрузка Composer работает по PSR-4: пространству имён App\ соответствует
каталог app/. При переносе файла без правки namespace путь перестанет
совпадать с именем класса, автозагрузчик не найдёт файл, и PHP сообщит, что
класс не найден.
Массив «класс + имя метода»: роутер создаёт объект контроллера и вызывает
index(). ::class разворачивается в полную строку на этапе разбора кода,
поэтому опечатку видно сразу, а переименование класса проходит по проекту;
строка '…@index' ошибется молча и только в момент запроса.
Точки — разделители каталогов, корень resources/views/, расширение
.blade.php добавляет Blade: получается resources/views/users/index.blade.php.
Ключ 'users' второго массива становится переменной $users в шаблоне. При
переименовании в $rows шаблон продолжит ждать $users — имя берётся из
ключа, а не из имени переменной.
@forelse — это foreach с проверкой на пустоту: ветка @empty выполняется,
если перебирать нечего, то есть при пустом массиве.
Блок обёрнут в @if (session('status')). В сессии нет значения status,
условие ложно, и Blade не выводит ничего — ни <div>, ни комментария.
23листайте
Что дальше
Что дальше
Часть 4 — база данных, миграции и Eloquent:
подключим MariaDB через .env и разберём, чем конфиг отличается от .env;
создадим таблицу usersмиграцией — файлом, который описывает схему и умеет
откатываться;
наполним таблицу сидером, чтобы не вводить записи руками;
заменим массив в index() на запрос модели User и посмотрим, какой SQL при
этом выполняется;
добавим пагинацию — уже на настоящих записях.
Три слоя, которые вы собрали в этой части, останутся на месте. Адрес /users
продолжит вести в UserController@index, шаблон поменяет способ обращения к полю
и счётчик, контроллер — источник данных. Это и есть проверка того, что разделение
ответственности настоящее, а не нарисованное на схеме.