Часть 6 — Эксплуатация: ошибки, кэш, тесты, деплой
Приложение написано и работает. Осталось научиться его чинить и переносить на другую машину.
Что сделаем: воспроизведём две настоящие ошибки 500 (нет прав на storage
и пустой APP_KEY), научимся читать laravel.log, включим и выключим кэш,
починим упавший тест и соберём чек-лист переноса на сервер.
Что понадобится: тот же стенд (Linux Mint 22, PHP 8.3, nginx + зона .rs,
MariaDB) и проект ~/recoursia/project-laravel в том состоянии, в котором мы оставили его
в части 5. Все выводы в этой части — настоящие захваты прогона на стенде.
php artisan about — с чего начинается любая диагностика
Когда приложение ведёт себя странно, первую команду вводят не «наугад», а ту, которая показывает состояние целиком. В части 1 мы запускали её сразу после установки; теперь посмотрим, что изменилось.
Environment ................................................................ Application Name ................................................... Laravel Laravel Version .................................................... 13.37.0 PHP Version .......................................................... 8.3.6 Composer Version ..................................................... 2.7.1 Environment .......................................................... local Debug Mode ......................................................... ENABLED URL ..................................................... project-laravel.rs Maintenance Mode ....................................................... OFF Timezone ............................................................... UTC Locale .................................................................. ru 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
Разбор команды
Любая диагностика приложения начинается с одной команды: 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 Version 8.3.6 — версия стенда, её печатает about. Проверьте свою: php -v.
В командах этой части версия не встречается: всё запускается через php artisan,
composer, curl, grep и mysql, без версионных путей (/etc/php/8.3/…,
/run/php/…) и имени сервиса (php8.3-fpm) — заменять в них нечего.
Там же: драйверы, кэш и хранилище
Остальные блоки 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()
и заглядывать в конфиги. Запомните её: почти любая диагностика начинается здесь.
500 из-за прав на storage/framework/views
Это ошибка номер один при переносе на сервер. Воспроизведём её специально —
чтобы потом узнавать за секунды. В блоке видно все шесть команд: удаляем
скомпилированные шаблоны, закрываем папку на запись, запрашиваем /users,
смотрим журнал, возвращаем права, запрашиваем снова.
GET /users при закрытой папке views -> 500[2026-10-17 10:22:46] local.ERROR: tempnam(): file created in the system's temporary directory {"exception":"[object] (ErrorException(code: 0): tempnam(): file после возврата прав -> 200
Разбор команды
Ошибка номер один при переносе на сервер, воспроизведённая специально: убираем скомпилированные шаблоны, закрываем папку на запись и смотрим, что происходит с запросом.
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| Токен | Зачем он и что меняет |
|---|---|
curl | HTTP-клиент: делает настоящий запрос через nginx и PHP-FPM, как браузер. |
-sS | два флага слитно: -s (silent) убирает прогресс-бар, -S (show-error) оставляет сообщения об ошибках. Без -S в тихом режиме не видно даже того, что запрос не удался. |
-o /dev/null | тело ответа отправить в «никуда»: нужен только код, а не HTML страницы ошибки. |
-w | write-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по журналу, читайте новую запись и проверьте владельца каталога, а не только цифры прав.
Разберём по шагам, иначе вывод выглядит магией.
rm -f storage/framework/views/*.php. Без этого шага эксперимент ничего
не покажет: about выше сообщал Views CACHED, то есть готовые PHP-файлы
уже лежали в папке. Пока они есть и не устарели, Blade их просто подключает
и ничего не записывает.
chmod 500 storage/framework/views. В блоке прав (lesson.programs.02)
вы разбирали числовую запись: 500 — это r-x для владельца, то есть читать
и входить можно, писать нельзя.
GET /users -> 500. Blade понадобилось скомпилировать шаблон заново,
создать файл не удалось — Laravel остановил запрос и отдал 500.
grep -a "local.ERROR" storage/logs/laravel.log | tail -1 — последняя запись
об ошибке: tempnam(): file created in the system's temporary directory.
PHP не смог создать файл в нужной папке и полез в системный каталог временных
файлов; Laravel превращает такое предупреждение в исключение.
chmod 775 — владелец и группа снова могут писать. Тот же запрос даёт 200.
Почему так происходит и как не наступить на сервере
Причина не в 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.
500 из-за пустого APP_KEY
Ещё одна частая ошибка после переноса: код скопировали, а .env — нет
(его не коммитят), либо скопировали без значения APP_KEY. Проверяем: обнуляем APP_KEY
в .env, запрашиваем страницу, возвращаем файл на место.
GET /users с пустым APP_KEY -> 500No application encryption key has been specified.после возврата ключа -> 200base64:+ojjAGd8wuPdgREfJ2Hag/vcsl7GQNg10MpdX/BveHE=
Разбор команды
Вторая частая ошибка после переноса: код скопировали, а .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| Токен | Зачем он и что меняет |
|---|---|
curl | HTTP-клиент: настоящий запрос через nginx и PHP-FPM. |
-sS | без прогресс-бара, но с сообщениями об ошибках. |
-o /dev/null | тело ответа в «никуда»: здесь нужен только код. |
-w | write-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| Токен | Зачем он и что меняет |
|---|---|
curl | HTTP-запрос к странице. |
-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.
Частые ошибки
- Без
-ogrepнапечатает строку целиком: страница ошибки — это длинный 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 | тело ответа не печатаем — нужен код. |
-w | write-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:
Флаг --show печатает ключ и не трогает .env — именно так ключ
получают для боевого сервера: сгенерировали, вставили в .env вручную.
44 символа в base64 — это 32 байта случайных данных.
Зачем вообще нужен APP_KEY
Ключ — это секрет всего приложения. Им шифруются данные, которые приложение отдаёт наружу и потом читает обратно.
- Кука сессии. В части 2 в заголовках ответа мы видели
laravel_sessionиXSRF-TOKEN. Содержимое этих кук подписано и зашифровано ключом: подделать или прочитать его без ключа нельзя. - CSRF-токен. Форма из части 5 присылает
_token, сервер сверяет его с токеном в сессии. А сессия опознаётся по куке, зашифрованной ключом: нет ключа — нет ни сессии, ни проверки токена, ни отправки форм. - Данные, зашифрованные через криптографию Laravel (например, значения полей модели, помеченные как шифруемые).
Если на работающем проекте заменить APP_KEY, всё зашифрованное прежним
ключом перестанет расшифровываться: текущие сессии станут недействительными
(пользователей разлогинит), а зашифрованные данные будут потеряны.
Поэтому на каждом окружении ключ генерируют один раз и хранят в .env,
а .env не попадает ни в git, ни в скриншоты.
Установщик из части 1 сгенерировал его сам при создании проекта. Теряют ключ
обычно при переносе: .env копируют вручную и забывают строку APP_KEY.
Как читать storage/logs/laravel.log
Обе ошибки выше мы нашли в одном файле. Путь к нему виден в командах
экспериментов, а в дереве проекта есть каталог storage/logs. Драйвер
stack / single из about означает: пишется один файл, без ротации по дням.
Запись в журнале выглядит так (строка из захвата с правами):
Разбор по частям:
[2026-10-17 10:22:46]— дата и время;local.ERROR— окружение изAPP_ENVи уровень (бываютWARNING,INFO,DEBUG);- дальше — короткое сообщение, а за ним JSON с исключением и стектрейсом.
tail -1 laravel.log почти бесполезенСтектрейс занимает десятки строк, поэтому последняя строка файла — это хвост записи, часто бессмысленный. Искать нужно строку с уровнем ошибки, а потом читать запись от неё вниз:
Именно так мы и сделали в эксперименте с правами. Флаг -a нужен, потому что
в журнале встречаются байты, которые grep считает двоичными и отказывается
печатать; cut -c1-160 просто обрезает строку до 160 символов, чтобы она
влезла на экран — в работе удобнее читать запись целиком.
laravel.log — что случилось внутри PHP-кода. error.log nginx
(блок Linux, lesson.linux.06) — что случилось до PHP: не найден файл,
недоступен сокет PHP-FPM, неверный root. Если Laravel не запустился вовсе,
в laravel.log может не появиться ничего — тогда смотрите в nginx.
Что именно кэширует Laravel
Три команды — и приложение перестаёт читать свои же файлы на каждом запросе.
INFO Configuration cached successfully. INFO Routes cached successfully. INFO Blade templates cached successfully. Cache ...................................................................... Config .............................................................. CACHED Events .......................................................... NOT CACHED Routes .............................................................. CACHED Views ............................................................... CACHED 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
Разбор команды
Три команды убирают чтение собственных файлов приложения на каждом запросе, четвёртая показывает результат, пятая возвращает стенд в состояние разработки.
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: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 заново, поэтому всё, что посчитано один раз и положено
в файл, экономится на каждом запросе.
В разработке кэш мешает. 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.
Первая команда тестов — и она упала
До сих пор мы проверяли приложение руками: открывали страницу в браузере и смотрели. Теперь проверкой займётся машина. В каркасе уже лежат два теста — запускаем их как есть, ничего не меняя.
PASS Tests\Unit\ExampleTest ✓ that true is true FAIL Tests\Feature\ExampleTest ⨯ the application returns a successful response 0.23s ──────────────────────────────────────────────────────────────────────────── FAILED Tests\Feature\ExampleTest > the application returns a successful… Expected response status code [200] but received 302.Failed asserting that 302 is identical to 200. at tests/Feature/ExampleTest.php:17 13▕ public function test_the_application_returns_a_successful_response(): void 14▕ { 15▕ $response = $this->get('/'); 16▕ ➜ 17▕ $response->assertStatus(200); 18▕ } 19▕ } 20▕ Tests: 1 failed, 1 passed (2 assertions) Duration: 0.42s
Разбор команды
До сих пор приложение проверяли руками: открывали страницу и смотрели. Запускаем тесты из каркаса как есть, ничего не меняя, — и читаем, что упало.
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.
Почему 302 вместо 200
Вот тест, который упал. Он целиком помещается в один экран.
Строка $response = $this->get('/'); выполняет настоящий HTTP-запрос
к приложению, но внутри PHP: без nginx, без браузера, без сети. Laravel
поднимает ядро приложения, проводит через него запрос GET / и возвращает
объект-ответ, у которого можно спросить код, заголовки и содержимое.
$response->assertStatus(200); — утверждение: «мы ожидаем код 200».
Тест получил 302:
И это не поломка. 302 — редирект, и сделали его мы сами: в части 5 маршрут
/ перестал показывать приветственную страницу и теперь перенаправляет
на /users. В route:list (дальше в этой части) видно, что / — это
замыкание в routes/web.php:6, а не контроллер.
Тест падает, когда поведение изменилось. Он не знает, случайно это или намеренно, — решает человек. Либо поведение ошибочно и чинить надо код, либо поведение новое и тест нужно осознанно переписать. Худшее, что можно сделать, — удалить тест, чтобы не мешал.
Полезно и то, как показан провал: at tests/Feature/ExampleTest.php:17
и фрагмент кода со стрелкой ➜ на строке с утверждением. Искать место
в файле не нужно — это ровно та строка 17, которая подсвечена в карточке
файла выше.
Тесты, которые описывают наше приложение
Переписываем тест так, чтобы он проверял не «что-нибудь работает», а реальное поведение нашего приложения: редирект, страницу списка и создание записи через форму.
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.
Зелёные
PASS Tests\Unit\ExampleTest ✓ that true is true 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
Разбор команды
Тест переписан под реальное поведение приложения: редирект, страница списка, создание записи через форму. Команда та же — сравниваем вывод.
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 при правке), удаление записи, поиск и сортировку из домашнего задания. Каждый такой тест — это зафиксированное требование: через месяц вы увидите, что именно сломалось, а не «что-то не так».
Зависимости на сервере: --no-dev и --optimize-autoloader
На боевом сервере не нужны инструменты разработки: тесты, генераторы данных,
отладочные страницы. Composer умеет поставить только то, что требуется
приложению. Мы запускаем с --dry-run — «показать, что произойдёт»,
не трогая vendor/ на стенде.
- Removing mockery/mockery (1.6.15) - Removing laravel/pint (v1.32.1) - Removing laravel/pao (v1.1.5) - Removing laravel/pail (v1.2.7) - Removing laravel/agent-detector (v2.0.4) - Removing hamcrest/hamcrest-php (v3.0.0) - Removing filp/whoops (2.18.6) - Removing fakerphp/faker (v1.24.1)Package dragon-code/pretty-array is abandoned, you should avoid using it. Use symfony/var-exporter instead.Package dragon-code/support is abandoned, you should avoid using it. Use illuminate/support instead.90 packages you are using are looking for funding.Use the `composer fund` command to find out more!
Разбор команды
На боевом сервере не нужны инструменты разработки. Смотрим, что поставит 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 запускать не обязательно.
Чек-лист переноса на сервер
Порядок важен: часть шагов бессмысленна без предыдущих.
Без vendor/ и node_modules/ — их ставят на месте. .env не копируют
из разработки: на сервере свой.
composer install --no-dev --optimize-autoloader — из composer.lock,
без инструментов разработки.
.envAPP_ENV=production, APP_DEBUG=false, APP_URL боевого домена,
доступы к базе и свой APP_KEY (php artisan key:generate --show).
Без ключа приложение вернёт 500 — мы это проверили выше.
Владелец storage/ и bootstrap/cache/ — пользователь, от которого
работает PHP-FPM; право записи владельцу и группе. Без этого первый же
запрос, компилирующий шаблон, даст 500.
root в public/, try_files с переходом на index.php, fastcgi_pass
в сокет PHP-FPM — ровно та схема, которую мы собрали в части 2
и в lesson.linux.06.
php artisan migrate --force. Флаг --force подтверждает действие:
в окружении production Laravel переспрашивает, потому что миграции
меняют боевую базу.
php artisan config:cache, route:cache, view:cache — и пересобирать
их при каждом следующем выкатывании кода.
php artisan about (окружение, драйверы, кэш), php artisan route:list
и живая страница. Только после этого — сообщать, что перенос закончен.
Сам перенос мы на стенде не выполняли: здесь порядок действий, а не вывод команд. Все команды, чей вывод показан в этой части, выполнялись по-настоящему.
Что в базе
users12Tables_in_project_laravelcachecache_locksfailed_jobsjob_batchesjobsmigrationspassword_reset_tokenssessionsusers
Разбор команды
Финальная сверка с базой: сколько записей в таблице 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 с паролем в командной строке.
Для учебного стенда это нормально, но запомните: пароль в команде остаётся
в истории оболочки, а на сервере его так передавать нельзя.
Какие маршруты остались в приложении
GET|HEAD / ......................................... routes/web.php:6 GET|HEAD users ................... users.index › UserController@index POST users ................... users.store › UserController@store GET|HEAD users/create .......... users.create › UserController@create PUT|PATCH users/{user} .......... users.update › UserController@update DELETE users/{user} ........ users.destroy › UserController@destroy GET|HEAD users/{user}/edit ......... users.edit › UserController@edit Showing [7] routes
Разбор команды
Финальная карта приложения: какие маршруты остались после части 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).
Как выглядит проект целиком
.├── app│ ├── Http│ ├── Models│ └── Providers├── bootstrap│ ├── cache│ ├── app.php│ └── providers.php├── config│ ├── app.php│ ├── auth.php│ ├── cache.php│ ├── database.php│ ├── filesystems.php│ ├── logging.php│ ├── mail.php│ ├── queue.php│ ├── services.php│ └── session.php├── database│ ├── factories│ ├── migrations│ ├── seeders│ ├── .gitignore│ └── database.sqlite├── 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).
То, ради чего всё это было

Страница http://project-laravel.rs/users: 12 записей из MariaDB, пагинация по 5,
в каждой строке действия «Правка» и «Удалить». Всё, что стоит между запросом
браузера и этой таблицей, — на следующей схеме.
Путь запроса целиком
Цепочка, которую мы собирали пять частей. Имена и пути в узлах — не иллюстрация:
это route:list, about и дерево проекта из этой части.
Проблема «чистого 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 |
эта часть |
Куда двигаться дальше
Приложение 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, которую мы переносим на сервер.
Проверьте себя
Ответы — на следующем слайде. Отвечайте развёрнуто: в этой части важны не команды, а причины.
- Страница отдаёт
500, сообщение в браузере ничего не объясняет. Какие две вещи вы посмотрите первыми и что именно в них ищете? - Вы выполнили
tail -1 storage/logs/laravel.logи получили бессмысленный обрывок строки. Почему так вышло и как найти нужную запись? - Вы поменяли значение в
.env, перезагрузили страницу — поведение прежнее. Что произошло и как это исправить? - Тест ожидал
200на/и получил302. В каком случае нужно чинить код, а в каком — тест? По какому признаку вы это решаете? - Перечислите по порядку шаги переноса приложения на новый сервер: код,
зависимости,
.env, права, vhost, миграции, кэш, проверка.
Ответы
php artisan about— окружение (localилиproduction),Debug Mode, драйверы, состояние кэша; иstorage/logs/laravel.log— запись с уровнемERROR. Если в журнале Laravel пусто, смотримerror.lognginx: значит, дело до PHP не дошло.- Одна запись об ошибке занимает много строк: сообщение плюс стектрейс.
Последняя строка файла — это конец записи. Нужную ищут по маркеру уровня:
grep -a "local.ERROR" storage/logs/laravel.log | tail -1, а читают от найденной строки вниз. - Конфигурация закэширована (
config:cache): значения.envчитаются из кэша, а не из файла. Исправляетсяphp artisan optimize:clearили повторнымconfig:cacheпосле правки. - Код чинят, если поведение должно было остаться прежним, — это регрессия,
тест её и поймал. Тест переписывают, если поведение изменили намеренно:
у нас
/стал редиректом на/users, поэтому утверждение заменили наassertRedirect('/users'). Признак — ответ на вопрос «что приложение обязано делать по требованию». - Код без
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и живой страницы.
Что дальше
Курс закончен. Вы прошли путь от «обезьяны, которая выполняет код сверху вниз» до приложения на фреймворке: маршруты, контроллеры, Blade, миграции, Eloquent, формы с валидацией и CSRF, тесты и подготовка к переносу.
Домашнее задание — три задачи поверх готового приложения:
- Дата рождения. Добавьте поле
birthday: миграция (какphoneв части 4), правило валидации, поле в форме, вывод в списке. Проверьте, что правка существующей записи не теряет значение. - Сортировка. Список должен уметь сортироваться по ФИО — по возрастанию и по убыванию. Ссылку сортировки соберите так, чтобы она не теряла текущую страницу пагинации.
- Поиск по ФИО. Форма поиска над списком: частичное совпадение, результат с пагинацией, пустой результат — отдельное сообщение, а не пустая таблица.
После каждой задачи: php artisan test (ни один тест не сломался),
php artisan route:list (маршруты на месте) и php artisan about
(окружение не уехало). Затем добавьте тест на новое поведение —
хотя бы один на задачу.
Если что-то в этой части не сошлось с вашей машиной — сравните вывод
php artisan about со слайдом в начале. Почти все расхождения видны там:
версия, окружение, драйвер базы, состояние кэша.