Laravel · Часть 6 — Эксплуатация: ошибки, кэш, тесты, деплой
1 / 1
lesson.laravel · часть 6

Часть 6 — Эксплуатация: ошибки, кэш, тесты, деплой

Приложение написано и работает. Осталось научиться его чинить и переносить на другую машину.

Что сделаем: воспроизведём две настоящие ошибки 500 (нет прав на storage и пустой APP_KEY), научимся читать laravel.log, включим и выключим кэш, починим упавший тест и соберём чек-лист переноса на сервер.

Что понадобится: тот же стенд (Linux Mint 22, PHP 8.3, nginx + зона .rs, MariaDB) и проект ~/recoursia/project-laravel в том состоянии, в котором мы оставили его в части 5. Все выводы в этой части — настоящие захваты прогона на стенде.

1листайте
Часть 6 · Приборная панель

php artisan about — с чего начинается любая диагностика

Когда приложение ведёт себя странно, первую команду вводят не «наугад», а ту, которая показывает состояние целиком. В части 1 мы запускали её сразу после установки; теперь посмотрим, что изменилось.

student@mint: ~/recoursia/project-laravel869 мс
student@mint:~/recoursia/project-laravel$
  Cache ......................................................................    Config .......................................................... NOT CACHED    Events .......................................................... NOT CACHED    Routes .......................................................... NOT CACHED    Views ............................................................... CACHED    Drivers ....................................................................    Broadcasting ........................................................... log    Cache ............................................................. database    Database ............................................................. mysql    Logs ........................................................ stack / single    Mail ................................................................... log    Queue ............................................................. database    Session ........................................................... database    Storage ....................................................................    public/storage .................................................. NOT LINKED    Locales ....................................................................    Installed ............................................................... ru    LaravelLang\Lang\Plugin ............................................ 15.37.4    Locales Version ..................................................... 2.11.0    Protected ............................................................... ru    Publisher Version ................................................... 16.8.0  
Итоговая сводка приложения — Теперь видно: MySQL, локаль ru, APP_URL project-laravel.rs.
Разбор команды

Любая диагностика приложения начинается с одной команды: about печатает состояние целиком — версии, окружение, драйверы, кэш, хранилище, локали. В части 1 мы запускали её сразу после установки; теперь смотрим, что изменилось.

php artisan about
ТокенЗачем он и что меняет
phpинтерпретатор PHP в консоли. 8.3 — версия стенда; подставьте свою (php -v). Запуск идёт напрямую из CLI, не через nginx и PHP-FPM.
artisanфайл-обёртка в корне проекта: поднимает ядро Laravel и передаёт ему команду из аргументов. Работает только из каталога проекта, где этот файл лежит.
aboutимя команды — сводка о приложении. Флагов и аргументов нет, поэтому печатается всё: в захвате пять блоков (Environment, Cache, Drivers, Storage, Locales).

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

Печатает сводку: Environment (Laravel Version 13.37.0, PHP Version 8.3.6, Composer Version 2.7.1, Environment local, Debug Mode ENABLED, Maintenance Mode OFF, Timezone UTC, Locale ru), затем Cache, Drivers, Storage и Locales. Код возврата 0.

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

Четыре строки Environment отвечают на вопросы любой диагностики: Environment local — значение APP_ENV из .env; Debug Mode ENABLED — APP_DEBUG=true, подробности ошибок видны пользователю; URL project-laravel.rs — домен из APP_URL; версии — с ними сверяют документацию. В Cache: Config NOT CACHED, Routes NOT CACHED, Views CACHED (шаблоны компилируются сами, по мере запросов). В Drivers: Database mysql, Session/Cache/Queue database, Logs stack / single, Mail log. В Storage: public/storage NOT LINKED. В Locales: Installed ru и LaravelLang\Lang\Plugin 15.37.4.

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

  • could not open input file: artisan — команду запустили не из каталога проекта. И учтите: если конфигурация закэширована (config:cache), about показывает значения из кэша, а не из .env — свежую правку файла вы в нём не увидите.

Блок Environment отвечает на четыре вопроса, которые приходится задавать при любой ошибке.

Строка Что означает
Laravel Version 13.37.0, PHP 8.3.6, Composer 2.7.1 версии фреймворка, языка и пакетного менеджера — с ними сверяют документацию
Environment local значение APP_ENV из .env; «локальная разработка», а не боевой сервер
Debug Mode ENABLED APP_DEBUG=true: пользователю показывается подробная страница ошибки. В разработке удобно, на сервере — утечка внутренностей
URL project-laravel.rs APP_URL из .env — по этому адресу приложение себя знает

Рядом Maintenance Mode OFF (режим обслуживания выключен), Timezone UTC и Locale ru — русские сообщения валидации, которые мы подключили в части 5.

Версия PHP

PHP Version 8.3.6 — версия стенда, её печатает about. Проверьте свою: php -v. В командах этой части версия не встречается: всё запускается через php artisan, composer, curl, grep и mysql, без версионных путей (/etc/php/8.3/…, /run/php/…) и имени сервиса (php8.3-fpm) — заменять в них нечего.

2листайте
Часть 6 · Приборная панель

Там же: драйверы, кэш и хранилище

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

Драйвер Значение Почему так
Database mysql в части 4 мы переключили .env с SQLite на MariaDB
Session database сессии лежат в таблице sessions, а не в файлах
Cache database кэш приложения — таблица cache
Queue database задачи очередей — таблицы jobs, failed_jobs
Logs stack / single один файл журнала, без ротации по дням
Mail log письма не отправляются, а пишутся в журнал
Broadcasting log события никуда не транслируются

Cache. Config NOT CACHED, Routes NOT CACHED, Views CACHED. Шаблоны компилируются сами, по мере запросов, — поэтому они уже в кэше. Кэш конфигурации и маршрутов сам не появляется: его включают рукой. Займёмся этим ниже.

Storage. public/storage NOT LINKED — нет ссылки из public/storage в storage/app/public. Она нужна, чтобы отдавать загруженные пользователями файлы по HTTP. Загрузок у нас нет, поэтому и ссылки нет.

Locales. Installed ru, LaravelLang\Lang\Plugin 15.37.4 — тот самый пакет перевода из части 5.

Одна команда вместо десятка проверок

about собирает то, ради чего в «чистом PHP» пришлось бы писать phpinfo() и заглядывать в конфиги. Запомните её: почти любая диагностика начинается здесь.

3листайте
Часть 6 · Ошибка №1

500 из-за прав на storage/framework/views

Это ошибка номер один при переносе на сервер. Воспроизведём её специально — чтобы потом узнавать за секунды. В блоке видно все шесть команд: удаляем скомпилированные шаблоны, закрываем папку на запись, запрашиваем /users, смотрим журнал, возвращаем права, запрашиваем снова.

student@mint: ~/recoursia/project-laravel738 мс
student@mint:~/recoursia/project-laravel$
>
>
>
>
>
Ошибка 500: нет прав на запись в storage/framework/views — Blade компилирует шаблоны в эту папку; без права записи запрос падает. Это ошибка №1 при переносе на сервер.
Разбор команды

Ошибка номер один при переносе на сервер, воспроизведённая специально: убираем скомпилированные шаблоны, закрываем папку на запись и смотрим, что происходит с запросом.

rm -f storage/framework/views/*.php
ТокенЗачем он и что меняет
rmудаляет файлы.
-fне спрашивать подтверждения и не сообщать об ошибке, если файла нет: эксперимент не остановится, даже если папка уже пуста.
storage/framework/views/*.phpпуть с подстановкой: * раскрывает оболочка ещё до запуска rm, подставляя список скомпилированных Blade-шаблонов.

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

Скомпилированные шаблоны удалены, команда ничего не печатает. Без этого шага эксперимент ничего бы не показал: about выше сообщал Views CACHED, то есть готовые PHP-файлы лежали в папке, и Blade просто подключал бы их, ничего не записывая.

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

Пустой вывод здесь и есть успех. Убедиться, что папка пуста, можно одной командой: ls storage/framework/views.

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

  • * раскрывает shell, а не rm, а -f глушит ошибку «нет такого файла»: опечатка в пути пройдёт молча — удалится не то или ничего.
chmod 500 storage/framework/views
ТокенЗачем он и что меняет
chmodменяет права доступа к файлу или каталогу.
500числовая запись прав: 5 = r-x владельцу (4 — чтение, 1 — вход в каталог), 0 группе и 0 остальным. Читать и входить можно, писать нельзя.
storage/framework/viewsкаталог, куда Blade кладёт скомпилированные шаблоны.

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

Каталог закрыт на запись. Вывода нет; в листинге права будут видны как dr-x------ (d — каталог, r-x у владельца, у группы и остальных ничего).

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

500 строже привычного 755: у группы и остальных нет даже чтения. Для эксперимента достаточно снять право записи у владельца — на стенде nginx и PHP-FPM работают от вашего пользователя (часть 1), поэтому писать в папку пытается именно он.

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

  • На чужом сервере один chmod может не помочь: если файлы скопированы от root, а PHP-FPM работает от отдельного пользователя, менять нужно владельца каталогов storage/ и bootstrap/cache/, а не только цифры прав.
curl -sS -o /dev/null -w "GET /users при закрытой папке views -> %{http_code}\n" http://project-laravel.rs/users
ТокенЗачем он и что меняет
curlHTTP-клиент: делает настоящий запрос через nginx и PHP-FPM, как браузер.
-sSдва флага слитно: -s (silent) убирает прогресс-бар, -S (show-error) оставляет сообщения об ошибках. Без -S в тихом режиме не видно даже того, что запрос не удался.
-o /dev/nullтело ответа отправить в «никуда»: нужен только код, а не HTML страницы ошибки.
-wwrite-out: напечатать строку по шаблону после завершения запроса.
"GET /users при закрытой папке views -> %{http_code}\n"шаблон строки в двойных кавычках: обычный текст плюс %{http_code} — код ответа, который curl подставит сам, и \n — перевод строки. Кавычки нужны, чтобы шаблон с пробелами дошёл до curl одним аргументом.
http://project-laravel.rs/usersадрес страницы: домен учебной зоны .rs и маршрут /users из нашего приложения.

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

Одна строка: GET /users при закрытой папке views -> 500. Тело ответа выброшено, код подставлен в шаблон.

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

500, а не 403: файл nginx отдал, PHP-FPM запустил приложение, а упало оно внутри — на попытке записать скомпилированный шаблон. Права здесь проверяет процесс PHP, а не nginx.

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

  • Без -o /dev/null на экран вывалится HTML страницы ошибки; без \n строка склеится с приглашением оболочки. %{http_code} пишется именно в фигурных скобках.
grep -a "local.ERROR" storage/logs/laravel.log | tail -1 | cut -c1-160
ТокенЗачем он и что меняет
grepищет в файле строки, подходящие под шаблон.
-aобрабатывать файл как текст: в журнале попадаются байты, которые grep считает двоичными, и без -a вместо текста будет binary file matches.
"local.ERROR"шаблон: окружение из APP_ENV (local) и уровень (ERROR). Кавычки защищают шаблон от shell.
storage/logs/laravel.logжурнал приложения. Драйвер stack / single из about означает: пишется один файл, без ротации по дням.
|пайп: найденные строки идут дальше, в tail.
tailпечатает хвост потока.
-1одна последняя строка — самая свежая запись об ошибке.
|второй пайп: найденная строка уходит в cut.
cutрежет строку по символам или полям.
-c1-160оставить символы с 1-го по 160-й, чтобы строка влезла на экран. В работе удобнее читать запись целиком, без обрезки.

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

Одна строка журнала, обрезанная до 160 символов: [2026-10-17 10:22:46] local.ERROR: tempnam(): file created in the system's temporary directory {"exception":"[object] (ErrorException(code: 0): tempnam(): file — обрыв на середине это работа cut, а не повреждённый файл.

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

tempnam() — PHP не смог создать файл в нужной папке и полез в системный каталог временных файлов; Laravel превращает такое предупреждение в исключение (ErrorException) и отдаёт 500. Перед сообщением — дата и время в квадратных скобках: сверяйте их с моментом запроса.

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

  • tail -1 laravel.log без grep почти бесполезен: одна запись — это сообщение плюс десятки строк стектрейса, поэтому последняя строка файла чаще всего бессмысленный обрывок. Искать нужно строку с уровнем ошибки и читать запись от неё вниз.
chmod 775 storage/framework/views
ТокенЗачем он и что меняет
chmodвозвращает права на место.
775владельцу и группе rwx (7), остальным r-x (5): запись разрешена владельцу и группе, читать и входить могут все.
storage/framework/viewsтот же каталог, который закрывали предыдущей командой.

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

Права восстановлены, вывода нет; в листинге каталог будет виден как drwxrwxr-x.

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

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

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

  • 777 тоже «починит», но откроет каталог на запись всем — на сервере так делать нельзя.
curl -sS -o /dev/null -w "после возврата прав -> %{http_code}\n" http://project-laravel.rs/users
ТокенЗачем он и что меняет
curlтот же HTTP-запрос, что и до правки прав: сравниваем ответы.
-sSтихий режим, но с сообщениями об ошибках.
-o /dev/nullтело ответа не печатаем.
-wшаблон строки для вывода после запроса.
"после возврата прав -> %{http_code}\n"тот же приём: подпись своими словами, %{http_code} — код ответа, \n — перевод строки.
http://project-laravel.rs/usersтот же адрес, что и в первом запросе.

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

после возврата прав -> 200. Тот же запрос и тот же код приложения — изменилось только право записи в каталог.

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

Пара 500 → 200 при неизменном коде: дело было не в коде, не в базе и не в маршрутах. Симптом, который стоит запомнить: 500 плюс запись в laravel.log о том, что файл не удалось создать или открыть на запись, = права на storage.

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

  • Если после возврата прав снова 500 — причина другая: повторите grep по журналу, читайте новую запись и проверьте владельца каталога, а не только цифры прав.

Разберём по шагам, иначе вывод выглядит магией.

1
Удалить скомпилированные шаблоны

rm -f storage/framework/views/*.php. Без этого шага эксперимент ничего не покажет: about выше сообщал Views CACHED, то есть готовые PHP-файлы уже лежали в папке. Пока они есть и не устарели, Blade их просто подключает и ничего не записывает.

2
Закрыть папку на запись

chmod 500 storage/framework/views. В блоке прав (lesson.programs.02) вы разбирали числовую запись: 500 — это r-x для владельца, то есть читать и входить можно, писать нельзя.

3
Запросить страницу

GET /users -> 500. Blade понадобилось скомпилировать шаблон заново, создать файл не удалось — Laravel остановил запрос и отдал 500.

4
Прочитать причину в журнале

grep -a "local.ERROR" storage/logs/laravel.log | tail -1 — последняя запись об ошибке: tempnam(): file created in the system's temporary directory. PHP не смог создать файл в нужной папке и полез в системный каталог временных файлов; Laravel превращает такое предупреждение в исключение.

5
Вернуть права

chmod 775 — владелец и группа снова могут писать. Тот же запрос даёт 200.

4листайте
Часть 6 · Ошибка №1

Почему так происходит и как не наступить на сервере

Причина не в Laravel, а в том, что приложение пишет на диск во время запроса. Blade-шаблон не исполняется напрямую: сначала он компилируется в обычный PHP-файл, и этот файл кладётся в storage/framework/views (мы видели эти файлы в части 2). Компиляция происходит, когда исходный шаблон новее скомпилированного или когда скомпилированного файла нет вовсе.

Отсюда два вывода, которые спасают часы на боевом сервере.

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

2. Важен владелец файлов, а не только цифры прав. На вашем стенде nginx и PHP-FPM запущены от вашего пользователя (часть 1), поэтому владельцем storage оказываетесь вы, и запись разрешена. На чужом сервере PHP-FPM часто работает от отдельного пользователя: если файлы скопированы от root, то chmod уже не помогает — нужно менять владельца папок storage/ и bootstrap/cache/.

Симптом, который нужно запомнить

500 в браузере + запись в laravel.log о том, что файл не удалось создать или открыть на запись (у нас — tempnam(): file created in the system's temporary directory) = права на storage. Не код, не база, не маршруты.

Что проверить при переносе

Владелец storage/ и bootstrap/cache/ — пользователь, от которого работает PHP-FPM; у обоих каталогов есть право записи владельцу и группе. В нашем эксперименте рабочим оказалось 775.

5листайте
Часть 6 · Ошибка №2

500 из-за пустого APP_KEY

Ещё одна частая ошибка после переноса: код скопировали, а .env — нет (его не коммитят), либо скопировали без значения APP_KEY. Проверяем: обнуляем APP_KEY в .env, запрашиваем страницу, возвращаем файл на место.

student@mint: ~/recoursia/project-laravel1539 мс
student@mint:~/recoursia/project-laravel$
>
>
>
>
>
>
Что будет без APP_KEY — Ключом шифруются куки и сессии; без него Laravel отказывается стартовать запрос.
Разбор команды

Вторая частая ошибка после переноса: код скопировали, а .env — нет (его не коммитят) либо скопировали без значения APP_KEY. Обнуляем ключ, смотрим на ответ, возвращаем файл на место.

cp .env /tmp/env.keep
ТокенЗачем он и что меняет
cpкопирует файл.
.envнастройки приложения: APP_KEY, APP_ENV, APP_DEBUG, доступы к базе. Файл не коммитят в git, поэтому при переносе на сервер его чаще всего и теряют.
/tmp/env.keepкуда кладём копию: каталог временных файлов, имя выбираем сами — важно его не потерять.

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

Резервная копия .env сделана, вывод пустой. Без копии возвращать ключ пришлось бы генерацией нового, а это сделало бы недействительными текущие сессии.

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

Команда ничего не печатает — успех проверяется наличием файла-копии.

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

  • cp молча перезаписывает существующий файл назначения: если /tmp/env.keep уже был, прежняя копия потеряется.
sed -i "s/^APP_KEY=.*/APP_KEY=/" .env
ТокенЗачем он и что меняет
sedпотоковый редактор строк.
-iправить файл на месте (in-place): без этого флага sed печатает результат в stdout, а файл остаётся прежним.
"s/^APP_KEY=.*/APP_KEY=/"команда подстановки s/что/чем/: ^APP_KEY= — начало строки и буквальный текст, .* — всё до конца строки, то есть текущее значение ключа, а после второго / замена пустая, поэтому строка становится APP_KEY=. Двойные кавычки обязательны: без них shell раскрыл бы .* как шаблон имён файлов.
.envфайл, который правим.

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

Значение APP_KEY в .env стало пустым. Вывода нет: с -i sed печатает только ошибки.

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

^ в шаблоне важен: без него sed задел бы любую строку, где встречается APP_KEY=, в том числе закомментированную.

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

  • -i меняет файл безвозвратно, резервной копии sed не делает (для неё нужен -i.bak). Поэтому первой командой .env скопировали вручную.
curl -sS -o /dev/null -w "GET /users с пустым APP_KEY -> %{http_code}\n" http://project-laravel.rs/users
ТокенЗачем он и что меняет
curlHTTP-клиент: настоящий запрос через nginx и PHP-FPM.
-sSбез прогресс-бара, но с сообщениями об ошибках.
-o /dev/nullтело ответа в «никуда»: здесь нужен только код.
-wwrite-out: строка по шаблону после запроса.
"GET /users с пустым APP_KEY -> %{http_code}\n"подпись опыта своими словами, %{http_code} — код ответа, \n — перевод строки; всё в двойных кавычках, чтобы шаблон дошёл одним аргументом.
http://project-laravel.rs/usersадрес страницы списка пользователей.

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

GET /users с пустым APP_KEY -> 500. Без ключа Laravel отказывается обслуживать запрос.

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

Это не 404 и не ошибка nginx: запрос дошёл до PHP, приложение поднялось и остановилось на проверке ключа. Сам по себе код 500 причину не объясняет — её читают в теле ответа (следующая команда) или в журнале.

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

  • Если оставить -o /dev/null и не читать тело ответа, останется только цифра 500: причина в ней не видна.
curl -sS http://project-laravel.rs/users | grep -o "No application encryption key has been specified\." | head -1
ТокенЗачем он и что меняет
curlHTTP-запрос к странице.
-sSтихий режим с сообщениями об ошибках.
http://project-laravel.rs/usersадрес; -o /dev/null здесь нет — тело ответа нам нужно.
|пайп: HTML ответа уходит в grep.
grepищет строку по шаблону.
-oпечатать только совпавшую часть строки, а не всю строку: из страницы ошибки остаётся одно предложение.
"No application encryption key has been specified\."текст сообщения. \. в конце — экранированная точка, «буквально точка»: без экранирования точка означала бы «любой символ».
|второй пайп — в head.
headпечатает начало потока.
-1одна первая строка: гарантия, что в выводе одно сообщение, даже если в HTML оно встречается несколько раз.

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

Одна строка: No application encryption key has been specified. — сообщение об ошибке, доставшееся прямо из HTML-ответа.

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

Так подробно ответ выглядит только потому, что Debug Mode ENABLED (APP_DEBUG=true из about). На сервере с APP_DEBUG=false пользователь получит страницу 500 без подробностей, а вся информация останется в laravel.log.

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

  • Без -o grep напечатает строку целиком: страница ошибки — это длинный HTML в одну строку, и в терминале будет каша.
cp /tmp/env.keep .env
ТокенЗачем он и что меняет
cpкопирует файл.
/tmp/env.keepисточник — копия, сделанная первой командой.
.envназначение: файл настроек возвращается на место вместе с ключом.

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

.env снова с ключом; вывод пустой.

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

Перезагружать nginx и PHP-FPM не пришлось: кэш конфигурации снят (optimize:clear), поэтому приложение читает .env заново на каждом запросе — следующий curl уже даёт 200.

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

  • Порядок аргументов cp — источник, затем назначение. Перепутав их, вы перезапишете резервную копию испорченным файлом.
curl -sS -o /dev/null -w "после возврата ключа -> %{http_code}\n" http://project-laravel.rs/users
ТокенЗачем он и что меняет
curlтот же запрос, что и до правки .env.
-sSтихий режим с сообщениями об ошибках.
-o /dev/nullтело ответа не печатаем — нужен код.
-wwrite-out: строка по шаблону.
"после возврата ключа -> %{http_code}\n"подпись, %{http_code} — код ответа, \n — перевод строки.
http://project-laravel.rs/usersтот же адрес: сравниваем ответ до и после.

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

после возврата ключа -> 200. Ни одной правки кода: менялось только значение в .env.

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

Пара 500 → 200 доказывает, что причина была именно в ключе. И что .env читается на запросе, а не при старте сервиса.

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

  • Если после возврата .env всё ещё 500 — включён кэш конфигурации: приложение читает значения из bootstrap/cache, а не из файла. Снимите кэш (php artisan optimize:clear) или пересоберите его после правки.
php artisan key:generate --show
ТокенЗачем он и что меняет
phpинтерпретатор PHP в консоли; 8.3 — версия стенда, подставьте свою (php -v).
artisanфайл-обёртка в корне проекта: передаёт команду ядру Laravel; запуск из каталога проекта.
key:generateсгенерировать новый ключ приложения: 32 случайных байта в кодировке base64.
--showнапечатать ключ и НЕ трогать .env: без флага команда перезаписала бы значение APP_KEY в файле.

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

Одна строка с новым ключом: base64:+ojjAGd8wuPdgREfJ2Hag/vcsl7GQNg10MpdX/BveHE=. Значение APP_KEY в .env команда при этом не меняет.

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

Префикс base64: — часть значения, его не обрезают. 44 символа после него — это 32 байта случайных данных в base64. Именно так ключ получают для боевого сервера: сгенерировали, вставили в .env вручную.

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

  • Без --show ключ перезапишется в .env, и всё зашифрованное прежним ключом перестанет расшифровываться: текущие сессии станут недействительными (пользователей разлогинит), зашифрованные данные будут потеряны. Поэтому на каждом окружении ключ генерируют один раз.

Без ключа Laravel отказывается обслуживать запрос: 500 и сообщение No application encryption key has been specified. Как только ключ вернулся в .env — снова 200. Обратите внимание: сообщение об ошибке мы достали прямо из HTML-ответа. Это работает, потому что Debug Mode ENABLED (слайд с about): в таком режиме приложение показывает подробности. На сервере с APP_DEBUG=false пользователь получит страницу 500 без подробностей, а вся информация останется только в журнале.

Последняя команда — php artisan key:generate --show:

text
base64:+ojjAGd8wuPdgREfJ2Hag/vcsl7GQNg10MpdX/BveHE=

Флаг --show печатает ключ и не трогает .env — именно так ключ получают для боевого сервера: сгенерировали, вставили в .env вручную. 44 символа в base64 — это 32 байта случайных данных.

6листайте
Часть 6 · Ошибка №2

Зачем вообще нужен APP_KEY

Ключ — это секрет всего приложения. Им шифруются данные, которые приложение отдаёт наружу и потом читает обратно.

  • Кука сессии. В части 2 в заголовках ответа мы видели laravel_session и XSRF-TOKEN. Содержимое этих кук подписано и зашифровано ключом: подделать или прочитать его без ключа нельзя.
  • CSRF-токен. Форма из части 5 присылает _token, сервер сверяет его с токеном в сессии. А сессия опознаётся по куке, зашифрованной ключом: нет ключа — нет ни сессии, ни проверки токена, ни отправки форм.
  • Данные, зашифрованные через криптографию Laravel (например, значения полей модели, помеченные как шифруемые).
Ключ нельзя «просто поменять»

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

Почему в вашем проекте ключ уже есть

Установщик из части 1 сгенерировал его сам при создании проекта. Теряют ключ обычно при переносе: .env копируют вручную и забывают строку APP_KEY.

7листайте
Часть 6 · Журнал

Как читать storage/logs/laravel.log

Обе ошибки выше мы нашли в одном файле. Путь к нему виден в командах экспериментов, а в дереве проекта есть каталог storage/logs. Драйвер stack / single из about означает: пишется один файл, без ротации по дням.

Запись в журнале выглядит так (строка из захвата с правами):

text
[2026-10-17 10:22:46] local.ERROR: tempnam(): file created in the system's temporary directory {"exception":"[object] (ErrorException(code: 0): tempnam(): file

Разбор по частям:

  • [2026-10-17 10:22:46] — дата и время;
  • local.ERROR — окружение из APP_ENV и уровень (бывают WARNING, INFO, DEBUG);
  • дальше — короткое сообщение, а за ним JSON с исключением и стектрейсом.
Почему tail -1 laravel.log почти бесполезен

Стектрейс занимает десятки строк, поэтому последняя строка файла — это хвост записи, часто бессмысленный. Искать нужно строку с уровнем ошибки, а потом читать запись от неё вниз:

bash
grep -a "local.ERROR" storage/logs/laravel.log | tail -1 | cut -c1-160

Именно так мы и сделали в эксперименте с правами. Флаг -a нужен, потому что в журнале встречаются байты, которые grep считает двоичными и отказывается печатать; cut -c1-160 просто обрезает строку до 160 символов, чтобы она влезла на экран — в работе удобнее читать запись целиком.

Журналов два, и они про разное

laravel.log — что случилось внутри PHP-кода. error.log nginx (блок Linux, lesson.linux.06) — что случилось до PHP: не найден файл, недоступен сокет PHP-FPM, неверный root. Если Laravel не запустился вовсе, в laravel.log может не появиться ничего — тогда смотрите в nginx.

8листайте
Часть 6 · Кэш

Что именно кэширует Laravel

Три команды — и приложение перестаёт читать свои же файлы на каждом запросе.

student@mint: ~/recoursia/project-laravel2355 мс
student@mint:~/recoursia/project-laravel$
>
>
>
>
  Drivers ....................................................................    Cache ............................................................. database    Database ............................................................. mysql    Logs ........................................................ stack / single    Mail ................................................................... log    Queue ............................................................. database    Session ........................................................... database    Storage ....................................................................    public/storage .................................................. NOT LINKED    Locales ....................................................................    Installed ............................................................... ru    LaravelLang\Lang\Plugin ............................................ 15.37.4    Locales Version ..................................................... 2.11.0    Protected ............................................................... ru    Publisher Version ................................................... 16.8.0     INFO  Clearing cached bootstrap files.    config ......................................................... 1.84ms DONE  cache ......................................................... 26.11ms DONE  compiled ....................................................... 1.70ms DONE  events ......................................................... 1.28ms DONE  routes ......................................................... 1.41ms DONE  views ......................................................... 13.98ms DONE
Кэширование конфигурации, маршрутов и шаблонов — В проде это убирает чтение файлов на каждый запрос; в разработке кэш мешает — поэтому optimize:clear.
Разбор команды

Три команды убирают чтение собственных файлов приложения на каждом запросе, четвёртая показывает результат, пятая возвращает стенд в состояние разработки.

php artisan config:cache
ТокенЗачем он и что меняет
phpинтерпретатор PHP в консоли; 8.3 — версия стенда, подставьте свою (php -v).
artisanфайл-обёртка в корне проекта: передаёт команду ядру Laravel; запуск из каталога проекта.
config:cacheимя команды: собрать конфигурацию в один кэш-файл. Десять файлов из config/ (они видны в дереве проекта) и значения из .env превращаются в готовый массив в bootstrap/cache; без кэша этот набор читается и разбирается заново на каждом запросе.

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

INFO Configuration cached successfully. — файл кэша конфигурации создан. Другого вывода нет, код возврата 0.

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

Проверяется следующим вызовом about: строка Config меняется с NOT CACHED на CACHED.

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

  • Пока кэш конфигурации включён, приложение читает значения из кэша, а не из .env: правите файл, перезагружаете страницу — ничего не меняется. После любой правки .env кэш пересобирают (config:cache) или снимают (optimize:clear).
php artisan route:cache
ТокенЗачем он и что меняет
phpинтерпретатор PHP в консоли; 8.3 — версия стенда, подставьте свою (php -v).
artisanфайл-обёртка в корне проекта: передаёт команду ядру Laravel; запуск из каталога проекта.
route:cacheсохранить таблицу маршрутов одним файлом: роутеру не нужно подключать routes/web.php и перебирать регистрации маршрутов, чтобы найти подходящий.

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

INFO Routes cached successfully. — таблица маршрутов записана в кэш.

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

В about строка Routes становится CACHED; route:list показывает те же семь маршрутов.

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

  • Маршрут, добавленный в routes/web.php после сборки кэша, не заработает, пока кэш не пересобрать: приложение читает готовый файл, а не routes/web.php. В разработке этот кэш обычно держат выключенным.
php artisan view:cache
ТокенЗачем он и что меняет
phpинтерпретатор PHP в консоли; 8.3 — версия стенда, подставьте свою (php -v).
artisanфайл-обёртка в корне проекта: передаёт команду ядру Laravel; запуск из каталога проекта.
view:cacheскомпилировать все Blade-шаблоны заранее, а не в момент первого обращения к странице. Готовые PHP-файлы кладутся в storage/framework/views.

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

INFO Blade templates cached successfully. — шаблоны скомпилированы и лежат в storage/framework/views.

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

В about строка Views — CACHED. Это ровно та папка, права на которую дают 500 (захват 06-perm-error).

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

  • Команда пишет на диск, поэтому без права записи в storage/framework/views она не сработает — причина та же, что и у ошибки 500 в начале части.
php artisan about | sed -n "/Cache/,/Drivers/p"
ТокенЗачем он и что меняет
php artisan aboutта же сводка; дальше её вывод уходит в фильтр, а не на экран целиком.
|пайп: stdout about становится stdin для sed.
sedпотоковый редактор; здесь работает фильтром строк.
-nне печатать строки по умолчанию. Без этого флага sed вывел бы весь поток, а p добавил бы сверху ещё и выбранные строки — получились бы дубли.
"/Cache/,/Drivers/p"адрес-диапазон и действие: печатать (p) строки от первой, содержащей Cache, до первой, содержащей Drivers. Это поиск подстроки, а не номера строк. Двойные кавычки передают шаблон sed одним аргументом; / — разделители шаблона.

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

На экране — блок Cache после включения кэша: Config CACHED, Events NOT CACHED, Routes CACHED, Views CACHED, а затем всё до конца вывода about (Drivers, Storage, Locales).

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

Почему вывод длиннее одного блока: слово Cache встречается в about дважды — как заголовок секции и как строка драйвера. Первый диапазон закрывается на Drivers, затем sed открывает второй (от строки Cache … database) и, не найдя больше Drivers, печатает до конца вывода. Строка Broadcasting при этом пропущена: она не попала ни в один диапазон.

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

  • -n без p даёт пустой вывод, p без -n — каждую строку дважды. И диапазон sed ищет совпадение подстроки где угодно: второе совпадение меняет результат, что здесь и произошло.
php artisan optimize:clear
ТокенЗачем он и что меняет
phpинтерпретатор PHP в консоли; 8.3 — версия стенда, подставьте свою (php -v).
artisanфайл-обёртка в корне проекта: передаёт команду ядру Laravel; запуск из каталога проекта.
optimize:clearснять весь кэш одним вызовом: конфигурацию, кэш приложения, скомпилированные файлы, кэш событий, маршрутов и шаблонов.

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

INFO Clearing cached bootstrap files. и шесть строк с временем: config 1.84ms, cache 26.11ms, compiled 1.70ms, events 1.28ms, routes 1.41ms, views 13.98ms — все DONE. Приложение снова читает config/, routes/web.php и .env напрямую.

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

Все шесть строк DONE — кэш снят целиком. Дольше всех чистился cache (26.11ms): это кэш приложения (драйвер database из about), а не кэш конфигурации.

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

  • optimize:clear удаляет и скомпилированные шаблоны, поэтому следующий запрос компилирует их заново — в этот момент и вылезают проблемы с правами на storage. Правило: в разработке кэш выключен, на сервере включён и пересобирается при каждом деплое.
config/*.php — десять файлов и .envconfig/*.phproutes/web.php — регистрация маршрутовroutes/web.phpresources/views — шаблоны .blade.phpresources/viewsbootstrap/cache — конфигурация одним массивомbootstrap/cachebootstrap/cache — таблица маршрутовbootstrap/cachestorage/framework/views — готовые PHP-файлы шаблоновstorage/framework/viewsне читается с дискаconfig:cacheне читается с дискаroute:cacheне читается с дискаview:cacheconfig/*.php — десять файлов и .envconfig/*.phproutes/web.php — регистрация маршрутовroutes/web.phpresources/views — шаблоны .blade.phpresources/viewsbootstrap/cache — конфигурация одним массивомbootstrap/cachebootstrap/cache — таблица маршрутовbootstrap/cachestorage/framework/views — готовые PHP-файлы шаблоновstorage/framework/viewsне читается с дискаconfig:cacheне читается с дискаroute:cacheне читается с дискаview:cache
Что читается с диска и куда кладётся готовый результат
  • config:cache — готовый массив конфигурации вместо чтения десяти файлов config/ и .env на каждом запросе.
  • route:cache — таблица маршрутов собрана заранее: routes/web.php не подключается, регистрации не перебираются.
  • view:cache — шаблоны скомпилированы заранее, а не при первом обращении; это та самая папка, из-за прав на которую мы получали 500.

После трёх команд about показывает Config CACHED, Routes CACHED, Views CACHED; Events NOT CACHED — кэш событий мы не включали. Выигрыш объясняется жизненным циклом запроса из блока HTTP (lesson.network.02): PHP-процесс не хранит состояние между запросами, каждый запрос проходит путь от public/index.php заново, поэтому всё, что посчитано один раз и положено в файл, экономится на каждом запросе.

9листайте
Часть 6 · Кэш

В разработке кэш мешает. optimize:clear

Последняя команда в блоке выше — php artisan optimize:clear. Она снимает весь кэш одним вызовом; вот что она чистила и сколько это заняло:

Что сброшено Время
config 1.84 ms
cache 26.11 ms
compiled 1.70 ms
events 1.28 ms
routes 1.41 ms
views 13.98 ms
Ловушка config:cache

Пока конфигурация закэширована, приложение читает значения из кэша, а не из .env. Вы правите .env, перезагружаете страницу — и ничего не меняется. Это не мистика: после любой правки .env кэш конфигурации нужно пересобрать (config:cache) или снять (optimize:clear).

Правило, которое стоит запомнить:

  • разработка — кэш конфигурации и маршрутов выключен, иначе каждое изменение приходится «проталкивать» вручную;
  • боевой сервер — кэш включён и пересобирается при каждом деплое, потому что код и настройки на сервере уже не меняются между выкатами;
  • после очистки кэша шаблонов первое обращение к странице компилирует их заново — и именно в этот момент вылезают проблемы с правами на storage.
10листайте
Часть 6 · Тесты

Первая команда тестов — и она упала

До сих пор мы проверяли приложение руками: открывали страницу в браузере и смотрели. Теперь проверкой займётся машина. В каркасе уже лежат два теста — запускаем их как есть, ничего не меняя.

student@mint: ~/recoursia/project-laravelexit code 11327 мс
student@mint:~/recoursia/project-laravel$
Тесты из каркаса — Два теста: example и проверка, что главная страница отвечает 200.
Разбор команды

До сих пор приложение проверяли руками: открывали страницу и смотрели. Запускаем тесты из каркаса как есть, ничего не меняя, — и читаем, что упало.

php artisan test
ТокенЗачем он и что меняет
phpинтерпретатор PHP в консоли; 8.3 — версия стенда, подставьте свою (php -v).
artisanфайл-обёртка в корне проекта: передаёт команду ядру Laravel; запуск из каталога проекта.
testимя команды: прогнать тестовый набор. Аргументов нет, поэтому запускаются все тесты из tests/ — в захвате видны оба каталога, tests/Unit и tests/Feature. Сам прогон делает PHPUnit: после него в дереве проекта появляется .phpunit.result.cache.

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

PASS Tests\Unit\ExampleTest — that true is true и FAIL Tests\Feature\ExampleTest — the application returns a successful response (0.23s). Итог: Tests: 1 failed, 1 passed (2 assertions), Duration: 0.42s. Код возврата 1 — именно его движок рисует как бейдж «exit code 1».

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

Сообщение провала: Expected response status code [200] but received 302. и Failed asserting that 302 is identical to 200. Ниже показано место — at tests/Feature/ExampleTest.php:17 и фрагмент кода со стрелкой ➜ на строке $response->assertStatus(200);. Искать строку в файле не нужно.

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

  • 302 здесь не поломка: в части 5 маршрут / стал редиректом на /users (в route:list это замыкание в routes/web.php:6), а тест из каркаса ждёт 200. Тест падает, когда поведение изменилось, — чинить нужно либо код, либо тест; удалять тест, чтобы не мешал, нельзя.

Бейдж exit code 1 в шапке — настоящий: команда действительно завершилась ошибкой, так тестовый набор сообщает о провале.

  • PASS Tests\Unit\ExampleTest — that true is true. Заглушка из каркаса: проверяет, что тестовая обвязка вообще работает. Пользы в ней нет, папка tests/Unit — для тестов отдельных классов без HTTP и базы.
  • FAIL Tests\Feature\ExampleTest — the application returns a successful response. Папка tests/Feature — для тестов «сквозных»: они дёргают приложение по HTTP, как настоящий браузер.
  • Итог: 1 failed, 1 passed (2 assertions), Duration: 0.42s.
11листайте
Часть 6 · Тесты

Почему 302 вместо 200

Вот тест, который упал. Он целиком помещается в один экран.

tests/Feature/ExampleTest.php
1<?php23namespace Tests\Feature;45// use Illuminate\Foundation\Testing\RefreshDatabase;6use Tests\TestCase;78class ExampleTest extends TestCase9{10 /**11 * A basic test example.12 */13 public function test_the_application_returns_a_successful_response(): void14 {15 $response = $this->get('/');1617 $response->assertStatus(200);18 }19}

Строка $response = $this->get('/'); выполняет настоящий HTTP-запрос к приложению, но внутри PHP: без nginx, без браузера, без сети. Laravel поднимает ядро приложения, проводит через него запрос GET / и возвращает объект-ответ, у которого можно спросить код, заголовки и содержимое.

$response->assertStatus(200); — утверждение: «мы ожидаем код 200». Тест получил 302:

text
Expected response status code [200] but received 302.Failed asserting that 302 is identical to 200.

И это не поломка. 302 — редирект, и сделали его мы сами: в части 5 маршрут / перестал показывать приветственную страницу и теперь перенаправляет на /users. В route:list (дальше в этой части) видно, что / — это замыкание в routes/web.php:6, а не контроллер.

Главный вывод про упавший тест

Тест падает, когда поведение изменилось. Он не знает, случайно это или намеренно, — решает человек. Либо поведение ошибочно и чинить надо код, либо поведение новое и тест нужно осознанно переписать. Худшее, что можно сделать, — удалить тест, чтобы не мешал.

Полезно и то, как показан провал: at tests/Feature/ExampleTest.php:17 и фрагмент кода со стрелкой ➜ на строке с утверждением. Искать место в файле не нужно — это ровно та строка 17, которая подсвечена в карточке файла выше.

12листайте
Часть 6 · Тесты

Тесты, которые описывают наше приложение

Переписываем тест так, чтобы он проверял не «что-нибудь работает», а реальное поведение нашего приложения: редирект, страницу списка и создание записи через форму.

tests/Feature/ExampleTest.php
1<?php23namespace Tests\Feature;45use Illuminate\Foundation\Testing\RefreshDatabase;6use Tests\TestCase;78class ExampleTest extends TestCase9{10 use RefreshDatabase;1112 public function test_root_redirects_to_users(): void13 {14 $this->get('/')->assertRedirect('/users');15 }1617 public function test_users_page_returns_a_successful_response(): void18 {19 $this->get('/users')->assertStatus(200);20 }2122 public function test_user_can_be_created(): void23 {24 $this->post('/users', [25 'name' => 'Тестов Тест Тестович',26 'phone' => '+7 (900) 111-22-33',27 'email' => 'test@example.com',28 'password' => 'secret123',29 ])->assertRedirect(route('users.index'));3031 $this->assertDatabaseHas('users', ['email' => 'test@example.com']);32 }33}

use RefreshDatabase; — строка, которую в каркасном файле мы раскомментировали (в исходнике она была комментарием). Перед тестами поднимается схема базы в тестовом окружении — SQLite :memory:, — и после каждого теста всё откатывается. Рабочая база project_laravel не затрагивается: поэтому в списке пользователей не появляется test@example.com.

assertRedirect('/users') — фиксирует наше новое поведение корня сайта.

assertStatus(200) — страница списка открывается.

Третий тест делает то же, что делает пользователь в форме из части 5: POST /users с полями name, phone, email, password — те самые, которые принимает контроллер. Дальше два утверждения: редирект на route('users.index') и assertDatabaseHas('users', ['email' => ...]), то есть запись действительно легла в таблицу.

Заметьте: токена _token в запросе нет, а 419 мы не получили. В тестовом окружении проверка CSRF отключена — иначе тесты зависели бы от сессии и токена. В браузере, как вы видели в части 5, без токена будет 419.

13листайте
Часть 6 · Тесты

Зелёные

student@mint: ~/recoursia/project-laravel1503 мс
student@mint:~/recoursia/project-laravel$
Тесты зелёные
Разбор команды

Тест переписан под реальное поведение приложения: редирект, страница списка, создание записи через форму. Команда та же — сравниваем вывод.

php artisan test
ТокенЗачем он и что меняет
phpинтерпретатор PHP в консоли; 8.3 — версия стенда, подставьте свою (php -v).
artisanфайл-обёртка в корне проекта: передаёт команду ядру Laravel; запуск из каталога проекта.
testта же команда без аргументов: набор снова запускается целиком, и tests/Unit, и tests/Feature. Никаких отдельных флагов не нужно — изменённый файл подхватывается сам.

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

PASS Tests\Unit\ExampleTest и PASS Tests\Feature\ExampleTest с тремя тестами: root redirects to users (0.32s), users page returns a successful response (0.04s), user can be created (0.03s). Итог: Tests: 4 passed (7 assertions), Duration: 0.56s. Код возврата 0.

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

Первый тест Feature идёт 0.32s — на нём приложение поднимается и разворачивается тестовая база; два остальных проходят за 0.04s и 0.03s. Критерий успеха — строка Tests: и код возврата, а не длительность.

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

  • Зелёные тесты не трогают рабочую базу: записи из теста (test@example.com) в таблице users не появляется — в захвате 06-mysql-final те же 12 строк, что оставил сидер.

4 passed (7 assertions) за 0.56s. Первый тест Feature идёт 0.32s: на нём приложение поднимается и создаётся схема тестовой базы. Два остальных проходят за 0.04s и 0.03s — всё уже развёрнуто.

Что это даёт на практике. Раньше проверка «я ничего не сломал» означала: открыть список, открыть форму, ввести данные, отправить, посмотреть flash, отредактировать, удалить. Теперь это одна команда и полсекунды. Перед любым переносом кода, после любой правки контроллера или шаблона — php artisan test.

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

В блоке HTTP (lesson.http.i.04, lesson.http.ii.05) вы проверяли пагинацию и сортировку, переходя по ссылкам руками. Тест делает ровно тот же переход, только сам и каждый раз одинаково.

Что стоит протестировать дальше

Правила валидации (пустое имя, чужой email при правке), удаление записи, поиск и сортировку из домашнего задания. Каждый такой тест — это зафиксированное требование: через месяц вы увидите, что именно сломалось, а не «что-то не так».

14листайте
Часть 6 · Деплой

Зависимости на сервере: --no-dev и --optimize-autoloader

На боевом сервере не нужны инструменты разработки: тесты, генераторы данных, отладочные страницы. Composer умеет поставить только то, что требуется приложению. Мы запускаем с --dry-run — «показать, что произойдёт», не трогая vendor/ на стенде.

student@mint: ~/recoursia/project-laravel728 мс
student@mint:~/recoursia/project-laravel$
Как ставят зависимости на сервере — --no-dev не тянет phpunit и mockery; --optimize-autoloader строит карту классов. Здесь dry-run, чтобы не ломать стенд.
Разбор команды

На боевом сервере не нужны инструменты разработки. Смотрим, что поставит Composer в production-режиме, — но с --dry-run, чтобы не сломать рабочий стенд.

composer install --no-dev --optimize-autoloader --dry-run 2>&1 | tail -12
ТокенЗачем он и что меняет
composerпакетный менеджер PHP (в стенде 2.7.1 — строка Composer Version в about); ставит зависимости в vendor/.
installпоставить зависимости по composer.lock: на сервер приедут те же версии, что работают у вас. В отличие от update, lock-файл не пересчитывается.
--no-devне ставить пакеты из секции require-dev файла composer.json: тесты, генераторы данных, отладочные страницы.
--optimize-autoloaderавтозагрузчик получает готовую карту «класс → файл» вместо разбора правил PSR-4 на каждый класс.
--dry-runпоказать план и ничего не менять: vendor/ на стенде остаётся целым. Без этого флага мы бы сломали рабочий проект и тесты из этой же части.
2>&1направить stderr в тот же поток, что и stdout: предупреждения Composer (abandoned, funding) попадают в пайп, а не печатаются мимо него.
|пайп: вывод Composer идёт на вход tail.
tailпечатает хвост потока.
-12сколько строк хвоста оставить: в захвате ровно 12, начало списка - Removing осталось за кадром.

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

План production-установки, последние 12 строк. Восемь пакетов помечены на удаление: mockery/mockery (1.6.15), laravel/pint (v1.32.1), laravel/pao (v1.1.5), laravel/pail (v1.2.7), laravel/agent-detector (v2.0.4), hamcrest/hamcrest-php (v3.0.0), filp/whoops (2.18.6), fakerphp/faker (v1.24.1). Дальше два предупреждения Package dragon-code/… is abandoned, строка 90 packages you are using are looking for funding и подсказка про composer fund. Ничего не изменено, код возврата 0.

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

- Removing значит: пакет лежит в vendor/ сейчас, но в production-набор не входит. Это mockery (подделки для тестов), fakerphp/faker (случайные данные), filp/whoops (подробная страница ошибки), laravel/pint (форматирование кода), laravel/pail (журнал в терминале). abandoned — пометка автора пакета, а не ошибка: установка продолжается.

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

  • Без --dry-run команда пересобрала бы vendor/ прямо на стенде: mockery и faker пропали бы, и тесты перестали запускаться. Строки abandoned и 90 packages … looking for funding пугать не должны — на сервере их читают и идут дальше; запускать composer fund не обязательно.
  • --no-dev — не ставить пакеты из секции require-dev файла composer.json (мы разбирали его в части 1). В хвосте вывода видно восемь пакетов, которые команда помечает на удаление: mockery (подделки для тестов), fakerphp/faker (случайные данные), filp/whoops (подробная страница ошибки), laravel/pint (форматирование кода), laravel/pail (журнал в терминале). Список обрезан через tail -12, выше осталось ещё.
  • --optimize-autoloader — автозагрузчик получает готовую карту «класс → файл» вместо разбора правил PSR-4 на каждый класс.
  • install читает composer.lock: на сервер приедут те же версии, что работают у вас. В этом и был смысл lock-файла из части 1.
Две строчки, которые пугают зря

Package dragon-code/... is abandoned — автор пометил пакет как заброшенный. Это предупреждение, не ошибка: установка продолжается. Пакеты приехали вместе с русским переводом (LaravelLang\Lang\Plugin 15.37.4 из about). 90 packages you are using are looking for funding — просьба поддержать авторов, команду composer fund запускать не обязательно.

15листайте
Часть 6 · Деплой

Чек-лист переноса на сервер

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

1
Скопировать код

Без vendor/ и node_modules/ — их ставят на месте. .env не копируют из разработки: на сервере свой.

2
Поставить зависимости

composer install --no-dev --optimize-autoloader — из composer.lock, без инструментов разработки.

3
Собрать .env

APP_ENV=production, APP_DEBUG=false, APP_URL боевого домена, доступы к базе и свой APP_KEY (php artisan key:generate --show). Без ключа приложение вернёт 500 — мы это проверили выше.

4
Настроить права

Владелец storage/ и bootstrap/cache/ — пользователь, от которого работает PHP-FPM; право записи владельцу и группе. Без этого первый же запрос, компилирующий шаблон, даст 500.

5
Настроить vhost

root в public/, try_files с переходом на index.php, fastcgi_pass в сокет PHP-FPM — ровно та схема, которую мы собрали в части 2 и в lesson.linux.06.

6
Применить миграции

php artisan migrate --force. Флаг --force подтверждает действие: в окружении production Laravel переспрашивает, потому что миграции меняют боевую базу.

7
Включить кэш

php artisan config:cache, route:cache, view:cache — и пересобирать их при каждом следующем выкатывании кода.

8
Проверить

php artisan about (окружение, драйверы, кэш), php artisan route:list и живая страница. Только после этого — сообщать, что перенос закончен.

Честно про этот слайд

Сам перенос мы на стенде не выполняли: здесь порядок действий, а не вывод команд. Все команды, чей вывод показан в этой части, выполнялись по-настоящему.

16листайте
Часть 6 · Итоги

Что в базе

student@mint: ~/recoursia/project-laravel500 мс
student@mint:~/recoursia/project-laravel$
Итог по базе
Разбор команды

Финальная сверка с базой: сколько записей в таблице users и какие таблицы вообще созданы.

mysql -u project_laravel -psecret project_laravel -e "SELECT COUNT(*) AS users FROM users; SHOW TABLES;"
ТокенЗачем он и что меняет
mysqlконсольный клиент MariaDB/MySQL: подключаемся к той же базе, с которой работает приложение.
-uфлаг пользователя: имя идёт следом, через пробел.
project_laravelпользователь MariaDB, от имени которого приложение ходит в базу (доступы прописаны в .env).
-psecretпароль, прилепленный к -p без пробела: -p плюс значение. Пробел изменил бы смысл — слово после -p клиент принял бы за имя базы.
project_laravelимя базы (позиционный аргумент после флагов): оба запроса выполняются в ней. mysql подставит это имя в заголовок второго результата — Tables_in_project_laravel.
-eвыполнить SQL из аргумента и выйти, не открывая интерактивную сессию.
"SELECT COUNT(*) AS users FROM users; SHOW TABLES;"два запроса в двойных кавычках: кавычки нужны, чтобы SQL с пробелами дошёл до mysql одним аргументом, а ; разделяет запросы — оба выполнятся по очереди. AS users задаёт имя колонки в выводе, поэтому первая строка результата — users, а под ней значение.

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

Два результата. Первый: колонка users и значение 12 — столько строк в таблице users (их создал сидер в части 4). Второй: заголовок Tables_in_project_laravel и девять таблиц — cache, cache_locks, failed_jobs, job_batches, jobs, migrations, password_reset_tokens, sessions, users. Код возврата 0.

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

12 записей, и среди них нет test@example.com: тесты из этой части работают со своей базой, рабочая не затронута. Из девяти таблиц наша только users; остальные создал каркас: migrations — журнал применённых миграций, sessions, cache и cache_locks, jobs, job_batches и failed_jobs (очереди), password_reset_tokens. Набор совпадает с драйверами из about: Session database, Cache database, Queue database.

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

  • ERROR 1698 (28000): Access denied for user 'root'@'localhost' — так MariaDB отвечает, когда подключаются под root по TCP (mysql -h 127.0.0.1 -u root, часть 4): для root включён плагин unix_socket, и пароль в команде не помогает. Поэтому и приложение, и мы здесь ходим под отдельным пользователем. И второе: пароль в командной строке остаётся в истории оболочки и виден в списке процессов — для учебного стенда это нормально, на сервере так передавать пароль нельзя.

В таблице users 12 строк — столько же записей создал сидер в части 4. Тестового test@example.com среди них нет: тесты работают со своей базой, и скриншот списка в конце части, снятый после прогона тестов, показывает те же 12 записей.

Из девяти таблиц наша только одна:

Таблицы Откуда
users наша: миграция каркаса + колонка phone, которую мы добавили в части 4
migrations журнал применённых миграций — по нему migrate понимает, что уже выполнено
sessions сессии (about: Session database)
cache, cache_locks кэш приложения (about: Cache database)
jobs, job_batches, failed_jobs очереди (about: Queue database); мы их не используем, но таблицы созданы
password_reset_tokens сброс пароля — понадобится, когда дойдёте до аутентификации

Запрос выполнен от пользователя project_laravel с паролем в командной строке. Для учебного стенда это нормально, но запомните: пароль в команде остаётся в истории оболочки, а на сервере его так передавать нельзя.

17листайте
Часть 6 · Итоги

Какие маршруты остались в приложении

student@mint: ~/recoursia/project-laravel745 мс
student@mint:~/recoursia/project-laravel$
Итоговые маршруты
Разбор команды

Финальная карта приложения: какие маршруты остались после части 5 и куда каждый ведёт.

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

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

Семь маршрутов и последняя строка Showing [7] routes. GET|HEAD / ведёт в routes/web.php:6 — это замыкание, редирект на /users. Остальные шесть — ресурсные: users.index › UserController@index, users.store › UserController@store, users.create › UserController@create, users.update › UserController@update, users.destroy › UserController@destroy, users.edit › UserController@edit.

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

GET|HEAD — маршрут отвечает на оба метода. PUT|PATCH users/{user} и DELETE users/{user} приходят из HTML-форм через директиву @method: браузер умеет отправлять только GET и POST. {user} — параметр маршрута, в него попадает id записи. Маршрута GET users/{user} нет: страницу отдельного пользователя мы не делали, Route::resource(...)->except('show') её исключил.

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

  • Маршрута, которого нет в этом списке, для приложения не существует — страница даст 404. И если маршрут только что добавили, а кэш маршрутов включён (route:cache), список и поведение обновятся только после пересборки кэша или optimize:clear.

Семь маршрутов: один редирект с корня и шесть ресурсных — те, что даёт Route::resource(...)->except('show') из части 5. Маршрута users/{user} метода GET в списке нет: страницу отдельного пользователя мы сознательно не делали, список и правка закрывают задачу.

Метод и путь Имя Метод контроллера Что делает
GET / — замыкание, routes/web.php:6 редирект на /users
GET users users.index UserController@index список с пагинацией
GET users/create users.create UserController@create форма добавления
POST users users.store UserController@store валидация и запись
GET users/{user}/edit users.edit UserController@edit форма правки
PUT/PATCH users/{user} users.update UserController@update обновление записи
DELETE users/{user} users.destroy UserController@destroy удаление

Флаг --except-vendor убирает из списка служебные маршруты самого каркаса — оставляет только наши. PUT|PATCH и DELETE приходят из HTML-форм через директиву @method: браузер умеет отправлять только GET и POST (об этом — lesson.http.iii.01).

18листайте
Часть 6 · Итоги

Как выглядит проект целиком

student@mint: ~/recoursia/project-laravel574 мс
student@mint:~/recoursia/project-laravel$
├── public│   ├── css│   ├── .htaccess│   ├── favicon.ico│   ├── index.php│   └── robots.txt├── resources│   ├── css│   ├── js│   └── views├── routes│   ├── console.php│   └── web.php├── storage│   ├── app│   ├── framework│   └── logs├── tests│   ├── Feature│   ├── Unit│   └── TestCase.php├── .editorconfig├── .env├── .env.example├── .gitattributes├── .gitignore├── .npmrc├── .phpunit.result.cache├── AGENTS.md├── CLAUDE.md├── README.md├── artisan├── composer.json├── composer.lock├── package.json├── phpunit.xml└── vite.config.js26 directories, 37 files96M	.
Итоговая структура и размеры
Разбор команды

Проект целиком: структура до второго уровня и сколько места он занимает на диске.

tree -L 2 -a -I 'vendor|node_modules|.git|lang' --dirsfirst; du -sh . vendor storage/logs
ТокенЗачем он и что меняет
treeпечатает дерево каталогов и файлов.
-L 2глубина вывода — два уровня. Без ограничения tree полез бы внутрь vendor/ и вывел тысячи строк.
-aпоказывать скрытые имена (начинающиеся с точки): в захвате видны .env, .env.example, .gitignore, .editorconfig, .npmrc, .phpunit.result.cache.
-I 'vendor|node_modules|.git|lang'исключить то, что подходит под шаблон. Одинарные кавычки обязательны: без них shell принял бы | за пайп. Внутри | — разделитель списка шаблонов tree, а не регулярное выражение.
--dirsfirstв каждом уровне сначала каталоги, потом файлы: app, bootstrap, config… идут выше, чем artisan, composer.json.
;разделитель команд: вторая выполняется после первой, независимо от её кода возврата.
duсчитает место, занятое на диске.
-shдва флага слитно: -s (summarize) — одна итоговая строка на аргумент, без перечисления вложенных каталогов; -h (human-readable) — единицы K/M/G вместо числа блоков.
. vendor storage/logsтри аргумента du: каталог проекта целиком, vendor/ и каталог журнала. В захвате напечатана одна строка — 96M для . (весь проект, вместе с vendor/, который в дереве не показан).

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

tree рисует структуру проекта и подводит итог в последних строках: 26 directories, 37 files. du печатает размер — 96M занимает весь каталог проекта.

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

26 directories, 37 files — это без vendor/, node_modules/, .git/ и lang/: их скрыл -I. А 96M — уже вместе с ними. В дереве видно, где живёт ваш код (app/Http, app/Models, routes/web.php, resources/views, database/migrations, database/seeders, tests/, public/css), куда приложение пишет (storage/framework/views, storage/logs, bootstrap/cache) и что public/ — единственная папка, которую отдаёт nginx. Рядом .env (настройки, в git не коммитят) и .env.example (шаблон), а также остатки каркаса: database/database.sqlite, .phpunit.result.cache, vite.config.js, package.json.

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

  • Без -I вывод не влезет на экран: в vendor/ тысячи файлов. tree: command not found — пакет не установлен, ставится отдельно (sudo apt install tree). И du показывает занятое место на диске, а не сумму размеров файлов.

Вывод tree обрезан ключами: показаны два уровня и скрыты vendor, node_modules, .git и lang. Итог в последних двух строках вывода — 26 каталогов и 37 файлов, а весь каталог проекта занимает 96M (вместе с vendor/, который в дереве не показан).

Код, который написали вы: app/Http (контроллеры), app/Models, routes/web.php, resources/views, database/migrations, database/seeders, tests/, public/css.

Что осталось от каркаса и не мешает: database/database.sqlite — файл базы по умолчанию; с части 4 приложение работает с MariaDB. .phpunit.result.cache — создал phpunit, чтобы быстрее перезапускать упавшие тесты. vite.config.js и package.json — сборка ассетов, которую мы не включали.

Куда пишет приложение: storage/framework/views (скомпилированные шаблоны — источник ошибки 500 из начала части), storage/logs (laravel.log), bootstrap/cache (кэш конфигурации и маршрутов). Эти каталоги не должны попадать в git и должны быть доступны на запись.

Единственная папка, которую видит nginx: public/. Всё остальное — вне корня сайта, поэтому .env и vendor/ нельзя скачать по HTTP (мы проверяли это в части 2).

19листайте
Часть 6 · Итоги

То, ради чего всё это было

http://project-laravel.rs/users
12 записей, пагинация по 5, действия «Правка» и «Удалить» в каждой строке.
12 записей, пагинация по 5, действия «Правка» и «Удалить» в каждой строке.

Страница http://project-laravel.rs/users: 12 записей из MariaDB, пагинация по 5, в каждой строке действия «Правка» и «Удалить». Всё, что стоит между запросом браузера и этой таблицей, — на следующей схеме.

20листайте
Часть 6 · Итоги

Путь запроса целиком

Цепочка, которую мы собирали пять частей. Имена и пути в узлах — не иллюстрация: это route:list, about и дерево проекта из этой части.

Браузер — GET /usersБраузерGET /usersDNS зоны .rs — имя домена в адресDNS зоны .rsимя домена в адресnginx — vhost из части 2nginxvhost из части 2PHP-FPM — сокетPHP-FPMсокетКонтроллер — UserController@indexКонтроллерUserController@indexusers.index — GET usersusers.indexGET usersРоутер — ищет маршрутРоутерищет маршрутindex.php — public/index.phpindex.phppublic/index.phpМодель User — User::paginate(5)Модель UserUser::paginate(5)MariaDB — prepared statementMariaDBprepared statementШаблон Blade — resources/viewsШаблон Bladeresources/viewsОтвет в буфер — HTMLОтвет в буферHTMLfastcgi_passSELECT5 строк из 12200 OK через nginxfastcgi_passSELECT5 строк из 12200 OK через nginxБраузер — GET /usersБраузерDNS зоны .rs — имя домена в адресDNS зоны .rsnginx — vhost из части 2nginxPHP-FPM — сокетPHP-FPMКонтроллер — UserController@indexКонтроллерusers.index — GET usersusers.indexРоутер — ищет маршрутРоутерindex.php — public/index.phpindex.phpМодель User — User::paginate(5)Модель UserMariaDB — prepared statementMariaDBШаблон Blade — resources/viewsШаблон BladeОтвет в буфер — HTMLОтвет вбуфер
GET project-laravel.rs/users: запрос спускается к данным, ответ возвращается в браузер
21листайте
Часть 6 · Большая картина

Проблема «чистого PHP» → решение Laravel

Что делали руками в курсе Что делает Laravel Где это было у нас
Разбирали $_GET['page'] и сами решали, какой файл подключить (lesson.http.i.01) Маршруты и роутер: route:list показывает всю карту приложения части 2, 5
Собирали HTML строками, подставляя значения из запроса (lesson.http.i.04) Blade: {{ $user->name }} подставляет и экранирует вывод части 2, 3
Общий «каркас страницы» копировали или наследовали классом layout (lesson.http.ii.02) @extends / @section — макет один, содержимое разное часть 3
Писали SQL и следили за prepared statements через PDO (lesson.db.04) Eloquent и query builder: prepared statements получаются сами часть 4
Правили таблицу через ALTER TABLE и помнили, что изменили (lesson.dbii.03) Миграции: схема в коде, есть down и журнал migrations часть 4
Пароль БД держали в коде или в отдельном db.php .env + config/: настройки вне кода части 1, 4
Проверяли $_POST условиями и собирали сообщения об ошибках вручную $request->validate(): правила, старые значения через old(), @error часть 5
От подделки запроса защищались белым списком полей (lesson.sql-injection.i.03) #[Fillable] — тот же белый список, плюс CSRF-токен и 419 части 4, 5
Проверяли результат руками в браузере php artisan test: те же проверки за полсекунды эта часть
Переносили проект «скопировать папку» composer install --no-dev, кэш, migrate --force, права на storage эта часть
22листайте
Часть 6 · Куда расти

Куда двигаться дальше

Приложение users — это каркас, на котором видно всю механику. Дальше наращивают то, что в нём уже подготовлено, но не используется.

Аутентификация. Таблица password_reset_tokens в базе и модель User с хешированием пароля уже на месте. Готовые наборы (Breeze, Fortify) добавляют регистрацию, вход и сброс пароля — их стоит читать как исходники, а не как магию.

Права доступа. Сейчас удалить запись может кто угодно. Политики Laravel отвечают на вопрос «может ли этот пользователь сделать это с этой записью».

Очереди. Таблицы jobs, job_batches, failed_jobs созданы, драйвер Queue database уже выбран. Очереди нужны, когда действие долгое: письмо, выгрузка, обработка файла.

Фабрики. Каталог database/factories в проекте есть (видно в дереве), но мы им не пользовались: данные для сидера писали руками в части 4, а для теста — прямо в запросе. Фабрики генерируют записи сами, и на них удобно строить тесты с большим объёмом данных.

Vite и сборка ассетов. Мы обошлись public/css/app.css без сборщика (часть 3). Когда понадобится JS-фронтенд — package.json и vite.config.js уже в проекте.

API. Те же контроллеры умеют отдавать JSON: это отдельный набор маршрутов и ресурс-классы.

Что перечитать из курса перед следующими шагами:

  • lesson.db.vi.02 и lesson.db.vi.03 — индексы и EXPLAIN: как только в списке появятся сортировка и поиск, скорость упрётся именно в них;
  • lesson.sql-injection.i.03 — белый список: та же идея, что #[Fillable] и правила валидации;
  • lesson.programs.02 и lesson.programs.03 — права доступа: без них нельзя понять ошибку 500 из начала этой части;
  • lesson.network.02 — жизненный цикл запроса: объясняет, зачем нужен кэш;
  • lesson.linux.06 — nginx + PHP-FPM: схема vhost, которую мы переносим на сервер.
23листайте
Часть 6 · Проверьте себя

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

Ответы — на следующем слайде. Отвечайте развёрнуто: в этой части важны не команды, а причины.

  1. Страница отдаёт 500, сообщение в браузере ничего не объясняет. Какие две вещи вы посмотрите первыми и что именно в них ищете?
  2. Вы выполнили tail -1 storage/logs/laravel.log и получили бессмысленный обрывок строки. Почему так вышло и как найти нужную запись?
  3. Вы поменяли значение в .env, перезагрузили страницу — поведение прежнее. Что произошло и как это исправить?
  4. Тест ожидал 200 на / и получил 302. В каком случае нужно чинить код, а в каком — тест? По какому признаку вы это решаете?
  5. Перечислите по порядку шаги переноса приложения на новый сервер: код, зависимости, .env, права, vhost, миграции, кэш, проверка.
24листайте
Часть 6 · Проверьте себя

Ответы

  1. php artisan about — окружение (local или production), Debug Mode, драйверы, состояние кэша; и storage/logs/laravel.log — запись с уровнем ERROR. Если в журнале Laravel пусто, смотрим error.log nginx: значит, дело до PHP не дошло.
  2. Одна запись об ошибке занимает много строк: сообщение плюс стектрейс. Последняя строка файла — это конец записи. Нужную ищут по маркеру уровня: grep -a "local.ERROR" storage/logs/laravel.log | tail -1, а читают от найденной строки вниз.
  3. Конфигурация закэширована (config:cache): значения .env читаются из кэша, а не из файла. Исправляется php artisan optimize:clear или повторным config:cache после правки.
  4. Код чинят, если поведение должно было остаться прежним, — это регрессия, тест её и поймал. Тест переписывают, если поведение изменили намеренно: у нас / стал редиректом на /users, поэтому утверждение заменили на assertRedirect('/users'). Признак — ответ на вопрос «что приложение обязано делать по требованию».
  5. Код без vendor/ → composer install --no-dev --optimize-autoloader → .env (APP_ENV=production, APP_DEBUG=false, свой APP_KEY, доступы к базе) → права на storage/ и bootstrap/cache/ → vhost с root в public/ → php artisan migrate --force → config:cache, route:cache, view:cache → проверка about, route:list и живой страницы.
25листайте
lesson.laravel · финал

Что дальше

Курс закончен. Вы прошли путь от «обезьяны, которая выполняет код сверху вниз» до приложения на фреймворке: маршруты, контроллеры, Blade, миграции, Eloquent, формы с валидацией и CSRF, тесты и подготовка к переносу.

Домашнее задание — три задачи поверх готового приложения:

  1. Дата рождения. Добавьте поле birthday: миграция (как phone в части 4), правило валидации, поле в форме, вывод в списке. Проверьте, что правка существующей записи не теряет значение.
  2. Сортировка. Список должен уметь сортироваться по ФИО — по возрастанию и по убыванию. Ссылку сортировки соберите так, чтобы она не теряла текущую страницу пагинации.
  3. Поиск по ФИО. Форма поиска над списком: частичное совпадение, результат с пагинацией, пустой результат — отдельное сообщение, а не пустая таблица.
Как себя проверить

После каждой задачи: php artisan test (ни один тест не сломался), php artisan route:list (маршруты на месте) и php artisan about (окружение не уехало). Затем добавьте тест на новое поведение — хотя бы один на задачу.

Если что-то в этой части не сошлось с вашей машиной — сравните вывод php artisan about со слайдом в начале. Почти все расхождения видны там: версия, окружение, драйвер базы, состояние кэша.

26листайте