Laravel · Часть 2 — Структура проекта, первый запрос и nginx
1 / 1
lesson.laravel · часть 2

Часть 2 — Структура проекта, первый запрос и nginx

Что сделаем. Разложим по полочкам проект ~/recoursia/project-laravel, установленный в части 1: что в каком каталоге лежит и кто туда пишет. Прочитаем точку входа public/index.php и поймём, как запрос вообще попадает в наш код. Напишем первый маршрут и первый шаблон Blade, получим настоящий HTTP-ответ и измерим его. Затем уйдём со встроенного сервера на nginx: соберём vhost для домена project-laravel.rs, по дороге сделаем две типовые ошибки и разберём, почему они дают 403 и 404.

Что понадобится. Проект Laravel из части 1; стек из блока Linux — nginx 1.24, PHP-FPM 8.3 и локальная зона .rs через dnsmasq (lesson.linux.05–lesson.linux.06); sudo для правки /etc/nginx; терминал и редактор.

Все команды, файлы и снимки в этой части — настоящие захваты прогона на стенде mint (Linux Mint 22 / Ubuntu 24.04, пользователь student). Ничего не сочинено: что напечатала машина, то и на слайде.

1листайте
Структура · карта

Карта каталогов: что где лежит

Проект project-laravel — это не «одна папка со скриптами», а фиксированная структура: у каждого каталога своя роль, и Laravel ищет файлы именно там, где договорились. Вот вся карта; дальше мы зайдём в каждый каталог отдельно.

project-laravel/ # ~/recoursia/project-laravel — корень проекта├── app/ # НАШ код│   ├── Http/Controllers/Controller.php # базовый контроллер (пригодится в части 3)│   ├── Models/User.php # модель пользователя│   └── Providers/AppServiceProvider.php├── bootstrap/ # как собирается приложение│   ├── app.php # маршруты, middleware, обработка ошибок│   ├── providers.php # список провайдеров│   └── cache/ # кэш сборки, нужны права на запись├── config/ # 10 файлов конфигурации├── database/│   ├── migrations/ # 3 миграции: users, cache, jobs│   ├── factories/UserFactory.php # генератор тестовых строк│   ├── seeders/DatabaseSeeder.php # наполнение базы│   └── database.sqlite # файл базы по умолчанию├── public/ # ЕДИНСТВЕННАЯ папка, которую видит nginx│   ├── index.php # точка входа: сюда приходит любой запрос│   ├── .htaccess # настройка для Apache, nginx её не читает│   ├── favicon.ico # 0 байт│   └── robots.txt # 24 байта├── resources/│   ├── views/welcome.blade.php # шаблоны Blade│   ├── css/app.css # исходники для сборщика Vite│   └── js/app.js├── routes/│   ├── web.php # маршруты сайта (HTTP)│   └── console.php # маршруты консоли├── storage/ # всё, что приложение пишет само│   ├── app/ # private/ и public/ — файлы и загрузки│   ├── framework/ # cache/, sessions/, testing/, views/│   └── logs/ # логи приложения├── vendor/ # пакеты Composer — поставили в части 1├── artisan # консольный вход в приложение├── package.json # сборка ассетов (Vite)├── .env # секреты и настройки этой машины└── .gitignore
Откуда эта карта

Каталоги vendor/, composer.json, composer.lock и .env появились в части 1 при установке — здесь они показаны для полноты. Остальное взято из захватов этой части: tree и ls — для каталогов, файловые захваты — для bootstrap/app.php, bootstrap/providers.php, artisan, package.json и .gitignore. Ни одного выдуманного пути: vendor/autoload.php подключает public/index.php, а .env и /vendor перечислены в .gitignore проекта.

2листайте
Структура · app/

app/ — здесь живёт наш код

student@mint: ~/recoursia/project-laravel456 мс
student@mint:~/recoursia/project-laravel$
Каталог app — наш код — Здесь живут модели, контроллеры и провайдеры.
Разбор команды

Смотрим, что каркас оставил нам под собственный код: какие подкаталоги есть в app/ и какие файлы в них лежат.

tree app -L 3 --dirsfirst
ТокенЗачем он и что меняет
treeутилита, печатающая содержимое каталогов деревом: вложенность видна по отступам и линиям, в отличие от ls -R
appаргумент — корень обхода. Без него tree напечатал бы весь проект, включая vendor/ с тысячами файлов пакетов
-L 3ограничить глубину тремя уровнями: app → Http → Controllers → файл. Глубже в app/ ничего нет, лишний вывод не нужен
--dirsfirstпечатать каталоги раньше файлов: сначала видна структура, потом содержимое

Что произойдёт

Печатает дерево app/: подкаталоги Http (с вложенным Controllers), Models, Providers и три файла — Controller.php, User.php, AppServiceProvider.php. Итог: «5 directories, 3 files».

На что смотреть

Итоговая строка: каталогов пять, хотя «своих» подкаталогов три — посчитаны сам app/ и промежуточный Http/. Файлов ровно три, и все три мы разберём на этом слайде.

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

  • tree: command not found — в Ubuntu/Mint пакет не входит в базовую установку: sudo apt install tree.
  • Без -L вывод уходит вглубь на все уровни — на каталоге проекта это тысячи строк из vendor/.

Три подкаталога и три файла — вот и всё, что каркас отдаёт нам «на откуп». Итоговая строка tree говорит «5 directories, 3 files»: каталогов пять, потому что посчитаны сам app/ и промежуточный Http/.

  • Models/User.php — модель. В блоке «Базы данных» вы писали запросы через PDO руками: prepare(), маркеры, execute(), fetchAll(). Модель — это класс, который берёт эту работу на себя: одна запись таблицы становится объектом, а обращение к свойству — значением колонки. Подробно — в части 4.
  • Http/Controllers/Controller.php — базовый контроллер. Пока он пустой и ничем не используется: в этой части маршруты будут вызывать обычные замыкания. Свои контроллеры мы добавим в части 3.
  • Providers/AppServiceProvider.php — провайдер: класс, в котором приложение «регистрирует» свои сервисы. Он упомянут в bootstrap/providers.php, и мы увидим его на следующем же слайде про сборку приложения.

Почему код лежит отдельно от шаблонов и конфигов? Потому что у каталогов разные права и разный жизненный цикл: app/ меняете вы, storage/ пишет само приложение, а public/ видит веб-сервер.

3листайте
Структура · database/ и resources/

database/ и resources/: схема данных и шаблоны

student@mint: ~/recoursia/project-laravel500 мс
student@mint:~/recoursia/project-laravel$
Каталог database — Миграции, сидеры, фабрики и файл SQLite-базы.
Разбор команды

Проверяем, что приехало с каркасом в database/: описание схемы, наполнение и файл базы.

tree database -L 2 --dirsfirst
ТокенЗачем он и что меняет
treeвывод деревом — видно и подкаталоги, и файлы внутри них
databaseкорень обхода: каталог проекта, а не сервер СУБД
-L 2глубина 2 уровня: сам database и его подкаталоги. Файлы внутри migrations/ при этом ещё видны
--dirsfirstкаталоги раньше файлов

Что произойдёт

Видны factories/ (1 файл), migrations/ (3 файла), seeders/ (1 файл) и файл database.sqlite. Итог: «4 directories, 6 files».

На что смотреть

database.sqlite — база по умолчанию: отдельный сервер поднимать не нужно, это один файл. Три миграции каркаса: create_users_table, create_cache_table, create_jobs_table. Арифметика итога: 3 миграции + UserFactory.php + DatabaseSeeder.php + database.sqlite = 6 файлов.

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

  • Без -L дерево уходит вглубь на все уровни: здесь разница невелика, но на корне проекта вывод стал бы огромным из-за vendor/.
  • Файлы из database/ наружу не отдаются: их нет в public/. Это код, а не контент сайта.

migrations/ — описание таблиц в коде. В блоке «Базы данных» вы создавали таблицу rest в PHPMyAdmin руками: поля, varchar(255), auto_increment, первичный ключ. Миграция делает то же самое, но файлом, который лежит в git и выполняется командой php artisan migrate. Отсюда — воспроизводимость: у коллеги и на сервере получится та же схема, а не «как у меня».

seeders/ — наполнение базы данными (как ваш truncate + вставка 42 ресторанов циклом, только оформленная команда). factories/ — генератор правдоподобных тестовых строк.

database.sqlite — файл базы SQLite, который каркас использует по умолчанию: отдельный сервер поднимать не нужно. В части 4 переключимся на MariaDB.

student@mint: ~/recoursia/project-laravel411 мс
student@mint:~/recoursia/project-laravel$
Каталог resources — Шаблоны Blade, исходники css/js.
Разбор команды

Смотрим, где живут шаблоны и исходники ассетов.

tree resources -L 3 --dirsfirst
ТокенЗачем он и что меняет
treeвывод деревом
resourcesкорень обхода
-L 3три уровня: достаточно, чтобы увидеть файлы внутри css/, js/ и views/
--dirsfirstкаталоги раньше файлов

Что произойдёт

Три подкаталога — css/, js/, views/ — и три файла: app.css, app.js, welcome.blade.php. Итог: «4 directories, 3 files».

На что смотреть

views/welcome.blade.php — шаблон стартовой страницы: именно его соберёт первый запрос. css/ и js/ — исходники для сборщика Vite, браузеру они не отдаются: наружу идёт результат сборки из public/build/.

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

  • Файлов из resources/ в public/ нет, поэтому «страница без стилей» здесь — не проблема прав, а отсутствие сборки Vite (проверяем её отдельно).

views/welcome.blade.php — шаблон стартовой страницы. Blade — это PHP-файл с короткими метками вроде {{ $name }}; Laravel превращает его в обычный PHP и уже его выполняет. Свои шаблоны положим сюда же в этой и в третьей части.

css/app.css и js/app.js — исходники для сборщика Vite, а не то, что отдаёт nginx. Обратите внимание на разницу: файлы из resources/ наружу не видны, браузеру отдаётся результат сборки из public/build/. Его здесь нет — и это нормально, обсудим на слайде про Vite.

4листайте
Структура · storage/

storage/ — всё, что приложение пишет само

student@mint: ~/recoursia/project-laravel424 мс
student@mint:~/recoursia/project-laravel$
Каталог storage — Всё, что приложение пишет само: кэш, сессии, логи, загрузки.
Разбор команды

Смотрим каталог, в который приложение пишет само: кэш, сессии, логи, загрузки.

tree storage -L 3 --dirsfirst
ТокенЗачем он и что меняет
treeвывод деревом
storageкорень обхода
-L 3три уровня: видно framework/cache/data и framework/cache/locks, которые на глубине 2 не показались бы
--dirsfirstкаталоги раньше файлов

Что произойдёт

Дерево из 12 каталогов и 0 файлов: app/private, app/public, framework/{cache,sessions,testing,views}, logs. Итог: «12 directories, 0 files».

На что смотреть

Файлов ноль — на момент съёмки Laravel ещё ни разу не ответил на запрос. Запомните два каталога: framework/views/ (сюда попадут скомпилированные шаблоны) и logs/ (первое место, куда смотреть при ошибке 500).

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

  • «0 files» — норма до первого запроса, а не признак поломки.
  • Содержимое storage/ — кэш и данные, в git ему не место; второй такой каталог — bootstrap/cache/.

Это каталог, который приложение изменяет во время работы (второй такой — bootstrap/cache/, он в таблице ниже). Сейчас здесь 12 каталогов и 0 файлов: на момент съёмки Laravel ещё ни разу не ответил на запрос.

Каталог Что там появляется
framework/views/ скомпилированные шаблоны Blade (увидим в этой части)
framework/cache/data/ кэш приложения
framework/sessions/ сессии, если драйвер — файлы
framework/testing/ артефакты тестов (часть 6)
logs/ лог приложения — первое место, куда смотреть при ошибке 500
app/private/, app/public/ файлы и загрузки пользователей
bootstrap/cache/ (рядом) кэш конфигурации и списка сервисов

Отсюда два следствия, которые мы ещё проверим: каталогу нужны права на запись для того пользователя, от которого работает PHP-FPM (слайд про права), и его содержимое не должно попадать в git — это кэш, а не код.

5листайте
Структура · public/

public/ — единственная дверь наружу

student@mint: ~/recoursia/project-laravel452 мс
student@mint:~/recoursia/project-laravel$
Каталог public — единственная дверь наружу
Разбор команды

Смотрим единственную папку, которую видит веб-сервер.

ls -la public
ТокенЗачем он и что меняет
lsсписок содержимого каталога
-laдва флага слитно: -l — длинный формат (права, владелец, размер в байтах, дата), -a — показывать и скрытые файлы, имена которых начинаются с точки. Без -a мы не увидели бы .htaccess, без -l — что favicon.ico пустой
publicаргумент — каталог, содержимое которого печатаем

Что произойдёт

Четыре файла: .htaccess (740 байт), favicon.ico (0), index.php (543), robots.txt (24). Каталога build/ нет — ассеты не собраны.

На что смотреть

index.php — точка входа: через FastCGI nginx передаёт в PHP-FPM именно его. .htaccess — настройка для Apache, nginx её не читает. Права -rw-rw-rw- — особенность этого стенда, а не требование Laravel.

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

  • Строка total 20 — это занятые блоки, а не сумма байт файлов.
  • Без -a файлов покажется три, и возникнет ложный вывод, что скрытых настроек в public/ нет.

Четыре файла — и это весь «сайт», насколько его видит nginx:

  • index.php (543 байта) — точка входа. Через FastCGI веб-сервер передаёт в PHP-FPM именно его; разбор запроса начнётся внутри. Это тот самый index.php, путь к которому вы настраивали в lesson.linux.06 и разбирали в lesson.http.i.01: процесс PHP выполняет файл, вывод копится в буфере, буфер становится телом ответа.
  • .htaccess (740 байт) — файл настроек для Apache. Мы работаем с nginx, он .htaccess не читает; файл лежит «на всякий случай» — для тех, кто поставит проект на Apache.
  • favicon.ico — 0 байт: заглушка, чтобы браузер не получал 404 на иконку вкладки.
  • robots.txt — 24 байта, указание для поисковых роботов.

Здесь же, в public/, появится каталог build/ — если собрать ассеты Vite. И сюда же в части 3 мы положим свой css/app.css, чтобы подключить его обычным <link> без сборщика.

6листайте
Структура · принцип

Почему корень сайта — public/, а не папка проекта

Правило, из которого вырастает вся настройка nginx в этой части:

Веб-сервер видит только public/. Всё остальное — .env, vendor/, storage/, config/, app/, database/ — лежит выше корня сайта и по HTTP не отдаётся вообще.

Это не косметика, а защита. В .env живут ключ приложения APP_KEY и доступ к базе: кто получит этот файл, тот сможет расшифровать куки и прочитать вашу базу. Пока корень сайта — public/, файл ~/recoursia/project-laravel/.env физически не попадает в область, из которой nginx раздаёт файлы.

.gitignore
1*.log2.DS_Store3.env4.env.backup5.env.production6.phpactor.json7.phpunit.result.cache8/.codex9/.cursor/10/.idea11/.nova12/.phpunit.cache13/.vscode14/.zed15/auth.json16/node_modules17/public/build18/public/fonts-manifest.dev.json19/public/hot20/public/storage21/storage/*.key22/storage/pail23/vendor24_ide_helper.php25Homestead.json26Homestead.yaml27Thumbs.db

.env, /vendor, /node_modules и /public/build в игноре — и это логично:

  • .env — секреты конкретной машины: на вашем ноутбуке и на сервере они разные;
  • /vendor — пакеты Composer, которые восстанавливаются одной командой composer install из composer.lock (мы ставили их в части 1);
  • /public/build — результат сборки ассетов: его пересобирают, а не хранят.
Зачем второй рубеж

Корень сайта в public/ — первый рубеж. В vhost мы добавим ещё и location ~ /\. { deny all; } — запрет любых скрытых файлов. Зачем, если root уже их спрятал? Затем, что root иногда указывают неверно: именно такую ошибку мы сделаем намеренно через несколько слайдов. В конце части проверим, что запрос /.env возвращает 403.

7листайте
Первый запрос · точка входа

public/index.php: с чего начинается любой запрос

public/index.php
1<?php23use Illuminate\Foundation\Application;4use Illuminate\Http\Request;56define('LARAVEL_START', microtime(true));78// Determine if the application is in maintenance mode...9if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {10 require $maintenance;11}1213// Register the Composer autoloader...14require __DIR__.'/../vendor/autoload.php';1516// Bootstrap Laravel and handle the request...17/** @var Application $app */18$app = require_once __DIR__.'/../bootstrap/app.php';1920$app->handleRequest(Request::capture());

Строка 6. LARAVEL_START — момент старта в микросекундах. Позже по нему фреймворк считает, сколько длилась обработка запроса.

Строки 8–11. Режим обслуживания: если существует storage/framework/maintenance.php, выполняется он, а дальнейший код не запускается. Файл появляется по команде php artisan down (она есть в списке команд artisan) — и сайт начинает отвечать заглушкой «мы на обслуживании», пока вы катите обновление.

Строка 14. Подключается автозагрузчик Composer. Именно поэтому в проекте нет сотен require: класс Illuminate\... находится по правилам PSR-4 сам. Мы это видели в части 1 на примере nesbot/carbon.

Строка 18. bootstrap/app.php возвращает собранный объект приложения — $app. Файл подключается через require once: приложение создаётся один раз на запрос.

Строка 20. Всё. Request::capture() собирает запрос из того, что прислал nginx (метод, путь, заголовки, параметры, тело), handleRequest() прогоняет его через маршруты и middleware и отправляет ответ в буфер вывода.

Связка с курсом

Схема ровно та, что в lesson.http.i.01 и lesson.network.021: PHP-FPM запускает процесс, процесс выполняет index.php, вывод копится в буфере, буфер отдаётся nginx и уезжает браузеру. Laravel не меняет этот путь — он заполняет середину, которую вы раньше писали руками.

8листайте
Первый запрос · сборка

bootstrap/app.php: как собирается приложение

bootstrap/app.phpphp
1<?php23use Illuminate\Foundation\Application;4use Illuminate\Foundation\Configuration\Exceptions;5use Illuminate\Foundation\Configuration\Middleware;6use Illuminate\Http\Request;78return Application::configure(basePath: dirname(__DIR__))9 ->withRouting(10 web: __DIR__.'/../routes/web.php',11 commands: __DIR__.'/../routes/console.php',12 health: '/up',13 )14 ->withMiddleware(function (Middleware $middleware): void {15 //16 })17 ->withExceptions(function (Exceptions $exceptions): void {18 $exceptions->shouldRenderJsonWhen(19 fn (Request $request) => $request->is('api/*') || $request->expectsJson(),20 );21 })->create();
bootstrap/providers.phpphp
<?phpuse App\Providers\AppServiceProvider;return [ AppServiceProvider::class,];

withRouting — где искать маршруты. web: указывает на routes/web.php (HTTP-запросы браузера), commands: — на routes/console.php (консольные команды). health: '/up' добавляет служебный маршрут-проверку «приложение живо» — им удобно мониторить деплой. Регистрирует его сам каркас, поэтому в нашем route:list --except-vendor (слайд 30) его не будет.

withMiddleware — пустая заглушка //. Middleware — это «фильтры», через которые проходит запрос до маршрута и ответ после него. Заполним её, когда понадобится своё поведение между запросом и маршрутом.

withExceptions — как отдавать ошибки. Здесь настроено: если путь начинается с api/ или клиент ждёт JSON, ошибку отдавать JSON-ом, а не HTML-страницей. Для нашего сайта с HTML-страницами поведение остаётся обычным.

bootstrap/providers.php — список провайдеров приложения: один элемент, AppServiceProvider::class — тот самый файл из app/Providers/, который мы видели в дереве каталогов.

9листайте
Первый запрос · маршруты

routes/web.php: что такое маршрут

student@mint: ~/recoursia/project-laravel385 мс
student@mint:~/recoursia/project-laravel$
Каталог routes
Разбор команды

Проверяем, где объявлены маршруты сайта и консоли.

ls -la routes
ТокенЗачем он и что меняет
lsсписок содержимого каталога
-laдва флага слитно: -l — длинный формат, здесь интересны размеры файлов; -a — скрытые файлы (привычный флаг, хотя в routes/ их нет)
routesкаталог маршрутов приложения

Что произойдёт

Два файла: console.php (210 байт) и web.php (108 байт).

На что смотреть

Нас интересует web.php — маршруты HTTP. В нём строки 5 и 9, на которых объявлены оба наших маршрута; route:list в конце части это подтвердит.

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

  • Строки . и .. в выводе -a — сам каталог и его родитель: они есть всегда и к содержимому отношения не имеют.

В routes/ два файла: web.php — маршруты сайта, console.php — маршруты консоли (108 байт у web.php, 210 — у console.php). Нас интересует первый.

routes/web.php
1<?php23use Illuminate\Support\Facades\Route;45Route::get('/', function () {6 return view('welcome');7});

Маршрут — это правило «такой-то метод + такой-то путь = такое-то действие». Ниже — весь путь запроса GET / в нашем проекте, от браузера до ответа; номера строк — из route:list выше.

метод + путьGET / — браузер или curlGET /routes/web.php:5 — Route::get('/', …)routes/web.php:5Замыкание — return view('welcome')Замыканиеwelcome.blade.php — resources/views/welcome.blade.phpОтвет — 200, HTML из буфераОтветметод + путьGET / — браузер или curlGET /routes/web.php:5 — Route::get('/', …)routes/web.php:5Замыкание — return view('welcome')Замыканиеwelcome.blade.php — resources/views/welcome.blade.phpОтвет — 200, HTML из буфераОтвет
Запрос → строка таблицы маршрутов → действие → шаблон → ответ
Связка с курсом

В lesson.http.i.02 вы разбирали путь запроса и query string руками: $_GET['page'], $_GET['sort'], if/else на «какая страница нужна». В lesson.http.i.04–i.05 так выросла пагинация и сортировка списка рейсов. Роутер Laravel делает ту же работу: сравнивает метод и путь с таблицей маршрутов и вызывает нужное действие — только таблица объявлена заранее и лежит в одном файле, а не размазана по if-ам.

Параметры в фигурных скобках ({name}) — то, что в «чистом PHP» приезжало в $_GET: часть адреса, которую маршрут ловит и передаёт в замыкание аргументом. Проверим это через несколько слайдов.

10листайте
Структура · config/

config/: настройки и чем они отличаются от .env

student@mint: ~/recoursia/project-laravel436 мс
student@mint:~/recoursia/project-laravel$
Каталог config
Разбор команды

Смотрим, из каких файлов состоит конфигурация приложения.

ls config
ТокенЗачем он и что меняет
lsсписок содержимого каталога
configаргумент-каталог. Флагов нет намеренно: нужны только имена файлов, а права и размеры ничего не объясняют — без -l вывод короче и читается одним списком

Что произойдёт

Десять файлов — по одному на подсистему: app.php, auth.php, cache.php, database.php, filesystems.php, logging.php, mail.php, queue.php, services.php, session.php.

На что смотреть

Каждый файл — обычный PHP-массив, а значения в нём читаются через env('КЛЮЧ', 'значение по умолчанию'), то есть берутся из .env этой машины. config/ общий и лежит в git, .env — свой у каждой машины и в git не попадает.

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

  • Если конфигурацию закэшировали (php artisan optimize — видим его в списке команд), правка файлов config/ не применится, пока кэш не сбросили; в части 6 разбираем этот режим.

Десять файлов — по одному на подсистему: приложение (app.php), аутентификация (auth.php), кэш, база данных, файловые хранилища, логирование, почта, очереди, внешние сервисы, сессии.

Каждый файл — просто PHP-массив. Laravel читает их при старте, а значение из кода достают как config('app.name').

config/app.phpphp
1<?php23return [45 /*6 |--------------------------------------------------------------------------7 | Application Name8 |--------------------------------------------------------------------------9 |10 | This value is the name of your application, which will be used when the11 | framework needs to place the application's name in a notification or12 | other UI elements where an application name needs to be displayed.13 |14 */1516 'name' => env('APP_NAME', 'Laravel'),1718 /*19 |--------------------------------------------------------------------------20 | Application Environment21 |--------------------------------------------------------------------------22 |23 | This value determines the "environment" your application is currently24 | running in. This may determine how you prefer to configure various25 | services the application utilizes. Set this in your ".env" file.26 |27 */2829 'env' => env('APP_ENV', 'production'),3031 /*32 |--------------------------------------------------------------------------33 | Application Debug Mode34 |--------------------------------------------------------------------------35 |36 | When your application is in debug mode, detailed error messages with37 | stack traces will be shown on every error that occurs within your38 | application. If disabled, a simple generic error page is shown.39 |40 */4142 'debug' => (bool) env('APP_DEBUG', false),4344 /*45 |--------------------------------------------------------------------------46 | Application URL47 |--------------------------------------------------------------------------48 |49 | This URL is used by the console to properly generate URLs when using50 | the Artisan command line tool. You should set this to the root of51 | the application so that it's available within Artisan commands.52 |53 */5455 'url' => env('APP_URL', 'http://localhost'),5657 /*58 |--------------------------------------------------------------------------59 | Application Timezone60 |--------------------------------------------------------------------------61 |62 | Here you may specify the default timezone for your application, which63 | will be used by the PHP date and date-time functions. The timezone64 | is set to "UTC" by default as it is suitable for most use cases.65 |66 */6768 'timezone' => 'UTC',6970 /*71 |--------------------------------------------------------------------------72 | Application Locale Configuration73 |--------------------------------------------------------------------------74 |75 | The application locale determines the default locale that will be used76 | by Laravel's translation / localization methods. This option can be77 | set to any locale for which you plan to have translation strings.78 |79 */8081 'locale' => env('APP_LOCALE', 'en'),8283 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),8485 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),8687 /*88 |--------------------------------------------------------------------------89 | Encryption Key90 |--------------------------------------------------------------------------91 |92 | This key is utilized by Laravel's encryption services and should be set93 | to a random, 32 character string to ensure that all encrypted values94 | are secure. You should do this prior to deploying the application.95 |96 */9798 'cipher' => 'AES-256-CBC',99100 'key' => env('APP_KEY'),101102 'previous_keys' => [103 ...array_filter(104 explode(',', (string) env('APP_PREVIOUS_KEYS', ''))105 ),106 ],107108 /*109 |--------------------------------------------------------------------------110 | Maintenance Mode Driver111 |--------------------------------------------------------------------------112 |113 | These configuration options determine the driver used to determine and114 | manage Laravel's "maintenance mode" status. The "cache" driver will115 | allow maintenance mode to be controlled across multiple machines.116 |117 | Supported drivers: "file", "cache", "array"118 |119 */120121 'maintenance' => [122 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),123 'store' => env('APP_MAINTENANCE_STORE', 'database'),124 ],125126];

Файл на 126 строк — блок прокручивается. Подсвечены строки 16, 29, 42, 68, 98 и 100; между ними длинные комментарии-баннеры, их можно не читать.

Конфиг — это не .env. Смотрите на подсвеченные строки: почти каждое значение читается через env('КЛЮЧ', 'значение по умолчанию'). Первый аргумент — ключ из .env этой машины, второй — запасной вариант, если ключа не дали.

Зачем два слоя? .env у каждого свой и в git не попадает, а config/ — общий и в git попадает. Поэтому код не знает ни паролей, ни домена: он знает имена настроек, а значения подставляются при старте.

Значения по умолчанию подобраны так, что приложение остаётся в безопасном режиме, даже если в .env что-то забыли:

  • 'env' => env('APP_ENV', 'production') — если не сказано иное, считаем сервер боевым;
  • 'debug' => (bool) env('APP_DEBUG', false) — показ деталей ошибок выключен по умолчанию. Включённый APP_DEBUG на бою — это стектрейсы и переменные окружения прямо в ответе;
  • 'timezone' => 'UTC' (строка 68) и 'cipher' => 'AES-256-CBC' (строка 98) — значения без env(): они одинаковы на всех машинах и в .env не вынесены;
  • 'key' => env('APP_KEY') (строка 100) — и без значения по умолчанию. Это тот ключ, который сгенерировала установка в части 1 («Application key set successfully»); им шифруются, например, куки — мы их увидим в заголовках ответа.
11листайте
Структура · права

Кто пишет в storage/ и что будет без прав

student@mint: ~/recoursia/project-laravel464 мс
student@mint:~/recoursia/project-laravel$
Права на запись — Пишет приложение от имени пользователя php-fpm (у нас — student, см. lesson.linux.06).
Разбор команды

Проверяем, сможет ли процесс, обслуживающий запрос, писать туда, куда приложение пишет само.

ls -ld storage storage/framework storage/logs bootstrap/cache; id
ТокенЗачем он и что меняет
lsсписок содержимого — здесь он заменяется сведениями о самих объектах
-ldдва флага слитно: -l — длинный формат, нужны права, владелец и группа; -d — не заходить внутрь каталогов. Без -d ls напечатал бы содержимое каждого из четырёх каталогов, а важны права самого каталога
storage storage/framework storage/logs bootstrap/cacheчетыре аргумента подряд: ls принимает список путей и печатает по строке на каждый — одна команда вместо четырёх
;разделитель команд shell: вторая команда выполняется безусловно, независимо от кода возврата первой
idпечатает uid, gid и группы текущего пользователя — чтобы сравнить их с владельцем каталогов

Что произойдёт

Все четыре каталога — drwxrwxrwx, владелец и группа student. id подтверждает: uid=1002(student) gid=1002(student) groups=1002(student).

На что смотреть

d в начале строки прав — каталог, дальше три тройки rwx для владельца, группы и остальных, то есть 777. Владелец каталогов и пользователь веб-стека совпадают (настройка из lesson.linux.06), поэтому запись проходит.

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

  • На боевом сервере PHP-FPM обычно работает от www-data, а файлы принадлежат другому пользователю: приложение не сможет ни скомпилировать шаблон, ни записать лог — и вы получите 500. В части 6 это снято по-настоящему: local.ERROR: tempnam(): file created in the system's temporary directory.
  • 777 — не требование Laravel, а особенность стенда: даже index.php в public/ лежит как -rw-rw-rw-. Правильная настройка — владелец = пользователь веб-стека и минимум нужных прав.
  • Проверять нужно не только storage/: bootstrap/cache/ приложение тоже пишет, и его забывают чаще всего — тогда не сохраняется кэш конфигурации и сервисов.

Что мы видим. Четыре каталога, которые приложению нужны на запись: storage, storage/framework, storage/logs и bootstrap/cache. Права у всех — drwxrwxrwx: d — каталог, дальше три тройки rwx для владельца, группы и остальных, то есть 777 из lesson.programs.02. Владелец и группа — student, и id подтверждает: uid=1002(student) gid=1002(student).

Почему это важно. Пишет в эти каталоги не вы из терминала, а процесс PHP-FPM, который обслуживает запрос. В lesson.linux.06 вы настраивали nginx и PHP-FPM на работу от своего пользователя — поэтому здесь владелец каталогов и пользователь веб-стека совпадают, и запись проходит.

На боевом сервере PHP-FPM обычно работает от www-data, а файлы принадлежат другому пользователю. Тогда приложение не сможет ни скомпилировать шаблон, ни записать лог — и вы получите ошибку 500. Разбор такого случая — в части 6.

Почему права такие широкие

777 — не требование Laravel, а особенность этой машины: на стенде все файлы созданы с широкими правами. Взгляните на вывод ls -la public выше — даже index.php лежит как -rw-rw-rw- (666). Правильная настройка — это «владелец = пользователь веб-стека, права = минимум нужного», а не 777 всем.

12листайте
Структура · database/

Миграции из коробки: три таблицы одним файлом

student@mint: ~/recoursia/project-laravel385 мс
student@mint:~/recoursia/project-laravel$
Миграции, которые приехали с каркасом
Разбор команды

Смотрим миграции, которые приехали с каркасом, и порядок, в котором они применятся.

ls -la database/migrations
ТокенЗачем он и что меняет
lsсписок содержимого каталога
-laдва флага слитно: -l — длинный формат (имена миграций длинные, а размеры показывают, что users — самая маленькая из трёх: 1473 байта против 1883 у jobs); -a — скрытые файлы, которых в этом каталоге нет
database/migrationsпуть через слэш — аргументом ls может быть любой путь, не только каталог первого уровня

Что произойдёт

Три файла: 0001_01_01_000000_create_users_table.php (1473 байта), 0001_01_01_000001_create_cache_table.php (873), 0001_01_01_000002_create_jobs_table.php (1883).

На что смотреть

Имя миграции начинается с даты — это не украшение, а порядок применения: миграции выполняются по возрастанию имени. Дата 0001_01_01 у каркасных файлов гарантирует, что они отработают раньше ваших.

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

  • Переименовывать миграцию нельзя: имя задаёт порядок, и оно же попадает в список выполненных (php artisan migrate:status печатает «Migration name ... Batch / Status»).
  • Один файл может создавать несколько таблиц: 000000_create_users_table.php создаёт users, password_reset_tokens и sessions.

Три файла, и имена начинаются с даты: 0001_01_01_000000_.... Это не украшение, а порядок применения: миграции выполняются по возрастанию имени. Дата 0001_01_01 у каркасных файлов гарантирует, что они отработают раньше ваших.

Рядом лежат ..._000001_create_cache_table.php (таблица кэша) и ..._000002_create_jobs_table.php (очереди) — их мы не трогаем.

database/migrations/0001_01_01_000000_create_users_table.phpphp
1<?php23use Illuminate\Database\Migrations\Migration;4use Illuminate\Database\Schema\Blueprint;5use Illuminate\Support\Facades\Schema;67return new class extends Migration8{9 /**10 * Run the migrations.11 */12 public function up(): void13 {14 Schema::create('users', function (Blueprint $table) {15 $table->id();16 $table->string('name');17 $table->string('email')->unique();18 $table->timestamp('email_verified_at')->nullable();19 $table->string('password');20 $table->rememberToken();21 $table->timestamps();22 });2324 Schema::create('password_reset_tokens', function (Blueprint $table) {25 $table->string('email')->primary();26 $table->string('token');27 $table->timestamp('created_at')->nullable();28 });2930 Schema::create('sessions', function (Blueprint $table) {31 $table->string('id')->primary();32 $table->foreignId('user_id')->nullable()->index();33 $table->string('ip_address', 45)->nullable();34 $table->text('user_agent')->nullable();35 $table->longText('payload');36 $table->integer('last_activity')->index();37 });38 }3940 /**41 * Reverse the migrations.42 */43 public function down(): void44 {45 Schema::dropIfExists('users');46 Schema::dropIfExists('password_reset_tokens');47 Schema::dropIfExists('sessions');48 }49};

Подсвечены строки 14, 24, 30 — три вызова Schema::create(). Метод up() — строки 12–38, обратный ему down() — 43–48.

Один файл создаёт три таблицы, и все три нужны самому каркасу:

  • users — пользователи. $table->id() — это auto_increment-первичный ключ из lesson.db.03; string('email')->unique() — поле с уникальным индексом (индексы и их цену вы разбирали в lesson.db.vi.02–vi.03); rememberToken() — столбец для галочки «запомнить меня»; timestamps() — created_at и updated_at, которые Laravel заполняет сам.
  • password_reset_tokens — токены восстановления пароля: email как первичный ключ, сам токен и время создания.
  • sessions — сессии в базе: строковый id, foreignId('user_id') — внешний ключ на пользователя (связь «один ко многим» из lesson.db.v.01), ip_address длиной 45, user_agent, payload и индекс по last_activity — чтобы вычищать протухшие сессии.

down() делает обратное: Schema::dropIfExists('users') и так же для двух других таблиц. Пара «up() / down()» и есть смысл миграции: схему можно накатить и откатить, а не править руками в PHPMyAdmin. По-настоящему запустим их в части 4 — тогда же переключимся с database.sqlite на MariaDB.

13листайте
Структура · app/Models

Модель User, которая приехала с каркасом

app/Models/User.phpphp
1<?php23namespace App\Models;45// use Illuminate\Contracts\Auth\MustVerifyEmail;6use Database\Factories\UserFactory;7use Illuminate\Database\Eloquent\Attributes\Fillable;8use Illuminate\Database\Eloquent\Attributes\Hidden;9use Illuminate\Database\Eloquent\Factories\HasFactory;10use Illuminate\Foundation\Auth\User as Authenticatable;11use Illuminate\Notifications\Notifiable;1213#[Fillable(['name', 'email', 'password'])]14#[Hidden(['password', 'remember_token'])]15class User extends Authenticatable16{17 /** @use HasFactory<UserFactory> */18 use HasFactory, Notifiable;1920 /**21 * Get the attributes that should be cast.22 *23 * @return array<string, string>24 */25 protected function casts(): array26 {27 return [28 'email_verified_at' => 'datetime',29 'password' => 'hashed',30 ];31 }32}

namespace App\Models; — пространство имён совпадает с путём: App\ → app/. Это правило PSR-4 из composer.json, благодаря которому автозагрузчик находит класс без единого require (часть 1).

class User extends Authenticatable — обычное наследование из блока ООП: модель получает всё поведение «пользователя, которого можно аутентифицировать».

use HasFactory, Notifiable; — это трейты: способ подключить к классу готовое поведение, не выстраивая наследование. В курсе вы реализовывали интерфейсы (lesson.oop.polymorphism.01) — здесь идея похожая, но трейт приносит не только обещание, а готовый код: HasFactory связывает модель с database/factories/UserFactory.php, Notifiable — с отправкой уведомлений.

Атрибуты #[Fillable] и #[Hidden] — метаданные класса в квадратных скобках с #. Fillable перечисляет поля, которые можно заполнять сразу массивом (name, email, password); всё, чего в списке нет, массово не заполняется — защита от ситуации, когда посетитель добавляет в форму поле is_admin. Hidden прячет password и remember_token: при превращении модели в массив или JSON их не будет.

Метод casts() описывает, как превращать значения из базы:

  • 'email_verified_at' => 'datetime' — из строки в объекте даты;
  • 'password' => 'hashed' — при записи значение автоматически хешируется, поэтому в коде не нужно помнить «а не забыл ли я хешировать пароль».

К этой модели мы вернёмся в частях 4 и 5: регистрация, вход и форма пользователя работают ровно на этих настройках.

14листайте
Инструменты · artisan

artisan: консольный вход в то же приложение

artisanphp
1#!/usr/bin/env php2<?php34use Illuminate\Foundation\Application;5use Symfony\Component\Console\Input\ArgvInput;67define('LARAVEL_START', microtime(true));89// Register the Composer autoloader...10require __DIR__.'/vendor/autoload.php';1112// Bootstrap Laravel and handle the command...13/** @var Application $app */14$app = require_once __DIR__.'/bootstrap/app.php';1516$status = $app->handleCommand(new ArgvInput);1718exit($status);

Сравните с public/index.php — это почти он же, только для терминала: те же LARAVEL_START, тот же vendor/autoload.php, тот же bootstrap/app.php. Разница в последних строках: вместо handleRequest(Request::capture()) — handleCommand(new ArgvInput) и exit($status).

То есть запрос приходит не из браузера, а из аргументов командной строки (ArgvInput), а код возврата уходит в shell. Отсюда важное следствие: команда artisan видит то же приложение, те же конфиги и ту же базу, что и сайт.

student@mint: ~/recoursia/project-laravel797 мс
student@mint:~/recoursia/project-laravel$
  dev                       Run the dev processes  docs                      Access the Laravel documentation  down                      Put the application into maintenance / demo mode  env                       Display the current framework environment  help                      Display help for a command  inspire                   Display an inspiring quote  list                      List commands  migrate                   Run the database migrations  optimize                  Cache framework bootstrap, configuration, and metadata to increase performance  pail                      Tails the application logs  reload                    Reload running services  serve                     Serve the application on the PHP development server  test                      Run the application tests  tinker                    Interact with your application  up                        Bring the application out of maintenance mode auth  auth:clear-resets         Flush expired password reset tokens cache  cache:clear               Flush the application cache  cache:forget              Remove an item from the cache  cache:prune-stale-tags    Prune stale cache tags from the cache (Redis only) channel
Команды artisan — Полный список длиннее; здесь первые группы.
Разбор команды

Смотрим, какие команды даёт консольный вход в приложение: дальше нам понадобятся serve, tinker и route:list.

php artisan list | head -42
ТокенЗачем он и что меняет
phpисполняемый файл интерпретатора; на стенде PHP 8.3 — подставьте свою версию (php -v)
artisanPHP-скрипт в корне проекта — консольный вход в то же приложение: тот же vendor/autoload.php и тот же bootstrap/app.php, что и в public/index.php, только запрос приходит из аргументов командной строки
listаргумент скрипта artisan: команда, печатающая список всех доступных команд. Для shell это обычный аргумент, а не отдельная программа
|пайп: stdout artisan list становится stdin следующей команды
headоставляет только начало вывода
-42сколько строк оставить — 42. Полный список длиннее и не влезает в кадр; число подобрано так, чтобы вошли общие параметры и первые группы команд

Что произойдёт

Первая строка — версия каркаса Laravel Framework 13.37.0, затем общие параметры (--help, --silent, --quiet, --version, --ansi, --no-interaction, --env, --verbose) и команды: about, serve, migrate, tinker, test, optimize, pail, down/up и другие. Вывод обрезан на 42-й строке — на группе channel.

На что смотреть

Команды сгруппированы по префиксам: в кадре видны auth:clear-resets, cache:clear, cache:forget, cache:prune-stale-tags. Группы make: здесь нет — она дальше по алфавиту, и именно с неё начнётся часть 3.

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

  • Не нашли make:controller в этом выводе — он есть, просто обрезан: php artisan list | grep make:.
  • php artisan ... вне корня проекта даёт Could not open input file: artisan — скрипт ищется в текущем каталоге.
  • head закрывает пайп, не дочитав вывод; на результат это не влияет, но полагаться на код возврата такой связки в скриптах не стоит.

Первая строка вывода — версия каркаса: Laravel Framework 13.37.0. Дальше общие параметры (--help, --verbose, --env) и первые команды. Мы вывод обрезали (| head -42), полный список длиннее.

Что из показанного пригодится сразу:

  • about — сводка по приложению (мы запускали её в части 1);
  • serve — встроенный сервер разработки, следующий слайд;
  • migrate — применить миграции (часть 4);
  • tinker — интерактивная консоль внутри приложения (ей мы воспользуемся через пару слайдов);
  • down / up — режим обслуживания: тот самый storage/framework/maintenance.php из точки входа;
  • test — запуск тестов (часть 6);
  • optimize — закэшировать конфиг и метаданные (часть 6);
  • pail — смотреть лог приложения в реальном времени.

Команды сгруппированы по префиксам: auth:, cache: и дальше по алфавиту — в обрезанном выводе видны auth:clear-resets, cache:clear, cache:forget, cache:prune-stale-tags.

Есть и группа make: — команды, которые создают файлы за вас: контроллер, модель, миграцию, тест. В этом выводе их нет (мы обрезали список), но именно с них начнётся часть 3: вместо того чтобы создавать файлы руками и угадывать имена, мы скажем artisan, что нужно.

15листайте
Инструменты · ассеты

Vite и ассеты: что это и почему мы его пока не включаем

student@mint: ~/recoursia/project-laravel458 мс
student@mint:~/recoursia/project-laravel$
Как стартовая страница обходит отсутствие сборки ассетов — Laravel 13 проверяет наличие build/manifest.json и, если сборки нет, подставляет встроенный CSS.
Разбор команды

Проверяем, как стартовая страница обходится без сборки ассетов: ищем в шаблоне директивы Vite и условие на наличие манифеста.

grep -n "@vite\|@fonts\|file_exists(public_path" resources/views/welcome.blade.php
ТокенЗачем он и что меняет
grepпоиск строк, подходящих под шаблон, и печать их на экран
-nпечатать номер строки: без него не сказать, о каком месте шаблона идёт речь — в разборе мы ссылаемся на строки 9, 12 и 13
"@vite\|@fonts\|file_exists(public_path"один шаблон поиска в двойных кавычках: ищем сразу три фрагмента — директиву @vite, директиву @fonts и начало условия с file_exists(public_path. Кавычки обязательны: внутри есть |, ( и \, которые shell истолковал бы сам
\|«ИЛИ» в базовом регулярном синтаксисе grep. Обратная косая нужна именно в базовом синтаксисе: там одиночный | — обычный символ
resources/views/welcome.blade.phpфайл, в котором ищем: шаблон стартовой страницы

Что произойдёт

Три строки: 9 — @fonts, 12 — @if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot'))), 13 — @vite(['resources/css/app.css', 'resources/js/app.js']).

На что смотреть

Строка 12 проверяет, есть ли вообще сборка ассетов: манифест public/build/manifest.json либо файл public/hot запущенного dev-сервера Vite. В выводе ls -la public нет ни того, ни другого — условие ложно, @vite не выполняется, и страница обходится стилями, зашитыми в сам шаблон.

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

  • С флагом -E (расширенный синтаксис) «ИЛИ» пишется как |, а \| превращается в поиск литерала — шаблон перестанет находить нужные строки.
  • Без кавычек shell увидит | как пайп и попытается запустить команду @fonts: «command not found».
  • grep ничего не нашёл — код возврата 1, вывод пустой. Это не ошибка поиска, а факт: в шаблоне нет ни одной из трёх конструкций.

Это строки 9, 12 и 13 шаблона resources/views/welcome.blade.php. Логика простая:

  • @fonts — директива подключения шрифтов;
  • @if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot'))) — проверить, есть ли вообще сборка ассетов: либо готовый манифест в public/build/, либо запущенный dev-сервер Vite (он оставляет файл public/hot);
  • @vite(['resources/css/app.css', 'resources/js/app.js']) — подключить собранные css и js.

В public/ ни build/, ни hot нет — мы это видели в ls -la public. Значит, условие ложно, @vite не выполняется, и стартовая страница обходится теми стилями, что зашиты в сам шаблон.

package.jsonjson
1{2 "$schema": "https://www.schemastore.org/package.json",3 "private": true,4 "type": "module",5 "scripts": {6 "build": "vite build",7 "dev": "vite"8 },9 "devDependencies": {10 "@tailwindcss/vite": "^4.0.0",11 "concurrently": "^10.0.3",12 "laravel-vite-plugin": "^3.1",13 "tailwindcss": "^4.0.0",14 "vite": "^8.0.0"15 },16 "optionalDependencies": {17 "@laravel/multiplex": "^0.4.1"18 }19}

package.json описывает, чем собирают ассеты: скрипт build — это vite build, dev — просто vite (dev-сервер с горячей перезаменой). В devDependencies — vite ^8.0.0, laravel-vite-plugin ^3.1, tailwindcss ^4.0.0 и @tailwindcss/vite, concurrently ^10.0.3.

Чтобы всё это заработало, нужны Node.js и npm install, а затем npm run build. В public/ нет каталога build/ — состав public/ мы смотрели выше, там четыре файла. Значит, сборку ассетов на стенде не запускали.

Решение этого урока

Свои стили мы положим обычным файлом в public/css/app.css и подключим тегом <link> — как вы делали в блоке HTTP. Никакого сборщика и npm-пакетов: сохранили файл — и он уже отдаётся nginx как статика. Vite при этом никуда не девается: когда понадобится Tailwind или свой JavaScript, включите его ровно тем условием, которое показано слева.

Заметьте, .gitignore проекта уже готов к обоим вариантам: в нём есть /public/build (строка 17) и /public/hot (строка 19) — результат сборки в git не хранят.

16листайте
Первый ответ · serve

Первый запуск: php artisan serve

student@mint: ~/recoursia/project-laravel4876 мс
student@mint:~/recoursia/project-laravel$
>
>
>
>
>
Запускаем встроенный сервер разработки — php artisan serve поднимает встроенный сервер PHP на 127.0.0.1:8000 — только для разработки.
Разбор команды

Поднимаем встроенный сервер разработки, ждём его старта и сразу меряем первый ответ — чтобы было с чем сравнивать после переезда на nginx.

nohup php artisan serve --host=127.0.0.1 --port=8000 > /tmp/serve.log 2>&1 &
ТокенЗачем он и что меняет
nohupне завершать процесс при закрытии терминала (игнорировать SIGHUP): сервер должен пережить сессию shell, из которой его запустили
phpинтерпретатор; на стенде PHP 8.3 — подставьте свою версию (php -v)
artisanконсольный вход в приложение
serveкоманда запуска встроенного сервера разработки PHP: nginx и PHP-FPM для него не нужны
--host=127.0.0.1на каком сетевом интерфейсе слушать. Локальный адрес — с другой машины сайт не открыть; это осознанное ограничение режима разработки
--port=8000порт. 8000 — значение по умолчанию для serve, указываем явно, чтобы адрес в логе был предсказуемым
> /tmp/serve.logперенаправить stdout в файл: иначе сообщения сервера смешаются с выводом следующих команд
2>&1stderr — туда же, куда stdout: и информационный блок, и ошибки окажутся в одном файле
&запуск в фоновом режиме: терминал сразу возвращает приглашение и остаётся свободным

Что произойдёт

Сервер поднимается в фоне, весь его вывод уходит в /tmp/serve.log, а терминал готов к следующим командам.

На что смотреть

Команда ничего не печатает в терминал — это не ошибка: вывод перенаправлен в файл. Проверить, что сервер жив, можно следующим cat.

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

  • Без & сервер займёт терминал до Ctrl+C, и остальные команды захвата не выполнятся.
  • Если порт 8000 занят, serve предложит другой — и адрес в /tmp/serve.log изменится. Всегда читайте адрес из лога, а не из памяти.
sleep 4
ТокенЗачем он и что меняет
sleepпауза — ничего не делает заданное время
4длительность в секундах

Что произойдёт

Терминал ждёт 4 секунды.

На что смотреть

Пауза нужна, чтобы сервер успел начать слушать порт до первого запроса: без неё curl может получить «Connection refused».

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

  • Фиксированная пауза — грубый приём: 4 с взяты с запасом для стенда. В скриптах правильнее опрашивать порт в цикле.
cat /tmp/serve.log
ТокенЗачем он и что меняет
catнапечатать содержимое файла
/tmp/serve.logабсолютный путь к файлу, куда ушёл вывод сервера

Что произойдёт

Печатает блок INFO Server running on [http://127.0.0.1:8000] и подсказку Press Ctrl+C to stop the server.

На что смотреть

Адрес в квадратных скобках — подтверждение, что --host и --port приняты. Отдельных access.log и error.log, как у nginx, здесь нет: весь журнал — то, что сервер печатает в этот файл.

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

  • Пустой файл — сервер не стартовал: смотрите, не занят ли порт и из того ли каталога запущен artisan.
curl -sS -o /tmp/welcome.html -w "GET / -> HTTP %{http_code}, %{size_download} байт, %{time_total} с\n" http://127.0.0.1:8000/
ТокенЗачем он и что меняет
curlHTTP-клиент: показывает ровно то, что ушло по сети, без рендеринга браузером
-sSдва флага слитно: -s убирает прогресс-бар (в захвате он был бы мусором), -S возвращает видимость ошибок, которые -s подавляет вместе с ним
-o /tmp/welcome.htmlсохранить тело ответа в файл, а не печатать его: десятки килобайт HTML в терминале не нужны
-w "GET / -> HTTP %{http_code}, %{size_download} байт, %{time_total} с\n"формат строки, которую curl напечатает после запроса: именно она превращает проверку в измерение. Двойные кавычки обязательны — внутри пробелы, без них shell разбил бы строку на отдельные аргументы
%{http_code}подстановка curl: статус-код ответа
%{size_download}подстановка curl: сколько байт тела принято
%{time_total}подстановка curl: полное время запроса в секундах
\nперевод строки внутри формата — иначе метрики и следующее приглашение окажутся на одной строке
http://127.0.0.1:8000/адрес запроса — тот, на котором слушает serve

Что произойдёт

GET / -> HTTP 200, 70403 байт, 0.439242 с; тело стартовой страницы сохранено в /tmp/welcome.html.

На что смотреть

Три числа: статус, размер тела и время. 0.439242 с — первый запрос к холодному приложению: загрузка классов каркаса и компиляция шаблона. Запомните их: наш собственный маршрут отдаст 435 байт за 0.029839 с.

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

  • -o файл и -o /dev/null — разные вещи: первое сохраняет тело, второе выбрасывает. Перепутаете — и не сможете разобрать HTML следующей командой.
  • Без кавычек у -w curl получит слово GET как URL и ответит ошибкой, а не измерением.
grep -o "<title>[^<]*</title>" /tmp/welcome.html
ТокенЗачем он и что меняет
grepпоиск по содержимому файла
-oпечатать только найденный фрагмент, а не всю строку: HTML-страница лежит длинными строками, без -o вывод был бы нечитаемым
"<title>[^<]*</title>"регулярное выражение в кавычках — иначе shell принял бы < за перенаправление ввода
<title>литеральная открывающая граница искомого фрагмента
[^<]*«ноль или более любых символов, кроме <»: сам текст заголовка. [^ — отрицательный класс символов, * — повторение
</title>литеральная закрывающая граница
/tmp/welcome.htmlфайл, в котором ищем, — туда предыдущая команда сохранила тело ответа

Что произойдёт

Печатает одну строку: <title>Laravel</title>.

На что смотреть

По заголовку видно, что сервер отдал именно шаблон welcome.blade.php, а не страницу ошибки.

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

  • Без -o grep выдаст всю строку файла целиком — десятки килобайт HTML в терминале.
  • Без кавычек shell сообщит о синтаксической ошибке рядом с <: он решит, что это перенаправление.
ps -o pid,ppid,cmd -C php | head -5
ТокенЗачем он и что меняет
psснимок списка процессов (в отличие от top — одноразовый, без обновления)
-o pid,ppid,cmdкакие колонки печатать: PID, PID родителя и полную командную строку
-C phpотобрать только процессы с именем php
|пайп в следующую команду
headограничить число строк
-5не больше пяти строк — на случай, если php-процессов в системе много

Что произойдёт

Ровно один процесс: PID 49206, родитель 49199 (оболочка, которая его запустила), командная строка php artisan serve --host=127.0.0.1 --port=8000.

На что смотреть

Один процесс на все запросы — в этом главная слабость serve: ни пула воркеров PHP-FPM, ни очереди nginx. Колонка CMD подтверждает, что --host и --port дошли до процесса.

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

  • -C сравнивает имя процесса, а не аргументы: процессы php-fpm под этот фильтр не попадут.
  • Два процесса serve в выводе — сервер запустили повторно; второй не слушает порт, он занят первым.
127.0.0.1:8000curl или браузер — GET http://127.0.0.1:8000/curl илибраузерphp artisan serve — один процесс phpphp artisanservepublic/index.php — точка входаpublic/index.phpБуфер вывода — весь вывод PHPБуфер выводаОтвет — 200, HTML из буфераОтвет127.0.0.1:8000curl или браузер — GET http://127.0.0.1:8000/curl илибраузерphp artisan serve — один процесс phpphp artisanservepublic/index.php — точка входаpublic/index.phpБуфер вывода — весь вывод PHPБуфер выводаОтвет — 200, HTML из буфераОтвет
Путь запроса через встроенный сервер разработки

Что происходит. serve поднимает встроенный сервер разработки PHP на 127.0.0.1:8000 — nginx и PHP-FPM для него не нужны. Мы запустили его через nohup ... &, а вывод увели в /tmp/serve.log: иначе сервер занял бы терминал и не дал бы выполнить следующие команды. Отсюда же sleep 4 — даём серверу подняться перед первым запросом.

Что намерили. GET / → HTTP 200, 70 403 байта тела, 0.439242 с. Заголовок страницы — <title>Laravel</title>. Последний ps показывает ровно один процесс php (PID 49206, родитель 49199 — оболочка, которая его запустила).

Почему это только для разработки.

  • Адрес 127.0.0.1: сервер слушает локальный интерфейс, с другой машины сайт не открыть.
  • Один процесс на все запросы: ни пула воркеров PHP-FPM, ни очереди nginx.
  • Логи — только то, что сервер печатает в консоль (мы увели это в /tmp/serve.log). Отдельных access.log и error.log, как у nginx, а значит и привычного инструмента диагностики (lesson.programs.01), здесь нет.
  • Нет server_name: второй сайт рядом не поселить.

Всё это даёт nginx — поэтому вторую половину части мы потратим на vhost.

Про 0.44 с

Это первый запрос к холодному приложению: нужно загрузить классы каркаса и скомпилировать шаблон. Запомните цифру — дальше, на своём крошечном маршруте, мы получим ответ в разы быстрее.

17листайте
Первый ответ · стартовая страница

Так выглядит ответ каркаса

http://127.0.0.1:8000/
welcome.blade.php из каркаса. Ждём ~2.5 с: у страницы есть анимация появления (@starting-style), иначе снимок ловит полупрозрачное состояние.
welcome.blade.php из каркаса. Ждём ~2.5 с: у страницы есть анимация появления (@starting-style), иначе снимок ловит полупрозрачное состояние.

Это resources/views/welcome.blade.php из каркаса по адресу http://127.0.0.1:8000/ — те самые 70 403 байта HTML, которые намерил curl. Страница-заглушка: она нужна ровно затем, чтобы убедиться, что установка живая. Мы её заменим.

18листайте
Первый ответ · свой маршрут

Пишем свой маршрут: строка уже является ответом

routes/web.php
1<?php23use Illuminate\Support\Facades\Route;45Route::get('/', function () {6 return 'Hello, Laravel!';7});89Route::get('/hello/{name}', function (string $name) {10 return view('hello', ['name' => $name]);11});

Два изменения в routes/web.php:

  1. Корневой маршрут больше не рендерит welcome, а возвращает строку. Никакого echo, никакого HTML-каркаса: что вернуло замыкание, то и стало телом ответа. Обёртку в HTTP-ответ (статус, заголовки) Laravel берёт на себя.
  2. Добавлен маршрут /hello/{name}: фигурные скобки в пути — это параметр. Замыкание объявлено как function (string $name) — типизированный аргумент, значение берётся прямо из адреса. Внутри — view('hello', ['name' => $name]): «отрендерить шаблон hello, передав в него массив с ключом name».
student@mint: ~/recoursia/project-laravel511 мс
student@mint:~/recoursia/project-laravel$
Проверяем маршруты через curl
Разбор команды

Проверяем оба маршрута без браузера: сначала тело корневого ответа, затем метрики маршрута с параметром.

curl -sS http://127.0.0.1:8000/; echo; curl -sS -o /dev/null -w "GET /hello/студент -> HTTP %{http_code}, %{size_download} байт, %{time_total} с\n" "http://127.0.0.1:8000/hello/%D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82"
ТокенЗачем он и что меняет
curlHTTP-клиент: печатает ровно то, что пришло по сети
-sSтихо, но с сообщениями об ошибках: -s убирает прогресс-бар, -S возвращает видимость поломок
http://127.0.0.1:8000/адрес корневого маршрута. Тело короткое, поэтому печатаем его прямо в терминал, без -o
;разделитель команд: вторая выполняется независимо от результата первой
echoбез аргументов печатает пустую строку. Тело Hello, Laravel! не заканчивается переводом строки, и без echo метрики второго запроса прилипли бы к нему
curlвторой запрос — к маршруту с параметром
-sSтот же тихий режим с ошибками
-o /dev/nullтело ответа выбрасываем: нужны только цифры, а HTML мы снимем отдельной командой
-w "GET /hello/студент -> HTTP %{http_code}, %{size_download} байт, %{time_total} с\n"формат итоговой строки: своя подпись и три подстановки curl
%{http_code}статус-код ответа
%{size_download}число принятых байт тела
%{time_total}полное время запроса в секундах
\nперевод строки в конце формата
"http://127.0.0.1:8000/hello/%D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82"URL в двойных кавычках. Кириллица закодирована в percent-encoding: «студент» — семь букв, каждая по два байта UTF-8, отсюда ровно семь пар %XX

Что произойдёт

Первая строка вывода — тело корневого маршрута Hello, Laravel!, вторая — GET /hello/студент -> HTTP 200, 435 байт, 0.029839 с.

На что смотреть

Сравните два измерения: 70 403 байта и 0.439242 с у стартовой страницы против 435 байт и 0.029839 с у нашего маршрута. Разница — это в первую очередь размер ответа и стоимость большого шаблона, а не «магия фреймворка».

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

  • Ответ 404 — маршрут не совпал: сверьтесь с php artisan route:list. Connection refused — сервер не запущен или слушает другой порт.
  • Забыли -o /dev/null — тело смешается со строкой метрик, и цифры придётся искать глазами.
  • В подпись -w можно писать кириллицу, а в URL её нужно кодировать: браузер и curl делают это сами, и в лог nginx путь тоже попадёт в percent-encoding.

Проверяем без браузера — curl-ом, он показывает ровно то, что ушло по сети.

Первая команда печатает тело ответа на /: Hello, Laravel!. Вторая запрашивает /hello/студент и меряет только метрики: HTTP 200, 435 байт, 0.029839 с.

Откуда в адресе %D1%81%D1%82...

В URL разрешены не все символы: кириллицу браузер и curl кодируют в percent-encoding — каждый байт UTF-8 как %XX (кодировки вы разбирали в lesson.strings.01, структуру URL — в lesson.http.i.02). В слове «студент» семь букв, каждая в UTF-8 занимает два байта, — отсюда ровно семь пар %XX. Laravel отдаёт параметр уже раскодированным, поэтому в замыкание попадает нормальная строка.

Сравните два измерения: 70 403 байта и 0.439242 с у стартовой страницы против 435 байт и 0.029839 с у нашей. Разница — это в первую очередь размер ответа и стоимость большого шаблона, а не «магия фреймворка».

19листайте
Первый ответ · шаблон

Шаблон Blade: {{ }} подставляет значение

resources/views/hello.blade.php
1<!DOCTYPE html>2<html lang="ru">3<head>4 <meta charset="utf-8">5 <meta name="viewport" content="width=device-width, initial-scale=1">6 <title>Привет, {{ $name }}</title>7</head>8<body>9 <h1>Привет, {{ $name }}!</h1>10 <p>Это шаблон Blade: значение приехало из адреса <code>/hello/{name}</code>.</p>11 <p>Сейчас: {{ now()->locale('ru')->isoFormat('D MMMM YYYY, HH:mm') }}</p>12</body>13</html>

view('hello') ищет файл resources/views/hello.blade.php: имя шаблона — это путь относительно resources/views/ без суффикса .blade.php.

В шаблоне три подстановки:

  • строка 6 и строка 9 — {{ $name }}: переменная, которую мы передали массивом из маршрута;
  • строка 11 — {{ now()->locale('ru')->isoFormat('D MMMM YYYY, HH:mm') }}: now() возвращает объект даты и времени (тот самый Carbon, который мы ставили через Composer в части 1), locale('ru') переключает язык, isoFormat() задаёт формат.
student@mint: ~/recoursia/project-laravel515 мс
student@mint:~/recoursia/project-laravel$
HTML, который вернул Laravel — Видно, как Blade подставил значение и экранировал его.
Разбор команды

Смотрим сам HTML, который собрал Blade, — чтобы сравнить его с шаблоном строка в строку.

curl -sS "http://127.0.0.1:8000/hello/%D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82"
ТокенЗачем он и что меняет
curlHTTP-клиент; без -o тело ответа печатается в терминал — здесь оно короткое, и это ровно то, что нужно
-sSтихо, но с ошибками
"http://127.0.0.1:8000/hello/%D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82"адрес маршрута с параметром в кавычках; %D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82 — это «студент» в percent-encoding

Что произойдёт

13 строк HTML: <title>Привет, студент</title>, <h1>Привет, студент!</h1> и строка «Сейчас: 17 октября 2026, 10:20».

На что смотреть

Строк ровно столько же, сколько в шаблоне hello.blade.php: Blade не добавляет своей разметки, он только подставляет значения вместо {{ }}. Параметр {name} пришёл в замыкание уже раскодированным — в HTML стоит «студент», а не %D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82.

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

  • Вместо «студент» кракозябры — смотрите кодировку: в шаблоне есть <meta charset="utf-8">, а nginx отдаёт заголовок Content-Type: text/html; charset=utf-8.
  • Время в последней строке — момент рендера, а не момент запуска curl: шаблон собирается заново на каждый запрос.

Ответ — 13 строк, ровно как в шаблоне: Blade не добавляет своей разметки, он только подставляет значения вместо {{ }}. Сравните:

В шаблоне В ответе
<title>Привет, {{ $name }}</title> <title>Привет, студент</title>
<h1>Привет, {{ $name }}!</h1> <h1>Привет, студент!</h1>
{{ now()->locale('ru')->isoFormat('D MMMM YYYY, HH:mm') }} 17 октября 2026, 10:20

Всё остальное — байт в байт тот же HTML, что мы написали.

Связка с курсом

Раньше вы собирали страницу конкатенацией строк и echo внутри циклов (lesson.http.i.04: таблица рейсов, short tag). Blade — тот же вывод, только разметка и данные разделены: шаблон читается как HTML, а подстановки видны с первого взгляда.

20листайте
Первый ответ · безопасность

Экранирование: почему {{ }} безопасен

student@mint: ~/recoursia/project-laravel841 мс
student@mint:~/recoursia/project-laravel$
Что делает {{ }} под капотом — e() — та самая функция экранирования, которую вызывает Blade. Это защита от XSS: тот же приём, что prepared statements в уроке про SQL-инъекции.
Разбор команды

Проверяем, что именно делает {{ }} под капотом: вызываем функцию экранирования e() внутри приложения.

php artisan tinker --execute="echo e('<b>жирный</b>'), PHP_EOL;"
ТокенЗачем он и что меняет
phpинтерпретатор; на стенде PHP 8.3 — подставьте свою версию (php -v)
artisanконсольный вход в приложение
tinkerинтерактивная консоль Laravel: код выполняется внутри уже загруженного приложения, со всеми его классами и хелперами
--execute=опция с значением через = в той же строке: выполнить код и выйти, не открывая интерактивный сеанс — так проверку можно снять одним захватом
"echo e('<b>жирный</b>'), PHP_EOL;"значение опции в двойных кавычках: вся строка кода уходит одним аргументом, несмотря на пробелы и скобки. Внутренние кавычки одинарные, поэтому экранировать ничего не нужно
echoвывод результата в stdout
e('<b>жирный</b>')та самая функция экранирования, которую вызывает Blade; в одинарных кавычках < и > остаются собой — это строковый литерал PHP
, PHP_EOLвторой аргумент echo через запятую — константа перевода строки: без неё вывод слипся бы с приглашением
;конец PHP-инструкции внутри строки кода

Что произойдёт

Одна строка вывода: &lt;b&gt;жирный&lt;/b&gt; — угловые скобки заменены HTML-сущностями.

На что смотреть

Это и есть работа {{ }}: Blade не вставляет значение в HTML «как есть», а сначала прогоняет его через e(). Передайте в /hello/{name} строку <script>alert(1)</script> — и в ответе окажутся &lt;script&gt;alert(1)&lt;/script&gt;, то есть текст, а не выполнимый код.

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

  • Двойные кавычки внутри значения --execute="..." пришлось бы экранировать как \", иначе shell закрыл бы строку раньше времени; здесь внутри только одинарные — конфликта нет.
  • Для готовой HTML-разметки у Blade есть вывод без экранирования — но данным из адреса, формы или базы он противопоказан: вы своими руками отключаете защиту от XSS.
  • Без --execute откроется интерактивный сеанс, который будет ждать ввода и не вернёт терминал.

Мы попросили приложение выполнить одну строку в tinker — интерактивной консоли из списка команд artisan. Функция e() получила строку <b>жирный</b> и вернула &lt;b&gt;жирный&lt;/b&gt;: угловые скобки заменены на HTML-сущности.

Именно это делает {{ }} под капотом. Blade не вставляет значение в HTML «как есть» — он сначала прогоняет его через e(). Отсюда следствие, которое стоит запомнить: если в адресе /hello/{name} вместо «студент» передать <script>alert(1)</script>, в ответе окажутся &lt;script&gt;..., и браузер покажет это как текст, а не выполнит как код.

Что было бы без экранирования

Параметр {name} приходит из адреса, то есть целиком под контролем посетителя. Без e() чужая строка стала бы частью вашего HTML — а значит, чужой скрипт выполнился бы в браузере другого пользователя, с его куки и его сессией. Это атака XSS, и защита от неё — экранирование вывода.

Связка с курсом

Принцип вам уже знаком: в lesson.sql-injection.i.01–i.03 вы видели, как параметр sort из адресной строки менял структуру SQL-запроса, и как от этого защищаются — подготовленный запрос с маркерами (lesson.db.04) плюс белый список допустимых значений через in_array. Здесь та же логика, только с другой стороны: данные не должны становиться разметкой. Оба случая — одно правило: не доверять тому, что пришло от клиента.

Когда экранирование мешает

Если нужно вывести готовую HTML-разметку (например, текст статьи), у Blade есть отдельный синтаксис вывода без экранирования. Пользоваться им для данных из адреса, из формы или из базы нельзя: вы своими руками отключите защиту, которую только что разобрали.

21листайте
Первый ответ · внутри Blade

Blade не интерпретируется: шаблон компилируется в PHP

student@mint: ~/recoursia/project-laravel394 мс
student@mint:~/recoursia/project-laravel$
Куда Blade складывает собранные шаблоны — Blade не интерпретируется на лету: шаблон компилируется в обычный PHP и кэшируется.
Разбор команды

Смотрим, куда Blade складывает скомпилированные шаблоны, — доказательство, что шаблон превращается в обычный PHP-файл.

ls -la storage/framework/views | head -8
ТокенЗачем он и что меняет
lsсписок содержимого каталога
-laдва флага слитно: -l — длинный формат, здесь важны и размер, и минута изменения, и права; -a — скрытые файлы, без него не было бы видно .gitignore
storage/framework/viewsкаталог кэша видов
|пайп: вывод ls передаётся следующей команде
headоставить только начало вывода
-8ограничение в 8 строк — с запасом: здесь вывод короче (total, ., .., .gitignore и два файла)

Что произойдёт

Два файла с именами-хэшами: 72 662 байта и 593 байта, оба с временем 10:20, права -rwxrwxrwx, владелец student; рядом .gitignore на 14 байт.

На что смотреть

Меньший файл — наш hello.blade.php: страница из ответа curl заканчивалась строкой «Сейчас: 17 октября 2026, 10:20», и оба файла датированы той же минутой — шаблоны собрались при наших запросах. Больший на два порядка — собранный welcome.blade.php: в стартовой странице стили зашиты в сам шаблон. Права и владелец показывают, что файлы записал процесс, обслуживший запрос.

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

  • «Я поправил шаблон, а страница не изменилась» — смотрят именно сюда, в кэш видов; как его сбрасывать, разбираем в части 6.
  • Нет права записи в storage/ — скомпилировать шаблон некуда: вместо страницы будет 500, а в логе приложения tempnam(): file created in the system's temporary directory (снимаем это в части 6).
  • Имя файла — хэш, а не имя шаблона: найти «свой» файл по названию не получится, ориентируйтесь на размер и время.

Что лежит в storage/framework/views/. Два файла с именами-хэшами:

Файл Размер Время Что это
2ee673e246...php 72 662 байта 10:20 собранный welcome.blade.php
6933e8cf03...php 593 байта 10:20 собранный hello.blade.php

Оба файла записаны в 10:20 — в ту минуту, когда прогон запрашивал страницы: время «17 октября 2026, 10:20» видно в самом ответе на слайде про шаблон. Какой файл какому шаблону соответствует, видно по размеру: стартовая страница — это 70 403 байта HTML со стилями, зашитыми в шаблон, значит 72 662 байта — собранный welcome.blade.php; наш hello.blade.php — 13 строк и 457 байт исходника, откуда и 593 байта компиляции.

Рядом .gitignore (14 байт): содержимое каталога — кэш, в git ему не место.

Зачем это нужно. Blade-метки ({{ }}, @if, @foreach) — не PHP. Laravel заранее переводит шаблон в обычный PHP-файл и дальше выполняет уже его. Разбор Blade-синтаксиса делается один раз, а не на каждом запросе.

Права у файлов -rwxrwxrwx, владелец student — их записал тот самый процесс, который обслуживал запрос. Это прямая иллюстрация слайда про права: не будет записи в storage/ — не будет и скомпилированных шаблонов, а вместо страницы вы получите ошибку.

Отсюда же растёт типовая проблема «я поправил шаблон, а страница не изменилась»: в таком случае смотрят на кэш видов. Как его сбрасывать — разберём в части 6.

22листайте
Первый ответ · результат

Тот же ответ в браузере

http://127.0.0.1:8000/hello/%D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82
Страница собрана шаблоном hello.blade.php.
Страница собрана шаблоном hello.blade.php.

Адрес http://127.0.0.1:8000/hello/%D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82 — то, что curl отдал текстом, браузер показал страницей. Пока всё это работает через встроенный сервер разработки. Дальше — перенос на nginx.

23листайте
nginx · подготовка

Уходим со встроенного сервера на nginx

student@mint: ~416 мс
student@mint:~$
Что включено в nginx до нас
Разбор команды

Смотрим, что включено в nginx до нас, — чтобы понимать, куда добавлять свой vhost и что при этом уже работает.

ls -l /etc/nginx/sites-enabled/
ТокенЗачем он и что меняет
lsсписок содержимого каталога
-lдлинный формат: только он показывает, что запись — символическая ссылка, и куда она ведёт
/etc/nginx/sites-enabled/каталог включённых сайтов: nginx читает именно его, а не sites-available/

Что произойдёт

Одна запись: default -> /etc/nginx/sites-available/default — символическая ссылка на дефолтный сайт из пакета.

На что смотреть

Первый символ прав l (lrwxrwxrwx) означает ссылку, стрелка показывает цель. Дефолтный сайт отвечает на запросы, для которых не нашлось своего server_name. Дальше работаем по схеме из lesson.linux.05–lesson.linux.06: файл vhost кладём в sites-available/, ссылку на него — в sites-enabled/.

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

  • Править файл прямо в sites-enabled/ — типовая ошибка: это ссылка, и вы измените файл в sites-available/.
  • Удалять default молча не стоит: роль сайта по умолчанию перейдёт к первому оставшемуся блоку server, и запросы к чужому домену попадут в ваше приложение.

В sites-enabled/ одна символическая ссылка — default на sites-available/default. Это дефолтный сайт из пакета: он отвечает на запросы, для которых не нашлось своего server_name.

Работаем по схеме из lesson.linux.05–lesson.linux.06: файл vhost кладём в sites-available/, ссылку на него — в sites-enabled/. Домен project-laravel.rs при этом уже работает: dnsmasq отдаёт для всей зоны .rs адрес 127.0.0.1, поэтому настраивать DNS заново не нужно. Позже access.log это подтвердит — в строках будет client: 127.0.0.1.

student@mint: ~466 мс
student@mint:~$
Почему SCRIPT_FILENAME задают руками — В Ubuntu fastcgi_params не содержит SCRIPT_FILENAME — без него PHP-FPM не знает, какой файл выполнять.
Разбор команды

Проверяем, есть ли в системных файлах FastCGI параметр SCRIPT_FILENAME — от этого зависит, придётся ли задавать его руками.

grep -n "SCRIPT_FILENAME" /etc/nginx/fastcgi.conf /etc/nginx/fastcgi_params || echo "в fastcgi_params SCRIPT_FILENAME нет"
ТокенЗачем он и что меняет
grepпоиск строк по шаблону
-nдобавлять номер строки — будем ссылаться на конкретное место файла
"SCRIPT_FILENAME"шаблон поиска в двойных кавычках. Спецсимволов внутри нет, но с кавычками grep гарантированно получит ровно один аргумент
/etc/nginx/fastcgi.conf /etc/nginx/fastcgi_paramsдва файла одним вызовом: когда аргументов несколько, grep печатает перед строкой имя файла — сравнение видно сразу
||«ИЛИ» с коротким замыканием: правая часть выполняется только если grep вернул ненулевой код, то есть ничего не нашёл
echo "в fastcgi_params SCRIPT_FILENAME нет"запасное сообщение: отсутствие параметра будет напечатано текстом, а не пустым выводом

Что произойдёт

Одна строка: /etc/nginx/fastcgi.conf:2:fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;. В fastcgi_params параметр отсутствует — иначе grep напечатал бы и его строку, а echo не сработал бы.

На что смотреть

Мы подключаем fastcgi_params и дописываем параметр руками. $document_root — значение директивы root для этого location, $fastcgi_script_name — путь к скрипту из запроса; вместе они дают полный путь к public/index.php нашего проекта.

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

  • Подключили fastcgi_params и не задали SCRIPT_FILENAME — PHP-FPM не знает, какой файл выполнять; обычно это заканчивается коротким телом File not found.
  • || реагирует на любой ненулевой код возврата: если файла вообще нет на диске, grep завершится ошибкой, и сообщение echo скроет настоящую причину.

grep искал SCRIPT_FILENAME в двух файлах и нашёл только в одном: /etc/nginx/fastcgi.conf, строка 2. Значит, в /etc/nginx/fastcgi_params этого параметра нет — иначе grep напечатал бы и его строку.

Почему это важно: по протоколу FastCGI веб-сервер передаёт PHP-FPM набор параметров, и SCRIPT_FILENAME — путь к файлу, который нужно выполнить. Без него PHP-FPM не знает, что запускать.

Мы подключаем fastcgi_params и дописываем параметр руками:

nginx
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

$document_root — значение директивы root для этого location, $fastcgi_script_name — путь к скрипту из запроса. Вместе они дадут /home/student/recoursia/project-laravel/public/index.php.

Зачем вообще nginx, если serve работает

Статика напрямую из nginx, а не через PHP-процесс; access.log и error.log; несколько сайтов на одном порту 80 через server_name; пул процессов PHP-FPM вместо одного процесса. И главное — это боевая схема: так же ваш проект будет стоять на настоящем сервере.

24листайте
nginx · ошибка № 1

Ошибка № 1: root указывает на папку проекта — получаем 403

etc/nginx/sites-available/project-laravel.rs.confnginx
1server {2 listen 80;3 listen [::]:80;4 server_name project-laravel.rs;56 # ОШИБКА: корень — папка проекта, а не public7 root /home/student/recoursia/project-laravel;8 index index.php index.html;910 location ~ \.php$ {11 include fastcgi_params;12 fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;13 fastcgi_pass unix:/run/php/php8.3-fpm.sock;14 }15}

Конфиг выглядит разумно: server_name project-laravel.rs, index index.php index.html, .php уходит в PHP-FPM через сокет php8.3-fpm.sock. Одна строка всё ломает — root /home/student/recoursia/project-laravel;.

Дальше — привычный ритуал из lesson.linux.06: ссылка в sites-enabled, sudo nginx -t (проверить синтаксис до перезагрузки), systemctl reload nginx (перечитать конфиг без обрыва соединений), и только потом запрос.

student@mint: ~1674 мс
student@mint:~$
>
>
>
>
>
>
Проверяем вариант 1: получаем 403 — nginx ищет index.php в корне проекта, не находит, а список файлов показывать запрещено.
Разбор команды

Включаем первый вариант vhost — с намеренной ошибкой в root — и проходим весь ритуал: ссылка, проверка синтаксиса, перезагрузка, запрос, лог.

sudo ln -sf /etc/nginx/sites-available/project-laravel.rs.conf /etc/nginx/sites-enabled/project-laravel.rs.conf
ТокенЗачем он и что меняет
sudoзапись в /etc/nginx требует прав root (на стенде sudo настроен без пароля)
lnсоздание ссылки
-sfдва флага слитно: -s — символическая ссылка, а не жёсткая (так устроена схема nginx: файл в sites-available/, ссылка в sites-enabled/); -f — перезаписать цель без вопроса, если ссылка уже есть, иначе повторный запуск упадёт
/etc/nginx/sites-available/project-laravel.rs.confисточник — файл vhost, который мы написали
/etc/nginx/sites-enabled/project-laravel.rs.confцель — имя ссылки в каталоге, который читает nginx

Что произойдёт

Ссылка создана, конфиг считается включённым. Сам nginx о нём ещё не знает — нужна проверка и reload.

На что смотреть

Порядок аргументов: сначала источник, потом цель. Перепутаете — и в sites-available/ окажется ссылка, а не файл.

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

  • Без -f при уже существующей ссылке ln ответит File exists.
  • Ссылка сама по себе ничего не меняет в работе сервера: конфиг применяется только после reload.
sudo nginx -t
ТокенЗачем он и что меняет
sudoчтение чужих конфигов и проверка от root
nginxсам сервер
-ttest: разобрать все конфиги и проверить синтаксис, ничего не запуская и не перезагружая

Что произойдёт

Две строки: the configuration file /etc/nginx/nginx.conf syntax is ok и test is successful.

На что смотреть

Проверка ловит только синтаксис. Наша ошибка смысловая — root указывает на папку проекта, а не на public, — поэтому nginx -t молчит. Это и есть главная ловушка: «конфиг проверен» не значит «конфиг верен».

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

  • nginx -t не проверяет, существует ли каталог из root и лежит ли в нём index.php; проверку нельзя заменить запросом — и наоборот.
sudo systemctl reload nginx
ТокенЗачем он и что меняет
sudoуправление системным сервисом требует root
systemctlинтерфейс к systemd
reloadперечитать конфигурацию без остановки процесса и без обрыва соединений — в отличие от restart
nginxимя сервиса

Что произойдёт

nginx перечитывает конфиги и поднимает блок server для домена project-laravel.rs.

На что смотреть

reload сохраняет существующие соединения. Если в конфиге ошибка, перезагрузка не применится — поэтому перед ней и стоит nginx -t.

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

  • Забыли reload — запросы идут по старому конфигу, и вы «исправляете» то, что уже исправлено.
sleep 1
ТокенЗачем он и что меняет
sleepпауза
1одна секунда

Что произойдёт

Даём reload завершиться до первого запроса.

На что смотреть

Обычно секунды хватает; если ответ выглядит как старый, увеличьте паузу или посмотрите systemctl status nginx.

curl -sS -o /dev/null -w "GET http://project-laravel.rs/ -> %{http_code}\n" http://project-laravel.rs/
ТокенЗачем он и что меняет
curlHTTP-клиент
-sSтихо, но с сообщениями об ошибках
-o /dev/nullтело ответа выбросить: на этом шаге нужен только код
-w "GET http://project-laravel.rs/ -> %{http_code}\n"своя подпись и одна подстановка — код ответа; \n в конце обязателен, иначе строка сольётся с приглашением
http://project-laravel.rs/адрес запроса: домен локальной зоны .rs, для которой dnsmasq отдаёт 127.0.0.1

Что произойдёт

GET http://project-laravel.rs/ -> 403.

На что смотреть

403, а не 404: nginx искал в корне сайта index.php или index.html, не нашёл, а показывать список каталога запрещено — autoindex по умолчанию выключен.

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

  • По одному коду причину не определить — следующим шагом всегда смотрим тело ответа и error.log.
curl -sS http://project-laravel.rs/ | head -8
ТокенЗачем он и что меняет
curlвторой запрос — теперь ради тела ответа
-sSтихо, но с ошибками
http://project-laravel.rs/тот же адрес
|пайп: тело ответа передаётся следующей команде
headоставить только начало
-8первые 8 строк: страница ошибки nginx короткая и целиком в них помещается

Что произойдёт

HTML-страница ошибки самого nginx: <title>403 Forbidden</title>, <center><h1>403 Forbidden</h1></center> и подпись nginx/1.24.0 (Ubuntu).

На что смотреть

Подпись nginx/... — ключ к диагнозу: ошибку вернул веб-сервер, до PHP-FPM дело не дошло и Laravel не запускался. У Laravel страница ошибки своя.

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

  • Пустое тело вместо страницы — обычно значит, что PHP-FPM не ответил; смотрите заголовки (curl -I) и error.log.
  • Такой root опасен не только кодом 403: снаружи оказывается виден весь проект, включая .env, — запрета скрытых файлов в этом варианте нет.
echo "--- access.log ---"; sudo tail -2 /var/log/nginx/access.log
ТокенЗачем он и что меняет
echo "--- access.log ---"подпись-разделитель: в одном захвате видно, где кончается вывод curl и где начинается лог
;разделитель команд shell
sudoлог принадлежит root — читать его нужно с повышением
tailпечатать конец файла
-2последние две строки — наши два запроса
/var/log/nginx/access.logжурнал запросов nginx

Что произойдёт

Строка --- access.log --- и две одинаковые записи — по одной на каждый из двух наших запросов: 127.0.0.1 - - [17/Oct/2026:10:20:33 +0000] "GET / HTTP/1.1" 403 162 "-" "curl/8.5.0".

На что смотреть

Код 403 и 162 байта — размер страницы ошибки nginx. Клиент 127.0.0.1: домен project-laravel.rs разрешился в локальный адрес, это работа dnsmasq.

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

  • access.log фиксирует факт, но не причину: объяснение 403 лежит в error.log — directory index of "/home/student/recoursia/project-laravel/" is forbidden.

Разбор. nginx -t говорит, что синтаксис в порядке: конфиг корректен, ошибка смысловая, её проверка синтаксиса не ловит.

GET / → 403. И тело ответа — страница самого nginx: <center>nginx/1.24.0 (Ubuntu)</center>. Это значит, что до PHP дело не дошло: Laravel не запускался, ошибку вернул веб-сервер.

Почему 403, а не 404? nginx ищет в корне сайта index.php или index.html. В папке проекта их нет — index.php лежит на уровень глубже, в public/. Не найдя индексного файла, nginx хотел бы показать список каталога, но autoindex по умолчанию выключен — отсюда «доступ запрещён». Строку directory index of "/home/student/recoursia/project-laravel/" is forbidden мы увидим в error.log на слайде про логи.

И это ещё полбеды: при таком root снаружи виден весь проект, включая .env с ключом приложения и доступом к базе. Никакого deny для скрытых файлов в этом варианте нет — проверять мы это, разумеется, не будем.

Версия PHP

8.3 и php8.3 в командах выше — версия стенда. Проверьте свою: php -v. Если она другая — замените 8.3 на свою во всех путях (/etc/php/…, /run/php/…), именах пакетов (php8.3-*) и сервисе (php8.3-fpm).

25листайте
nginx · ошибка № 2

Ошибка № 2: root верный, но нет try_files — 404 на маршрутах

etc/nginx/sites-available/project-laravel.rs.confnginx
1server {2 listen 80;3 listen [::]:80;4 server_name project-laravel.rs;56 root /home/student/recoursia/project-laravel/public;7 index index.php index.html;89 location ~ \.php$ {10 include fastcgi_params;11 fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;12 fastcgi_pass unix:/run/php/php8.3-fpm.sock;13 }14}

Поправили одну строку: root теперь указывает на public. Всё остальное прежнее — и location / с try_files по-прежнему нет.

Что должно произойти по нашей теории? Корень сайта правильный, index.php в нём есть, значит главная страница заработает. А вот /hello/студент — не файл на диске, и nginx об этом ничего не знает.

student@mint: ~1703 мс
student@mint:~$
>
>
>
>
Проверяем вариант 2: корень верный, но маршруты не работают — nginx ищет на диске файл hello/студент, не находит и сам отдаёт 404 — до Laravel запрос не доходит.
Разбор команды

Проверяем второй вариант: root уже указывает на public, но try_files по-прежнему нет. Тот же ритуал — проверка, reload, два запроса.

sudo nginx -t && sudo systemctl reload nginx
ТокенЗачем он и что меняет
sudoправа root для работы с конфигом и сервисом
nginxсервер
-tпроверить синтаксис конфигов, ничего не перезагружая
&&логическое «И»: вторая команда выполнится, только если первая завершилась с кодом 0 — нельзя «перезалить» заведомо сломанный конфиг
sudo systemctl reload nginxперечитать конфигурацию без обрыва соединений

Что произойдёт

syntax is ok и test is successful, затем конфиг перечитан.

На что смотреть

Связка через &&, а не через ; — это страховка: перезагрузка зависит от результата проверки. Смысловых ошибок nginx -t не видит, поэтому отсутствие try_files он не заметит.

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

  • Если проверка не прошла, reload не выполнится вовсе, и nginx продолжит работать по старому конфигу — в логе сервиса останется сообщение об ошибке.
sleep 1
ТокенЗачем он и что меняет
sleepпауза
1одна секунда — даём reload завершиться

Что произойдёт

Терминал ждёт секунду до первого запроса.

На что смотреть

Без паузы запрос может попасть в момент перечитывания конфига, и вы снимете старое поведение.

curl -sS -o /dev/null -w "GET / -> %{http_code}\n" http://project-laravel.rs/
ТокенЗачем он и что меняет
curlHTTP-клиент
-sSтихо, но с ошибками
-o /dev/nullтело не нужно — меряем код ответа
-w "GET / -> %{http_code}\n"подпись и код ответа одной строкой
http://project-laravel.rs/корневой адрес сайта

Что произойдёт

GET / -> 200 — главная страница заработала.

На что смотреть

Сработала директива index index.php: nginx нашёл файл в public/, передал его в PHP-FPM, и Laravel вернул Hello, Laravel!.

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

  • 200 на главной не означает, что работают маршруты: следующий запрос это и проверяет.
curl -sS -o /dev/null -w "GET /hello/студент -> %{http_code}\n" "http://project-laravel.rs/hello/%D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82"
ТокенЗачем он и что меняет
curlзапрос к маршруту с параметром
-sSтихо, но с ошибками
-o /dev/nullтело выбрасываем
-w "GET /hello/студент -> %{http_code}\n"подпись с кириллицей и код ответа: в терминале — читаемый текст
"http://project-laravel.rs/hello/%D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82"URL в кавычках; по сети кириллица уходит в percent-encoding — семь пар %XX на слово «студент»

Что произойдёт

GET /hello/студент -> 404.

На что смотреть

Этот 404 вернул nginx, а не Laravel: маршрут существует, мы его видели в route:list, но до роутера запрос не дошёл.

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

  • Отличать источники ошибки полезно: страница nginx подписана nginx/1.24.0 (Ubuntu), у Laravel страница ошибки своя.
curl -sS "http://project-laravel.rs/hello/%D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82" | head -6
ТокенЗачем он и что меняет
curlтот же адрес — теперь ради тела ответа
-sSтихо, но с ошибками
"http://project-laravel.rs/hello/%D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82"адрес в кавычках
|пайп
headоставить начало вывода
-6шесть строк: страница ошибки nginx почти целиком — последняя строка </html> в них не попала

Что произойдёт

Страница <title>404 Not Found</title> с подписью nginx/1.24.0 (Ubuntu); последняя видимая строка — </body>, закрывающий </html> обрезан head -6.

На что смотреть

Подпись подтверждает: 404 сгенерировал веб-сервер. Причина — nginx обслуживает запрос как запрос к файлу: ищет на диске public/hello/студент, не находит и сам отдаёт 404. В error.log это записано как open() "/home/student/recoursia/project-laravel/public/hello/студент" failed (2: No such file or directory).

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

  • Маршруты Laravel — не файлы, их нет на диске: они объявлены в routes/web.php. Веб-серверу нужно прямо сказать, что всё несуществующее уходит в index.php.
  • Лечится одной директивой: try_files $uri $uri/ /index.php?$query_string;. Без ?$query_string параметры вроде ?page=2 потеряются при передаче.

Разбор. GET / → 200: index index.php сработал, nginx передал public/index.php в PHP-FPM, Laravel отдал строку Hello, Laravel!.

GET /hello/студент → 404, и снова страница с подписью nginx/1.24.0 (Ubuntu) — то есть 404 вернул nginx, не Laravel. Отличать их полезно: у Laravel страница ошибки своя.

Причина: nginx обслуживает запрос как запрос к файлу. Он ищет на диске /home/student/recoursia/project-laravel/public/hello/студент, не находит и сам отдаёт 404. До index.php — а значит, и до таблицы маршрутов — запрос не доходит вообще.

Ключевая мысль

Маршруты Laravel — не файлы. Их нет на диске, они объявлены в routes/web.php. Поэтому веб-серверу нужно прямо сказать: «всё, чего нет в файловой системе, передавай в index.php». Это и делает try_files.

Версия PHP

8.3 и php8.3 в командах выше — версия стенда. Проверьте свою: php -v. Если она другая — замените 8.3 на свою во всех путях (/etc/php/…, /run/php/…), именах пакетов (php8.3-*) и сервисе (php8.3-fpm).

26листайте
nginx · рабочий vhost

Итоговый vhost: четыре директивы, которые всё решают

etc/nginx/sites-available/project-laravel.rs.confnginx
1# Laravel на домене project-laravel.rs (локальная зона .rs из lesson.linux.05)2server {3 listen 80;4 listen [::]:80;5 server_name project-laravel.rs;67 # корень — ОБЯЗАТЕЛЬНО папка public проекта8 root /home/student/recoursia/project-laravel/public;9 index index.php index.html;1011 # всё, чего нет на диске, отдаём в index.php — там разберёт роутер Laravel12 location / {13 try_files $uri $uri/ /index.php?$query_string;14 }1516 # .php уходит в PHP-FPM через unix-сокет (адрес видели в /run/php)17 location ~ \.php$ {18 include fastcgi_params;19 fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;20 fastcgi_pass unix:/run/php/php8.3-fpm.sock;21 }2223 # скрытые файлы (.env, .git) снаружи отдавать нельзя24 location ~ /\. {25 deny all;26 }27}

Строки 3–5. listen 80 и listen [::]:80 — принимаем HTTP на 80-м порту по IPv4 и IPv6. server_name project-laravel.rs — этим доменом занимается именно этот блок server.

Строка 8. root .../project-laravel/public — корень сайта обязательно public. Всё, что выше, для nginx не существует.

Строка 9. index index.php index.html — чем отвечать на запрос к каталогу.

Строки 12–13. Главное новое:

nginx
try_files $uri $uri/ /index.php?$query_string;

Проверки идут слева направо, срабатывает первая совпавшая:

файла неткаталога нетfastcgiЗапрос — GET /css/app.css или /hello/…Запрос$uri — файл есть в public/ — отдать его$uri$uri/ — каталог — отдать его индекс$uri//index.php?$query_string — внутренняя передача, параметры сохранены/index.php?$query_stringPHP-FPM — unix-сокет php8.3-fpm.sockPHP-FPMфайла неткаталога нетfastcgiЗапрос — GET /css/app.css или /hello/…Запрос$uri — файл есть в public/ — отдать его$uri$uri/ — каталог — отдать его индекс$uri//index.php?$query_string — внутренняя передача, параметры сохранены/index.php?$query_stringPHP-FPM — unix-сокет php8.3-fpm.sockPHP-FPM
try_files: файл на диске → каталог → index.php → PHP-FPM

Именно поэтому статика из public/ отдаётся быстро и без PHP, а всё остальное уходит в роутер Laravel.

?$query_string в конце — обязательная деталь: без неё параметры после «?» потеряются при передаче, и ?page=2 из пагинации (lesson.http.i.05) до Laravel не доедет.

Строки 17–20. .php уходит в PHP-FPM через unix-сокет /run/php/php8.3-fpm.sock — тот же адрес и тот же протокол FastCGI, что в lesson.linux.06. include fastcgi_params + явный SCRIPT_FILENAME — по причине, которую мы проверили grep-ом.

Строки 24–25. location ~ /\. { deny all; } — любой путь, в котором есть «точка после слэша» (.env, .git, .htaccess), запрещён. Второй рубеж защиты к тому, что root уже спрятал эти файлы.

Связка с курсом

Схема ровно та, что вы собрали в lesson.linux.06: server_name → root → location для .php → fastcgi_pass в сокет. Отличия два: root на один уровень глубже (/public), и добавлен try_files, потому что теперь адреса не обязаны совпадать с файлами на диске.

Версия PHP

8.3 и php8.3 в командах выше — версия стенда. Проверьте свою: php -v. Если она другая — замените 8.3 на свою во всех путях (/etc/php/…, /run/php/…), именах пакетов (php8.3-*) и сервисе (php8.3-fpm).

27листайте
nginx · проверка

Проверяем: 200 на оба маршрута и заголовки ответа

student@mint: ~1735 мс
student@mint:~$
>
>
>
>
>
>
Вариант 3: добавили try_files — работает всё
Разбор команды

Третий вариант vhost — с try_files и запретом скрытых файлов. Проверяем оба маршрута и смотрим, что отвечает сервер в заголовках.

sudo nginx -t && sudo systemctl reload nginx
ТокенЗачем он и что меняет
sudoправа root
nginxсервер
-tпроверка синтаксиса конфигов
&&вторая команда выполнится только при нулевом коде возврата первой
sudo systemctl reload nginxперечитать конфигурацию без обрыва соединений

Что произойдёт

syntax is ok, test is successful, конфиг перечитан.

На что смотреть

Ритуал тот же, что в двух предыдущих вариантах: меняли только файл vhost — порядок проверки и перезагрузки не менялся.

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

  • nginx -t по-прежнему не гарантирует, что сайт заработает: он проверяет синтаксис, а не поведение.
sleep 1
ТокенЗачем он и что меняет
sleepпауза
1секунда до первого запроса

Что произойдёт

Ждём завершения reload.

На что смотреть

Без паузы первый запрос рискует попасть на старый конфиг.

curl -sS -o /dev/null -w "GET / -> %{http_code}\n" http://project-laravel.rs/
ТокенЗачем он и что меняет
curlHTTP-клиент
-sSтихо, но с ошибками
-o /dev/nullтело не выводим — нужен код
-w "GET / -> %{http_code}\n"подпись и код ответа одной строкой
http://project-laravel.rs/корень сайта

Что произойдёт

GET / -> 200.

На что смотреть

Корневой маршрут работает и через nginx: статика и index.php нашлись в public/.

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

  • 200 здесь не новость — он был и в варианте 2. Новое начнётся со следующего запроса.
curl -sS -o /dev/null -w "GET /hello/студент -> %{http_code}\n" "http://project-laravel.rs/hello/%D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82"
ТокенЗачем он и что меняет
curlзапрос к маршруту с параметром
-sSтихо, но с ошибками
-o /dev/nullтело не выводим
-w "GET /hello/студент -> %{http_code}\n"подпись и код ответа
"http://project-laravel.rs/hello/%D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82"URL в кавычках, кириллица в percent-encoding

Что произойдёт

GET /hello/студент -> 200 — вместо 404 из предыдущего варианта.

На что смотреть

Заработал try_files: файла hello/студент на диске нет, поэтому запрос внутренне передан на /index.php, а роутер Laravel нашёл маршрут hello/{name} в своей таблице.

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

  • Если бы здесь снова был 404 с подписью nginx — try_files не применился: проверяйте, что директива внутри location / и что reload действительно прошёл.
curl -sS http://project-laravel.rs/
ТокенЗачем он и что меняет
curlзапрос ради тела ответа
-sSтихо, но с ошибками
http://project-laravel.rs/корень сайта; без -o тело печатается в терминал

Что произойдёт

Одна строка: Hello, Laravel!.

На что смотреть

Тело ровно то, что вернуло замыкание в routes/web.php: статус и заголовки Laravel добавил сам. По access.log на тело ушло 25 байт.

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

  • 200 с пустым телом — другой случай: запрос дошёл до index.php, но PHP-FPM не получил SCRIPT_FILENAME.
echo
ТокенЗачем он и что меняет
echoбез аргументов печатает пустую строку — разделитель между телом предыдущего ответа и заголовками следующего

Что произойдёт

В выводе появляется пустая строка.

На что смотреть

Тело ответа не заканчивается переводом строки, поэтому без echo заголовки следующего curl прилипли бы к Hello, Laravel!.

curl -sSI http://project-laravel.rs/ | head -8
ТокенЗачем он и что меняет
curlHTTP-клиент
-sSIфлаги слитно: -s — без прогресс-бара, -S — но с сообщениями об ошибках, -I — отправить HEAD-запрос и напечатать только заголовки, без тела; в access.log это останется как "HEAD / HTTP/1.1" 200 0
http://project-laravel.rs/адрес запроса
|пайп
headоставить начало вывода
-8первые 8 строк: статус, пять заголовков и обе куки

Что произойдёт

HTTP/1.1 200 OK, Server: nginx/1.24.0 (Ubuntu), Content-Type: text/html; charset=utf-8, Connection: keep-alive, Cache-Control: no-cache, private, Date: Sat, 17 Oct 2026 10:20:38 GMT и две куки: XSRF-TOKEN=... и laravel-session=..., обе с Max-Age=7200, path=/, samesite=lax.

На что смотреть

Server: nginx/1.24.0 (Ubuntu) — отвечает nginx, встроенный сервер разработки больше не участвует. charset=utf-8 — поэтому «студент» не превращается в кракозябры. Cache-Control: no-cache, private — ответ динамический и личный, промежуточным кэшам хранить его нельзя. У laravel-session дополнительно стоит httponly: такую куку не прочитать из JavaScript.

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

  • -I отправляет HEAD, а не GET: приложение может ответить на него иначе, чем на GET.
  • Значения кук длинные и выглядят как мусор — это base64 от JSON с полями iv, value, mac и tag, а не опечатка.
  • head -8 обрезает вывод по строкам: обе куки попали целиком, но заголовки могут идти и дальше — Content-Length в первых восьми строках нет.

Оба маршрута работают. GET / → 200 и тело Hello, Laravel!, GET /hello/студент → 200. Значит, try_files передаёт запрос в index.php, а роутер Laravel находит в таблице и строку, и параметр.

Кто отвечает. Server: nginx/1.24.0 (Ubuntu) — отвечает nginx, встроенный сервер разработки больше не участвует.

Content-Type: text/html; charset=utf-8 — заголовок, по которому браузер понимает, как показывать тело (вы разбирали его в lesson.http.i.02). Указана кодировка UTF-8 — поэтому «студент» в ответе не превращается в кракозябры.

Cache-Control: no-cache, private — ответ динамический: промежуточным кэшам его хранить нельзя, он личный для этого пользователя.

Connection: keep-alive — соединение не закрывается после ответа, следующий запрос уйдёт по нему же.

Две куки в ответе.

Set-Cookie: XSRF-TOKEN=... и Set-Cookie: laravel-session=..., обе с Max-Age=7200 (два часа), path=/ и samesite=lax. У laravel-session дополнительно стоит httponly — такую куку не прочитать из JavaScript.

Значение — не открытый текст. Это base64; если раскодировать, получится JSON из четырёх полей: iv (24 символа base64 = 16 байт — размер блока шифра, в config/app.php стоит 'cipher' => 'AES-256-CBC'), value (зашифрованное содержимое), mac (64 hex-символа — код аутентификации, проверка, что значение не подменили) и пустой tag.

То есть приложение уже ведёт сессию и выдаёт токен защиты от подделки запроса. Что с этим делать — разберём в части 5, когда будем отправлять формы: без XSRF-TOKEN POST-запрос будет отклонён.

28листайте
nginx · диагностика

Логи: где искать причину, когда ответ «не тот»

student@mint: ~503 мс
student@mint:~$
Логи nginx — Те же логи, которые разбирали в блоке HTTP.
Разбор команды

Смотрим оба журнала nginx: кто приходил и почему не получалось. Причины обеих наших ошибок записаны текстом.

sudo tail -4 /var/log/nginx/access.log; echo "--- error.log ---"; sudo tail -3 /var/log/nginx/error.log
ТокенЗачем он и что меняет
sudoлоги принадлежат root — читать их нужно с повышением
tailпечатать конец файла
-4последние четыре строки access.log — ровно наши четыре запроса из предыдущего слайда
/var/log/nginx/access.logжурнал запросов: факты — кто, когда, что и с каким кодом
;разделитель команд shell
echo "--- error.log ---"подпись-разделитель перед вторым блоком вывода
sudo tailто же чтение от root
-3последние три строки журнала ошибок: одна запись варианта 1 и две записи варианта 2
/var/log/nginx/error.logжурнал ошибок: здесь причина, а не факт

Что произойдёт

Четыре строки access.log — "GET / HTTP/1.1" 200 25, "GET /hello/%D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82 HTTP/1.1" 200 447, снова "GET / HTTP/1.1" 200 25 и "HEAD / HTTP/1.1" 200 0. Затем три строки error.log: directory index of "/home/student/recoursia/project-laravel/" is forbidden (10:20:33) и дважды open() "/home/student/recoursia/project-laravel/public/hello/студент" failed (2: No such file or directory) (10:20:35).

На что смотреть

Поля access.log: IP клиента, время, «метод путь версия» в кавычках, статус, число байт, referer и User-Agent (curl/8.5.0). Путь записан в percent-encoding — как пришёл по сети. "HEAD / HTTP/1.1" 200 0 — наш curl -I: заголовки есть, тела нет. В error.log времена совпадают с попытками: 10:20:33 — вариант 1 (те самые 403), 10:20:35 — вариант 2 (404), причём строка показывает полный путь, который nginx подставил.

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

  • Получили от nginx неожиданный код — сначала error.log, потом догадки: nginx -t молчал, потому что синтаксис конфигов был верным.
  • tail показывает только хвост: при большом потоке запросов нужную строку ищут grep-ом по времени, статусу или пути.
  • access.log и error.log пишутся разными процессами-воркерами — в error.log видны их PID (49663#49663), по ним можно связать записи.

access.log — кто приходил. Поля в строке: IP клиента, время, "метод путь версия", статус-код, число байт, referer и User-Agent (у нас curl/8.5.0). Этот формат вы разбирали в lesson.programs.01 и lesson.http.i.01.

Три детали, на которые стоит посмотреть:

  • путь записан как /hello/%D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82 — в лог попадает URL в том виде, в каком он пришёл по сети, в percent-encoding;
  • "HEAD / HTTP/1.1" 200 0 — наш curl -I отправил HEAD-запрос: заголовки те же, что и на GET, а тела нет, поэтому байт 0;
  • client: 127.0.0.1 — домен project-laravel.rs разрешился в локальный адрес: это работа dnsmasq из lesson.linux.05.

error.log — почему не получилось. Здесь лежат объяснения обеих наших ошибок, и времена совпадают с попытками:

  • 10:20:33 — directory index of "/home/student/recoursia/project-laravel/" is forbidden. Это вариант 1: root на папке проекта, индексного файла нет, список каталога показывать запрещено → те самые 403.
  • 10:20:35 — open() "/home/student/recoursia/project-laravel/public/hello/студент" failed (2: No such file or directory), дважды. Это вариант 2: nginx буквально искал файл с таким именем → 404. Строка даже показывает путь, который он подставил, — сразу видно, что root уже правильный, а try_files нет.
Привычка

Получили от nginx неожиданный код — сначала error.log, потом догадки. В обеих наших ошибках причина была написана текстом; nginx -t при этом молчал, потому что синтаксис конфигов был верным.

29листайте
nginx · контроль

Список маршрутов и проверка, что .env снаружи не отдаётся

student@mint: ~/recoursia/project-laravel730 мс
student@mint:~/recoursia/project-laravel$
Список маршрутов приложения — Наши два маршрута плюс служебный /up (health-check из bootstrap/app.php).
Разбор команды

Сверяем таблицу маршрутов с тем, что написано в routes/web.php, — за секунду видно, зарегистрированы ли наши маршруты и где именно.

php artisan route:list --except-vendor
ТокенЗачем он и что меняет
phpинтерпретатор; на стенде PHP 8.3 — подставьте свою версию (php -v)
artisanконсольный вход в приложение
route:listкоманда вывода таблицы маршрутов: метод, путь и место, где маршрут объявлен
--except-vendorне показывать маршруты, которые регистрируют каркас и пакеты: в списке остаются только наши

Что произойдёт

Две строки: GET|HEAD с путём / и указанием routes/web.php:5, затем GET|HEAD с путём hello/{name} и указанием routes/web.php:9; итог — Showing [2] routes.

На что смотреть

Номера строк совпадают с местами, где написаны Route::get('/', ...) и Route::get('/hello/{name}', ...). Методов у каждого маршрута два: HEAD — это GET без тела ответа, такой запрос мы видели в access.log. Служебный /up из bootstrap/app.php в списке не показан: его регистрирует каркас, и флаг его отфильтровал.

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

  • Без --except-vendor список длиннее, и свои маршруты в нём теряются.
  • Маршрута нет в таблице — значит, файл с ним не подключён: withRouting в bootstrap/app.php указывает на routes/web.php.
  • После кэширования маршрутов (php artisan optimize) таблица берётся из кэша — свежую правку файла она не покажет, пока кэш не сбросили.

php artisan route:list печатает таблицу маршрутов: метод, путь и место, где маршрут объявлен. Обе строки указывают на routes/web.php, строки 5 и 9 — ровно те номера, на которых мы написали Route::get('/', ...) и Route::get('/hello/{name}', ...). Проверить себя можно за секунду.

Методов у каждого маршрута два: GET|HEAD. HEAD — это GET без тела ответа: мы только что видели такой запрос в access.log ("HEAD /" 200 0).

Флаг --except-vendor убирает из списка маршруты, которые регистрирует сам каркас, и оставляет только наши; поэтому итог — Showing [2] routes. Служебный /up из bootstrap/app.php в этом списке не показан.

student@mint: ~/recoursia/project-laravel438 мс
student@mint:~/recoursia/project-laravel$
Проверяем, что .env снаружи не отдаётся — location ~ /. { deny all; } — иначе ключ приложения и пароль базы утекают любому посетителю.
Разбор команды

Проверяем второй рубеж защиты: файл .env с ключом приложения и доступом к базе не должен отдаваться снаружи.

curl -sS -o /dev/null -w "GET /.env -> %{http_code}\n" http://project-laravel.rs/.env
ТокенЗачем он и что меняет
curlHTTP-клиент
-sSтихо, но с сообщениями об ошибках
-o /dev/nullтело ответа выбрасываем. Здесь это принципиально: если бы файл отдавался, секрет оказался бы в терминале и в истории
-w "GET /.env -> %{http_code}\n"подпись и код ответа одной строкой; \n не даёт строке слиться с приглашением
http://project-laravel.rs/.envзапрос к скрытому файлу в корне сайта — именно так это сделал бы злоумышленник

Что произойдёт

GET /.env -> 403.

На что смотреть

403, а не 404 — важная деталь. Файла public/.env не существует (в public/ четыре файла), «честным» ответом был бы 404. Но сработал location ~ /\. { deny all; }: nginx запрещает запрос, даже не проверяя наличие файла.

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

  • 404 вместо 403 означает, что запрета нет и вас защищает только root: ошибётесь в root — и .env уедет наружу. Такую ошибку мы сами сделали в варианте 1.
  • Утечка .env — это APP_KEY и доступ к базе: получив файл, посетитель сможет расшифровать куки приложения.

403, а не 404 — и это важная деталь.

Файла public/.env не существует: в public/ четыре файла, мы их перечисляли. «Честным» ответом был бы 404. Но сработал location ~ /\. { deny all; } — nginx запрещает запрос, даже не проверяя наличие файла.

Зачем второй рубеж, если root уже спрятал .env выше корня сайта? Затем, что root — настройка, которую меняют руками. Мы сами ошиблись в ней на этой же паре слайдов. Если когда-нибудь vhost уедет на папку проекта, запрет скрытых файлов всё равно не даст скачать .env с ключом приложения и доступом к базе.

Что проверили

GET / → 200, GET /hello/студент → 200, GET /.env → 403, route:list → 2 наших маршрута.

30листайте
nginx · результат

Те же страницы — теперь на домене project-laravel.rs

http://project-laravel.rs/
Маршрут возвращает строку — Laravel оборачивает её в ответ HTTP.
Маршрут возвращает строку — Laravel оборачивает её в ответ HTTP.
http://project-laravel.rs/hello/%D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82
Путь запроса: dnsmasq → 127.0.0.1 → nginx → PHP-FPM → Laravel.
Путь запроса: dnsmasq → 127.0.0.1 → nginx → PHP-FPM → Laravel.

Слева — корень домена: маршрут, который возвращает строку, и Laravel заворачивает её в полноценный HTTP-ответ. Справа — маршрут с параметром: строка, собранная шаблоном hello.blade.php.

Обратите внимание на адресную строку второго снимка: браузер сам закодировал кириллицу в %D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82. Путь этого запроса — dnsmasq, nginx с нашим vhost, unix-сокет, PHP-FPM и Laravel — целиком собран схемой на следующем слайде.

31листайте
Итог · как это работает

Жизненный цикл запроса целиком

Соберём в одну схему всё, что в этой части проверено захватами: девять узлов — девять шагов пути запроса GET /hello/студент.

DNS127.0.0.1порт 80/index.php?$query_stringSCRIPT_FILENAMEhandleRequestview('hello', …)буферБраузер — GET /hello/%D1%81%D1%82…Браузерdnsmasq — зона .rs → 127.0.0.1dnsmasqnginx — server_name, root publicnginxtry_files — файла и каталога нет → /index.phptry_filesPHP-FPM — unix-сокет php8.3-fpm.sockPHP-FPMpublic/index.php — autoload → bootstrap/app.phppublic/index.phpРоутер — routes/web.php:9, $name = студентРоутерBlade — storage/framework/views, подстановка через e()BladeОтвет — 200, Content-Type, Set-Cookie; строка в access.logОтветDNS127.0.0.1порт 80/index.php?$query_stringSCRIPT_FILENAMEhandleRequestview('hello', …)буферБраузер — GET /hello/%D1%81%D1%82…Браузерdnsmasq — зона .rs → 127.0.0.1dnsmasqnginx — server_name, root publicnginxtry_files — файла и каталога нет → /index.phptry_filesPHP-FPM — unix-сокет php8.3-fpm.sockPHP-FPMpublic/index.php — autoload → bootstrap/app.phppublic/index.phpРоутер — routes/web.php:9, $name = студентРоутерBlade — storage/framework/views, подстановка через e()BladeОтвет — 200, Content-Type, Set-Cookie; строка в access.logОтвет
Жизненный цикл GET /hello/студент: от браузера до строки в access.log
Версия PHP

8.3 и php8.3 в путях и именах сервисов этой части — версия стенда. Проверьте свою: php -v. Если она другая — замените 8.3 на свою во всех путях (/etc/php/…, /run/php/…), именах пакетов (php8.3-*) и сервисе (php8.3-fpm).

32листайте
Итог части

Что мы разобрали и где это лежит

nginx — vhost: server_name, root public, try_filesnginxvhost: server_name,root public,try_filespublic/index.php — точка входа: её выполняет PHP-FPMpublic/index.phpточка входа: еёвыполняет PHP-FPMbootstrap/app.php — маршруты, middleware, ошибкиbootstrap/app.phpмаршруты, middleware,ошибкиroutes/web.php — «метод + путь = действие»routes/web.php«метод + путь =действие»config/*.php + .env — имена настроек / значения машиныconfig/*.php +.envимена настроек /значения машиныresources/views/*.blade.php — HTML с подстановкамиresources/views/*.blade.phpHTML с подстановкамиstorage/framework/views/ — скомпилированный Blade → PHPstorage/framework/views/скомпилированныйBlade → PHPОтвет и логи — 200, Set-Cookie; строка в access.logОтвет и логи200, Set-Cookie;строка в access.logfastcgi в сокетrequirewithRoutingenv()view(…)компиляциябуферfastcgi в сокетrequirewithRoutingenv()view(…)компиляциябуферnginx — vhost: server_name, root public, try_filesnginxpublic/index.php — точка входа: её выполняет PHP-FPMpublic/index.phpbootstrap/app.php — маршруты, middleware, ошибкиbootstrap/app.phproutes/web.php — «метод + путь = действие»routes/web.phpconfig/*.php + .env — имена настроек / значения машиныconfig/*.php+ .envresources/views/*.blade.php — HTML с подстановкамиresources/views/*.blade.phpstorage/framework/views/ — скомпилированный Blade → PHPstorage/framework/views/Ответ и логи — 200, Set-Cookie; строка в access.logОтвет и логи
Путь запроса по файлам проекта — вся часть 2 одной схемой

Вне пути запроса, но в этой же части

  • app/Models/User.php — таблица users как класс (части 4–5);
  • database/migrations/ — схема базы в коде, up() / down() (часть 4);
  • artisan — те же bootstrap/app.php, но handleCommand();
  • /etc/nginx/sites-available/project-laravel.rs.conf + ссылка в sites-enabled/;
  • /var/log/nginx/error.log — почему не получилось, когда access.log сказал «нет».

Числа этой части

  • 4 файла в public/ — всё, что видит nginx;
  • 10 файлов конфигурации в config/;
  • 3 миграции из коробки → 3 таблицы (users, password_reset_tokens, sessions);
  • 2 маршрута показывает route:list --except-vendor;
  • 12 каталогов и 0 файлов в storage/ до первого запроса.

Измерения

  • 200 / 200 — / и /hello/студент через nginx на project-laravel.rs;
  • 403 — на /.env, и столько же вернул неверный root; 404 — маршруты без try_files;
  • 70 403 байта, 0.439242 с — стартовая страница каркаса через serve;
  • 435 байт, 0.029839 с — наш маршрут через serve;
  • 72 662 и 593 байта — два скомпилированных шаблона в storage/framework/views/;
  • Laravel Framework 13.37.0, nginx/1.24.0 (Ubuntu), сокет php8.3-fpm.sock.
Версия PHP

8.3 и php8.3 в командах выше — версия стенда. Проверьте свою: php -v. Если она другая — замените 8.3 на свою во всех путях (/etc/php/…, /run/php/…), именах пакетов (php8.3-*) и сервисе (php8.3-fpm).

33листайте
Самопроверка

Проверьте себя

Отвечайте своими словами, не подглядывая в слайды. Ответы — на следующем слайде.

  1. Почему root в nginx должен указывать на public/, а не на папку проекта? Какой конкретно файл утечёт, если указать неверно, и что в нём лежит?
  2. Через nginx / отвечает 200, а /hello/студент — 404 со страницей nginx/1.24.0 (Ubuntu). Какой директивы не хватает в vhost и почему без неё маршрут Laravel не срабатывает?
  3. Чем config/app.php отличается от .env? Где править APP_DEBUG, пароль базы и где живёт 'timezone' => 'UTC'?
  4. Что делает {{ $name }} в Blade кроме подстановки значения? От какой атаки это защищает и какой знакомый вам приём защиты играет ту же роль на стороне SQL?
  5. Куда Laravel складывает скомпилированные шаблоны и почему отсутствие прав на запись в этот каталог роняет сайт?
34листайте
Самопроверка · ответы

Коротко: ответы

1. Корень сайта — область, из которой nginx раздаёт файлы. Всё остальное лежит выше и по HTTP недоступно. При root на папке проекта снаружи виден .env — ключ приложения APP_KEY и доступ к базе. У нас в vhost есть второй рубеж: location ~ /\. { deny all; }, поэтому GET /.env → 403 (а не 404: nginx запрещает запрос, не проверяя наличие файла).

2. try_files $uri $uri/ /index.php?$query_string;. Без неё nginx обслуживает запрос как запрос к файлу: ищет public/hello/студент на диске, не находит и сам отдаёт 404 — до index.php и таблицы маршрутов дело не доходит. В error.log это выглядит как open() ".../public/hello/студент" failed (2: No such file or directory).

3. config/*.php — общие для всех окружений, лежат в git, значения читают через env('КЛЮЧ', 'по умолчанию'). .env — секреты и отличия конкретной машины, в git не попадает (он в .gitignore). APP_DEBUG и пароль базы — в .env; 'timezone' => 'UTC' — прямо в config/app.php, строка 68, без env().

4. Прогоняет значение через e(): < и > заменяются на &lt; и &gt;, поэтому данные из адреса не становятся разметкой. Это защита от XSS. На стороне SQL ту же работу делает подготовленный запрос с маркерами (PDO): значение передаётся отдельно от текста запроса и не может изменить его структуру. Общее правило — не доверять данным клиента.

5. storage/framework/views/ — файлы с именами-хэшами; в нашем прогоне их два, 72 662 и 593 байта. Blade-метки — не PHP: шаблон сначала компилируется, и результат нужно записать. Нет прав на запись у пользователя PHP-FPM — нечего выполнять, запрос падает. То же касается bootstrap/cache/: там кэш конфигурации и сервисов. Права мы проверяли: drwxrwxrwx, владелец student — тот же пользователь, от которого работает веб-стек.

35листайте
Что дальше

Что дальше

Часть 3 — контроллеры, Blade и список пользователей из массива.

Что возьмём с собой из этой части:

  • проект ~/recoursia/project-laravel и vhost project-laravel.rs — сайт уже отвечает через nginx, домен менять не будем;
  • routes/web.php с двумя маршрутами — их мы перепишем;
  • понимание, что шаблон компилируется в storage/framework/views/, а значит, права на запись там должны остаться рабочими.

Что будет нового:

  • замыкания в маршрутах заменим на контроллеры: метод класса вместо анонимной функции — их создадим командой php artisan make:controller;
  • макет страницы: общий каркас с шапкой и меню, от которого наследуются остальные шаблоны;
  • список пользователей из массива с выводом в таблицу — та же задача, что вы решали в lesson.http.i.04, только через Blade и без ручного echo;
  • свои стили: положим public/css/app.css и подключим <link> — сборщик Vite для этого не нужен.
Подготовка

Ничего переустанавливать не требуется: нужны проект из части 1, работающий vhost из этой части и запущенные nginx с PHP-FPM. Проверить, что всё на месте, можно двумя командами: curl -sS -o /dev/null -w "%{http_code}\n" http://project-laravel.rs/ и php artisan route:list --except-vendor.

36листайте