Часть 4 — База данных, миграции и Eloquent
До сих пор список пользователей жил в PHP-массиве внутри контроллера: запрос закончился — данных нет. Здесь у приложения появляется настоящая база.
Что сделаем: переключим Laravel с SQLite на MariaDB, заведём базе отдельного
пользователя, создадим схему миграциями, добавим колонку phone и откатим её,
опишем таблицу моделью User, наполним базу сидером и выведем 12 записей
списком с пагинацией по 5 на страницу.
Что понадобится: проект project-laravel из частей 1–3 (маршрут /users, контроллер,
макет и шаблон), MariaDB из lesson.linux.04, терминал в корне проекта.
Стенд: MariaDB 10.11.14-MariaDB-0ubuntu0.24.04.1, SQLite 3.45.1.
От массива — к базе
В части 3 UserController::index() передавал шаблону массив, записанный прямо
в коде. Для страницы этого хватило, но данные у такого решения бумажные:
PHP-FPM обрабатывает каждый запрос заново и ничего не помнит между запросами
(жизненный цикл запроса вы разбирали в lesson.network.02). Пользователь не
может ни добавить запись, ни исправить её — для этого пришлось бы менять код.
Хранить данные нужно вне процесса: в файле или в базе. Мы берём базу — ту самую
MariaDB, с которой вы работали весь блок lesson.db.
.env до страницы в браузереКаждый этап заканчивается проверяемым результатом: выводом команды, строкой в базе или страницей в браузере.
По умолчанию Laravel работает с SQLite
23:DB_CONNECTION=sqlite24:# DB_HOST=127.0.0.125:# DB_PORT=330626:# DB_DATABASE=laravel27:# DB_USERNAME=root28:# DB_PASSWORD=-rw-rw-rw- 1 student student 106496 Oct 17 10:20 database/database.sqlite
Разбор команды
Перед переключением смотрим, какое соединение задал каркас по умолчанию и есть ли файл базы SQLite.
grep -n "^DB_\|^# DB_" .env; ls -l database/database.sqlite| Токен | Зачем он и что меняет |
|---|---|
grep | утилита поиска строк по шаблону |
-n | печатать номера строк: по ним видно, что блок DB_* занимает строки 23–28, и их легко найти в редакторе |
"^DB_\|^# DB_" | шаблон в двойных кавычках: ^DB_ — строки, начинающиеся с DB_, | — «или» в базовом синтаксисе grep, ^# DB_ — те же строки, но закомментированные. Без кавычек shell принял бы | за пайп |
.env | файл, в котором ищем: конфигурация окружения Laravel, из неё читаются настройки соединения |
; | разделитель двух независимых команд в одной строке: вторая выполнится в любом случае, даже если grep ничего не нашёл |
ls | показать сведения о файле |
-l | длинный формат: права, владелец, размер в байтах и время изменения |
database/database.sqlite | путь к файлу базы SQLite относительно корня проекта |
Что произойдёт
Печатает шесть строк блока DB_* из .env (строка 23 — DB_CONNECTION=sqlite, строки 24–28 начинаются с #) и строку ls: database/database.sqlite, 106496 байт, права -rw-rw-rw-, владелец student.
На что смотреть
DB_CONNECTION=sqlite — соединение по умолчанию, а строки DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD закомментированы: каркас заранее приготовил вариант «серверная СУБД», его нужно раскомментировать и заполнить. Файл не пустой (106496 байт) — миграции в него уже прошли при установке проекта.
Частые ошибки
- Без кавычек вокруг шаблона shell разрежет его по | и
grepполучит только половину условия. grepвозвращает код выхода 1, если совпадений нет: при соединении команд через && вторая команда не выполнилась бы вообще.
Смотрим два факта. В .env на строке 23 стоит DB_CONNECTION=sqlite, а строки
24–28 — DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD —
закомментированы. Каркас заранее приготовил вариант «серверная СУБД»: его нужно
только раскомментировать и заполнить.
Рядом лежит database/database.sqlite — 106496 байт, права -rw-rw-rw-, время
создания Oct 17 10:20. Это не сервер, а обычный файл: миграции в него уже
прошли при установке проекта (часть 1), поэтому он не пустой.
Почему SQLite выбран по умолчанию: ноль настройки. Ни демона, ни порта, ни пароля — один файл в каталоге проекта, удобно для тестов и быстрых демо.
SQLite — библиотека внутри процесса PHP: у неё нет отдельного сервера, нет
пользователей и прав, и консольный клиент mysql с его SHOW TABLES,
DESCRIBE и EXPLAIN к ней не применим. В блоке lesson.db вы работали
именно с сервером MariaDB и именно этими командами; учебное приложение должно
вести себя как боевое.
Приборная панель соединения: php artisan db:show
SQLite .............................................................. 3.45.1 Connection .......................................................... sqlite Database .......................................... database/database.sqlite Host ....................................................................... Port ....................................................................... Username ................................................................... URL ........................................................................ Open Connections ........................................................... Tables ................................................................... 9 Schema / Table ........................................................ Size main / cache ............................................................. — main / cache_locks ....................................................... — main / failed_jobs ....................................................... — main / job_batches ....................................................... — main / jobs .............................................................. — main / migrations ........................................................ — main / password_reset_tokens ............................................. — main / sessions .......................................................... — main / users ............................................................. —
Разбор команды
Проверяем, куда приложение смотрит прямо сейчас, — до переключения на MariaDB.
php artisan db:show| Токен | Зачем он и что меняет |
|---|---|
php | интерпретатор PHP; стенд урока — 8.3, свою версию покажет php -v |
artisan | скрипт командной строки Laravel в корне проекта: все команды фреймворка вызываются как php artisan |
db:show | команда «приборная панель соединения»: драйвер и версия СУБД, база, хост, порт, пользователь, число таблиц и их размеры |
Что произойдёт
Показывает SQLite 3.45.1, соединение sqlite, базу database/database.sqlite; Host, Port, Username и URL пустые; Tables 9, размеры таблиц — прочерки.
На что смотреть
Девять таблиц появились без единой строчки SQL — их создали миграции каркаса при установке. Пустые Host, Port, Username — не поломка вывода: SQLite открывает файл сам процесс PHP, подключаться не к кому.
Частые ошибки
- Прочерки в колонке Size — особенность драйвера SQLite: после переключения на MariaDB размеры таблиц появятся.
db:show отвечает на вопрос «куда приложение смотрит прямо сейчас». Здесь:
SQLite 3.45.1, соединение sqlite, база — путь к файлу, а Host, Port,
Username и URL пустые. Пустые не потому, что вывод сломался: подключаться
не к кому, файл открывает сам процесс PHP. Tables 9, а размеры таблиц —
прочерки: SQLite их так не отдаёт.
Запомните цифру 9: мы не написали ни строчки SQL, а схема уже есть. Её создали миграции каркаса при установке. Чуть ниже разберём, откуда они взялись и что в них.
MariaDB на месте, и root заходит без пароля
version10.11.14-MariaDB-0ubuntu0.24.04.1Databaseinformation_schemamysqlperformance_schemasys
Разбор команды
Убеждаемся, что сервер MariaDB запущен и что root заходит без пароля через unix-сокет.
sudo mysql -e "SELECT VERSION() AS version; SHOW DATABASES;"| Токен | Зачем он и что меняет |
|---|---|
sudo | выполнить от имени root: учётка root@localhost в MariaDB проверяет ОС-пользователя, поэтому без sudo вход не сработает |
mysql | консольный клиент MariaDB — тот же, которым вы работали в блоке lesson.db |
-e | флаг: выполнить SQL из следующего аргумента и выйти, без интерактивной сессии — поэтому вывод можно снять в терминал |
"SELECT VERSION() AS version; SHOW DATABASES;" | аргумент флага -e целиком: два запроса в двойных кавычках, разделённые точкой с запятой |
SELECT VERSION() AS version | запрос версии сервера; AS version задаёт имя колонки в выводе |
; | разделитель запросов внутри строки SQL |
SHOW DATABASES | список баз на сервере |
Что произойдёт
Сервер отвечает версией 10.11.14-MariaDB-0ubuntu0.24.04.1 и списком четырёх служебных баз: information_schema, mysql, performance_schema, sys. Код выхода 0.
На что смотреть
Пароль не спросили: соединение идёт через локальный unix-сокет, а плагин unix_socket проверяет ОС-пользователя (мы пришли через sudo), а не пароль. Нашей базы в списке пока нет.
Частые ошибки
- Без
sudoклиент придёт от имениstudent— такой учётки у MariaDB нет, и вход закончится отказом в доступе.
Это тот самый консольный клиент из lesson.db.vi.01 — только без привычных
-u root -p: пароль не спросили. Версия на стенде —
10.11.14-MariaDB-0ubuntu0.24.04.1. В списке четыре служебные базы
(information_schema, mysql, performance_schema, sys); нашей пока нет.
Почему пускает без пароля: у учётки root@localhost в MariaDB на Ubuntu и Mint
включён плагин unix_socket. Сервер проверяет не пароль, а то, что соединение
пришло через локальный сокет и что ОС-пользователь — root (мы вошли через
sudo). Доверие берётся из операционной системы.
Но приложение так зайти не сможет
ERROR 1698 (28000): Access denied for user 'root'@'localhost'Разбор команды
Проверяем, пустит ли сервер root по TCP, — именно так к базе подключается Laravel.
mysql -h 127.0.0.1 -u root -e "SELECT 1;"| Токен | Зачем он и что меняет |
|---|---|
mysql | консольный клиент MariaDB |
-h 127.0.0.1 | хост: соединение по TCP вместо локального unix-сокета. Единственное отличие от прошлой команды — и оно решает всё |
-u root | пользователь сервера; пароль не передаём, потому что у root@localhost его нет — работает плагин unix_socket |
-e "SELECT 1;" | выполнить один запрос и выйти; SELECT 1 — простейшая проверка «соединение есть» |
Что произойдёт
Отказ в доступе: ERROR 1698 (28000): Access denied for user 'root'@'localhost', код выхода 1.
На что смотреть
1698 — ответ плагина unix_socket на сетевое соединение: проверять ОС-пользователя нечего, поэтому root по TCP не проходит. Laravel ходит в базу по DB_HOST=127.0.0.1 и DB_PORT=3306, то есть ровно так же, — под root приложение не заработает, сколько бы раз вы ни перезапускали nginx и php-fpm.
Частые ошибки
- Ошибка выглядит как «в терминале заходит, из кода — нет»: причина в способе соединения (сокет против TCP), а не в пароле.
- Соблазн задать
rootпароль и вписать его в.envдаёт коду праваrootна весь сервер: все базы и возможность выдавать права другим. Нужен отдельный пользователь с правами на одну базу.
Та же MariaDB, тот же root — и ERROR 1698 (28000): Access denied for user
'root'@'localhost', код выхода 1. Отличие от прошлой команды одно:
-h 127.0.0.1, то есть мы пришли по TCP, а не через unix-сокет. Плагину
unix_socket нечего проверять — у сетевого соединения нет ОС-пользователя,
поэтому в доступе отказано.
Почему это касается нас напрямую: Laravel подключается к базе так, как написано
в .env — DB_HOST=127.0.0.1, DB_PORT=3306, то есть по TCP. Значит, под
root приложение не заработает никогда, сколько бы раз мы ни перезапускали
nginx и php-fpm. Ошибка при этом выглядит обидно непонятной: «в терминале
заходит, из кода — нет».
Соблазн — задать root пароль и вписать его в .env. Заработает, но тогда у
кода приложения появятся права root на весь сервер MariaDB: все базы, все
таблицы, возможность выдавать права другим пользователям. Одна удачная
SQL-инъекция (вы эксплуатировали такую в lesson.sql-injection.i.02) — и
пострадает не только наша база.
Заводим базу и пользователя приложения
Databaseinformation_schemamysqlperformance_schemaproject_laravelsys
Разбор команды
Заводим приложению отдельную базу и отдельного пользователя с правами только на неё — и сразу проверяем результат.
sudo mysql -e "CREATE DATABASE IF NOT EXISTS project_laravel CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER IF NOT EXISTS 'project_laravel'@'localhost' IDENTIFIED BY 'secret'; GRANT ALL PRIVILEGES ON project_laravel.* TO 'project_laravel'@'localhost'; FLUSH PRIVILEGES;"| Токен | Зачем он и что меняет |
|---|---|
sudo | клиент идёт через unix-сокет от root: создавать базы и пользователей может только администратор сервера |
mysql | консольный клиент MariaDB |
-e | флаг: выполнить SQL из следующего аргумента и выйти, без интерактивной сессии |
"..." | весь SQL — один аргумент в двойных кавычках: четыре команды через точку с запятой; одинарные кавычки внутри shell не трогает |
CREATE DATABASE IF NOT EXISTS project_laravel | создать базу project_laravel; IF NOT EXISTS делает команду повторимой — второй запуск не упадёт с ошибкой «база уже существует» |
CHARACTER SET utf8mb4 | кодировка базы: полноценный UTF-8, до четырёх байт на символ, чтобы кириллица не обрезалась |
COLLATE utf8mb4_unicode_ci | правила сравнения строк; ci означает «без учёта регистра». От collation зависит и сортировка кириллицы в ORDER BY name |
CREATE USER IF NOT EXISTS 'project_laravel'@'localhost' | учётка приложения; запись читается как «пользователь project_laravel, пришедший с localhost» |
IDENTIFIED BY 'secret' | пароль этой учётки; он же попадёт в .env как DB_PASSWORD |
GRANT ALL PRIVILEGES ON project_laravel.* | все права, но маска project_laravel.* ограничивает их таблицами одной базы и ничем за её пределами |
TO 'project_laravel'@'localhost' | кому выдаём права; host-часть должна совпадать с CREATE USER, иначе права уйдут другой учётке |
FLUSH PRIVILEGES | перечитать таблицы прав, чтобы изменения подействовали сразу, без перезапуска сервера |
; | разделитель SQL-команд внутри строки, переданной через -e |
'...' | одинарные кавычки — строки SQL (имя базы, пользователь, пароль); внутри двойных кавычек shell их не трогает |
Что произойдёт
Успешный DDL ничего не печатает: вывод пустой, код выхода 0.
На что смотреть
Тишина — признак успеха. Ошибка SQL напечаталась бы в stderr и дала ненулевой код выхода.
Частые ошибки
- Права на . вместо project_laravel.* сделали бы пользователя приложения администратором всего сервера — смысл отдельной учётки потерялся бы.
- Пароль, написанный в командной строке, попадает в историю shell и виден в списке процессов; на общей машине используйте
-pбез значения (клиент спросит пароль сам) или файл~/.my.cnf.
sudo mysql -e "SHOW DATABASES;"| Токен | Зачем он и что меняет |
|---|---|
sudo | снова входим через unix-сокет от root |
mysql | консольный клиент MariaDB |
-e "SHOW DATABASES;" | контрольный запрос: список баз после создания |
Что произойдёт
В списке появилась project_laravel — пять баз вместо четырёх служебных.
На что смотреть
project_laravel видна серверу: значит, CREATE DATABASE отработал, а не только «не выдал ошибку».
Один вызов sudo mysql -e с четырьмя командами и контрольный SHOW DATABASES:
в списке появилась project_laravel. Разбор по частям:
CREATE DATABASE IF NOT EXISTS project_laravelсCHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci— имя базы совпадает с проектом и доменомproject-laravel.rsиз части 2.IF NOT EXISTSделает команду повторимой: второй запуск не упадёт.utf8mb4— полноценный UTF-8, до четырёх байт на символ, поэтому кириллица и прочие символы не обрезаются;utf8mb4_unicode_ci— правила сравнения строк,ciв названии означает «без учёта регистра».CREATE USER IF NOT EXISTS 'project_laravel'@'localhost' IDENTIFIED BY 'secret'— отдельная учётка именно для приложения. Запись'project_laravel'@'localhost'читается как «пользовательproject_laravel, пришедший с localhost».GRANT ALL PRIVILEGES ON project_laravel.* TO 'project_laravel'@'localhost'— все права, но только внутриproject_laravel: маскаproject_laravel.*— это все таблицы одной базы и ничего за её пределами.FLUSH PRIVILEGES— перечитать таблицы прав, чтобы изменения подействовали сразу, без перезапуска сервера.
Смысл отдельного пользователя — минимальные права. Если пароль утечёт или
запрос соберётся из пользовательских данных неправильно, ущерб ограничен одной
базой. Это тот же принцип, что и в lesson.sql-injection.i.03: не «разрешить
всё и потом проверять», а заранее сузить набор возможного.
secret — учебный пароль, и дальше мы будем вызывать клиент как
mysql -u project_laravel -psecret. Так удобно снимать вывод, но пароль при этом
попадает в историю shell и виден в списке процессов. На общей машине
используйте -p без значения (клиент спросит пароль сам) или файл ~/.my.cnf.
Переключаем .env на MariaDB
Файл .env целиком. Подсвечены строки, которые важны в этой части:
5 — APP_URL, 16 — BCRYPT_ROUNDS, 23–28 — блок DB_*.
5:APP_URL=http://project-laravel.rs23:DB_CONNECTION=mysql24:DB_HOST=127.0.0.125:DB_PORT=330626:DB_DATABASE=project_laravel27:DB_USERNAME=project_laravel28:DB_PASSWORD=secret
Разбор команды
После правки .env проверяем, что блок DB_* раскомментирован и заполнен, а APP_URL совпадает с доменом проекта.
grep -n "^DB_\|^APP_URL" .env| Токен | Зачем он и что меняет |
|---|---|
grep | поиск строк по шаблону |
-n | номера строк: видно, что блок DB_* занял строки 23–28, а APP_URL — строку 5 |
"^DB_\|^APP_URL" | шаблон: строки, начинающиеся с DB_, или строки, начинающиеся с APP_URL; | — «или» в базовом синтаксисе grep |
.env | файл окружения проекта: из него Laravel берёт настройки соединения и адрес приложения |
Что произойдёт
Печатает APP_URL=http://project-laravel.rs на строке 5 и шесть строк DB_* на строках 23–28: mysql, 127.0.0.1, 3306, project_laravel, project_laravel, secret.
На что смотреть
Строки DB_* больше не начинаются с # — значения действительно читаются. DB_CONNECTION=mysql подходит и MySQL, и MariaDB: протокол у них общий, поэтому переезд оказался заменой шести строк.
Частые ошибки
- Если конфигурацию закэшировали командой
php artisan config:cache, правки.envприменяться не будут — кэш важнее файла (подробно в части 6).
Проверка: тот же файл, но только нужные строки и с номерами.
Шесть строк блока DB_* — всё, что нужно для соединения:
DB_CONNECTION=mysql— имя драйвера изconfig/database.php. Одно и то же значение подходит и MySQL, и MariaDB: протокол у них общий, поэтому «переезд» оказался заменой шести строк.DB_HOST=127.0.0.1иDB_PORT=3306— TCP. Именно поэтомуrootс прошлого слайда не годится.DB_DATABASE,DB_USERNAME,DB_PASSWORD— базаproject_laravel, пользователь с тем же именем и парольsecret.
Проверка справа — grep -n "^DB_\|^APP_URL" .env: строки 23–28 больше не
закомментированы, а APP_URL=http://project-laravel.rs на строке 5 совпадает с доменом
из части 2. Обратите внимание на BCRYPT_ROUNDS=12 (строка 16) — к этой строке
вернёмся, когда будем смотреть на хеш пароля.
.env, а не в кодеНа рабочем сервере база другая, и менять ради неё файлы проекта неправильно:
код один, окружения разные. .env не коммитят (часть 1), а значения читаются
через config/database.php. Отсюда важное следствие: если конфигурацию
закэшировали командой php artisan config:cache, правки .env применяться не
будут — кэш важнее файла. Подробно разберём в части 6.
Миграция — это изменение схемы, записанное кодом
Структуру таблицы вы уже меняли руками: в lesson.dbii.03 тип поля category
правили запросом ALTER TABLE из phpMyAdmin. Работает, но у такого изменения
нет истории — оно осталось в базе и в памяти того, кто его делал.
Миграция — тот же ALTER TABLE (или CREATE TABLE), но записанный в PHP-файл
в каталоге database/migrations/. У файла два метода: up() — что сделать,
down() — как отменить. Файл попадает в git, и любая машина — ваша, коллеги,
боевой сервер — получает одинаковую схему одной командой.
| Вопрос | Правка руками | Миграция |
|---|---|---|
| Где живёт изменение | в базе и в голове автора | в файле в репозитории |
| Как повторить на другой машине | вспомнить и набрать заново | php artisan migrate |
| Как отменить | писать обратный SQL по памяти | php artisan migrate:rollback |
| Что уже применено | неизвестно | журнал в таблице migrations |
| В каком порядке применять | на глаз | по метке времени в имени файла |
Порядок обеспечивает имя файла: 2026_10_17_102101_add_phone_to_users_table.php
начинается с даты и времени создания. Laravel применяет файлы по алфавиту, то
есть хронологически, и помнит в базе, какие уже выполнены.
Пересоздаём схему уже в MariaDB
INFO Preparing database. Creating migration table ...................................... 30.43ms DONE INFO Running migrations. 0001_01_01_000000_create_users_table ......................... 211.97ms DONE 0001_01_01_000001_create_cache_table ......................... 135.60ms DONE 0001_01_01_000002_create_jobs_table .......................... 221.05ms DONE
Разбор команды
База только что создана и пуста: прогоняем в ней схему заново — служебную таблицу migrations и три миграции каркаса.
php artisan migrate:fresh| Токен | Зачем он и что меняет |
|---|---|
php | интерпретатор PHP (стенд — 8.3; свою версию покажет php -v) |
artisan | CLI-скрипт Laravel в корне проекта |
migrate:fresh | удалить все таблицы текущего соединения и заново прогнать все файлы из database/migrations |
Что произойдёт
Создана таблица migrations (30.43ms), затем применены три миграции каркаса: 0001_01_01_000000_create_users_table (211.97ms), 0001_01_01_000001_create_cache_table (135.60ms), 0001_01_01_000002_create_jobs_table (221.05ms) — все DONE.
На что смотреть
Все три файла — не наши: они лежат в database/migrations с момента установки проекта. Они прошли одним запуском, поэтому получат общий номер батча — это видно в migrate:status.
Частые ошибки
- На базе с реальными данными
migrate:freshуничтожит всё: для обычной работы естьphp artisan migrate, он применяет только файлы, которых ещё нет в журнале. - Если
.envне переключён, команда отработает по SQLite-файлу — соединение проверяют черезphp artisan db:show.
Читаем вывод сверху вниз:
Preparing database→Creating migration table ... 30.43ms DONE. Laravel создал служебную таблицуmigrations— журнал применённых миграций. Её не было, потому что базаproject_laravelсоздана только что.Running migrations→ три файла, каждый со своим временем: 211.97ms, 135.60ms, 221.05ms. Это миграции каркаса, мы их не писали: они лежат вdatabase/migrations/с момента установки проекта.
Имена говорят сами за себя: create_users_table, create_cache_table,
create_jobs_table. Внутри этих файлов — обычный PHP с вызовами
Schema::create(...), из которых Laravel собирает SQL под текущую СУБД. Какие
именно таблицы кто создаёт, посмотрим на следующем слайде.
migrate:fresh удаляет данныеfresh означает «снести все таблицы текущего соединения и прогнать миграции
заново». На учебной базе это удобно: схема всегда чистая, без мусора от
экспериментов. На базе с реальными данными команда уничтожит всё. Для обычной
работы есть php artisan migrate — он применяет только те файлы, которых ещё
нет в журнале.
Три файла каркаса: кто какие таблицы создаёт
Подсвечены вызовы Schema::create(...) — из них и складываются те девять таблиц,
которые показал SHOW TABLES.
| Миграция | Таблицы | Зачем они приложению |
|---|---|---|
0001_01_01_000000_create_users_table |
users, password_reset_tokens, sessions |
пользователи; токены сброса пароля; сессии — в .env стоит SESSION_DRIVER=database |
0001_01_01_000001_create_cache_table |
cache, cache_locks |
кэш — CACHE_STORE=database |
0001_01_01_000002_create_jobs_table |
jobs, job_batches, failed_jobs |
очередь — QUEUE_CONNECTION=database |
Внутри обычный PHP, и его видно в схеме: строка $table->string('email')->unique()
из первой колонки превратилась в email varchar(255) NO UNI из DESCRIBE users.
down() у каждой миграции зеркальный — Schema::dropIfExists(...) для тех же
таблиц, поэтому откат каркасных миграций тоже работает.
Что физически появилось в базе
Tables_in_project_laravelcachecache_locksfailed_jobsjob_batchesjobsmigrationspassword_reset_tokenssessionsusersField Type Null Key Default Extraid bigint(20) unsigned NO PRI NULL auto_incrementname varchar(255) NO NULL email varchar(255) NO UNI NULL email_verified_at timestamp YES NULL password varchar(255) NO NULL remember_token varchar(100) YES NULL created_at timestamp YES NULL updated_at timestamp YES NULL
Разбор команды
Смотрим глазами клиента, что физически появилось в базе: список таблиц и схему users.
mysql -u project_laravel -psecret project_laravel -e "SHOW TABLES; DESCRIBE users;"| Токен | Зачем он и что меняет |
|---|---|
mysql | консольный клиент MariaDB |
-u project_laravel | пользователь приложения, которого мы создали; не root — проверка, что прав на одну базу хватает |
-psecret | пароль слитно с флагом: у -p значение пишется без пробела. Учебный приём — на общей машине так делать не стоит |
project_laravel | позиционный аргумент: базу открыть сразу после подключения, чтобы SHOW/SELECT работали без USE |
-e | флаг: выполнить SQL из следующего аргумента и выйти, без интерактивной сессии |
"SHOW TABLES; DESCRIBE users;" | два запроса одной строкой: список таблиц открытой базы и схема таблицы users — колонка, тип, Null, Key, Default, Extra |
Что произойдёт
Девять таблиц (cache, cache_locks, failed_jobs, job_batches, jobs, migrations, password_reset_tokens, sessions, users) и восемь колонок users: id bigint(20) unsigned с PRI и auto_increment, name varchar(255), email varchar(255) с UNI, email_verified_at, password, remember_token, created_at, updated_at.
На что смотреть
Предметная область здесь — только users; остальные таблицы появились потому, что в .env стоят SESSION_DRIVER=database, CACHE_STORE=database и QUEUE_CONNECTION=database. NO в столбце Null означает NOT NULL (значение обязательно), UNI — уникальный индекс на email.
Частые ошибки
- Пробел после
-pломает команду: клиент спросит пароль интерактивно, а следующее слово примет за имя базы. - Пароль в командной строке виден в истории shell и в списке процессов.
Команда — тот же клиент mysql, но от имени пользователя приложения:
mysql -u project_laravel -psecret project_laravel -e "SHOW TABLES; DESCRIBE users;" —
пользователь, пароль, база и запрос в одну строку.
SHOW TABLES показывает девять таблиц. Наша предметная область здесь — только
users. Остальные появились не «просто так»: в .env стоят
SESSION_DRIVER=database, CACHE_STORE=database и QUEUE_CONNECTION=database,
поэтому сессии, кэш и очередь Laravel хранит в той же базе. migrations —
журнал из прошлого слайда.
DESCRIBE users — восемь колонок:
| Колонка | Тип и флаги | Что это значит |
|---|---|---|
id |
bigint(20) unsigned, NO, PRI, auto_increment |
первичный ключ, номер растёт сам; в lesson.db.iv.03 вы доставали его через PDO::lastInsertId() |
name |
varchar(255), NO |
строка до 255 символов, значение обязательно |
email |
varchar(255), NO, UNI |
уникальный: вторую запись с тем же адресом не пропустит сама база |
email_verified_at |
timestamp, YES |
может быть NULL — адрес ещё не подтвердили |
password |
varchar(255), NO |
здесь будет хеш, а не пароль — увидим ниже |
remember_token |
varchar(100), YES |
токен «запомнить меня» |
created_at, updated_at |
timestamp, YES |
метки времени; модель проставляет их сама |
Столбец Null — это про NOT NULL: NO значит «значение обязательно», YES —
«можно оставить пустым».
Журнал миграций: php artisan migrate:status
Migration name .............................................. Batch / Status 0001_01_01_000000_create_users_table ............................... [1] Ran 0001_01_01_000001_create_cache_table ............................... [1] Ran 0001_01_01_000002_create_jobs_table ................................ [1] Ran
Разбор команды
Проверяем журнал миграций: какие файлы применены и каким батчем.
php artisan migrate:status| Токен | Зачем он и что меняет |
|---|---|
php artisan | CLI Laravel: интерпретатор и скрипт проекта |
migrate:status | команда печатает список файлов миграций с номером батча и статусом (Ran — применён, Pending — лежит на диске, но не выполнен) |
Что произойдёт
Три строки: 0001_01_01_000000_create_users_table, 0001_01_01_000001_create_cache_table, 0001_01_01_000002_create_jobs_table — у всех батч [1] и статус Ran.
На что смотреть
[1] — всё применилось одним запуском migrate:fresh. Откат работает батчами: migrate:rollback --step=1 отменяет последний из них.
Частые ошибки
- Ситуация «локально работает, а на сервере нет колонки» почти всегда означает, что на сервере просто не запустили
migrate:migrate:statusпоказывает Pending-файлы за одну секунду.
Три строки — три применённые миграции, у всех батч [1] и статус Ran. Батч —
это «пачка»: всё, что применилось одним запуском migrate, получает общий
номер. Именно батчами работает откат: migrate:rollback --step=1 отменяет
последний из них.
Зачем эта команда в ежедневной работе: она отвечает на два вопроса — что база
уже видела и какие файлы лежат на диске, но ещё не применены. Ситуация «локально
работает, а на сервере нет колонки» почти всегда означает, что на сервере просто
не запустили migrate; migrate:status показывает это за одну секунду.
Нужна колонка phone — делаем новую миграцию
INFO Migration [database/migrations/2026_10_17_102101_add_phone_to_users_table.php] created successfully. Разбор команды
Колонки phone в таблице нет — создаём под неё отдельный файл миграции.
php artisan make:migration add_phone_to_users_table --table=users| Токен | Зачем он и что меняет |
|---|---|
php artisan | CLI Laravel |
make:migration | генератор файла миграции в database/migrations |
add_phone_to_users_table | имя миграции: artisan превращает его в имя файла и добавляет префикс из даты и времени создания — по нему строится порядок применения |
--table=users | подсказка генератору: менять существующую таблицу users. В заготовке появится Schema::table('users', ...), а не Schema::create(...) |
Что произойдёт
Создан файл database/migrations/2026_10_17_102101_add_phone_to_users_table.php: INFO ... created successfully.
На что смотреть
Дата и время в имени файла — не украшение: Laravel применяет файлы по алфавиту, то есть хронологически. Сам файл пока заготовка — код up() и down() пишем мы.
Частые ошибки
- Дописывать колонку в уже применённую миграцию нельзя: на вашей машине ничего не произойдёт (файл есть в журнале), а на чистой схеме колонка появится — схемы разойдутся при одинаковом коде.
Приложение хранит ФИО, телефон, email и пароль, но колонки phone в таблице
нет: каркас о ней не знает. Добавляем.
artisan превращает имя миграции в имя файла: add_phone_to_users_table →
database/migrations/2026_10_17_102101_add_phone_to_users_table.php, где
префикс — дата и время создания. Параметр --table=users подсказывает
генератору, что менять будем существующую таблицу: в заготовке сразу появится
Schema::table('users', ...), а не Schema::create(...).
phone в старую миграциюФайл 0001_01_01_000000_create_users_table.php уже применён и записан в
журнал. Если дописать колонку в него:
- на вашей машине не произойдёт ничего —
migrateзапускает только файлы, которых нет в таблицеmigrations; - на чистой машине (у коллеги или на сервере) колонка появится — и схемы разойдутся при одинаковом коде;
- из истории исчезнет сам факт изменения: по коду будет не понять, что и когда менялось.
Правило простое: применённая миграция неизменяема, каждое следующее изменение схемы — новый файл. Как с коммитами: историю не переписывают, добавляют новый коммит.
Заготовка и наш код
Что создал artisan
Что написали мы
Заготовка — анонимный класс (return new class extends Migration) с двумя
пустыми замыканиями и комментариями // внутри. Мы заполняем оба:
up():$table->string('phone', 32)->after('name')— колонкаphoneтипаVARCHAR(32), обязательная, встать сразу послеname. Длины 32 хватает с запасом: самый длинный телефон в нашем сидере —+7 (912) 345-67-89, 18 символов.down():$table->dropColumn('phone')— точная обратная операция. Без неё миграция необратима и откатить её не получится.
Методы $table->... — не SQL, а PHP-обёртка: один и тот же код Laravel
превратит в ALTER TABLE для MariaDB, SQLite или PostgreSQL. Именно поэтому
переезд с SQLite на MariaDB не потребовал переписать ни одного файла миграций.
Применяем и смотрим схему
INFO Running migrations. 2026_10_17_102101_add_phone_to_users_table .................... 48.72ms DONE
Разбор команды
Применяем только что написанную миграцию с колонкой phone.
php artisan migrate| Токен | Зачем он и что меняет |
|---|---|
php artisan | CLI Laravel |
migrate | применить файлы, которых ещё нет в журнале migrations; в отличие от migrate:fresh ничего не удаляет и не пересоздаёт |
Что произойдёт
Выполнен один файл — 2026_10_17_102101_add_phone_to_users_table, 48.72ms DONE. Три миграции каркаса не тронуты.
На что смотреть
Запустился ровно тот файл, которого не было в журнале: это обычный рабочий цикл — добавили миграцию, выполнили migrate, ничего лишнего не пересоздалось.
Частые ошибки
- Если
migrate«ничего не делает», файл уже есть в журнале — проверьтеphp artisan migrate:status.
Field Type Null Key Default Extraid bigint(20) unsigned NO PRI NULL auto_incrementname varchar(255) NO NULL phone varchar(32) NO NULL email varchar(255) NO UNI NULL email_verified_at timestamp YES NULL password varchar(255) NO NULL remember_token varchar(100) YES NULL created_at timestamp YES NULL updated_at timestamp YES NULL
Разбор команды
Проверяем, что колонка phone действительно появилась в схеме таблицы.
mysql -u project_laravel -psecret project_laravel -e "DESCRIBE users;"| Токен | Зачем он и что меняет |
|---|---|
mysql | консольный клиент MariaDB |
-u project_laravel | пользователь приложения, которого мы создали; не root — проверка, что прав на одну базу хватает |
-psecret | пароль слитно с флагом: у -p значение пишется без пробела. Учебный приём — на общей машине так делать не стоит |
project_laravel | позиционный аргумент: базу открыть сразу после подключения, чтобы SHOW/SELECT работали без USE |
-e | флаг: выполнить SQL из следующего аргумента и выйти |
"DESCRIBE users;" | схема таблицы: Field, Type, Null, Key, Default, Extra |
Что произойдёт
Девять колонок: между name и email встала phone varchar(32) с NO в столбце Null.
На что смотреть
Порядок колонок подтверждает ->after('name') из миграции. Обязательная колонка добавилась без DEFAULT, потому что в таблице сейчас ноль строк — нечего заполнять.
Частые ошибки
- На заполненной таблице обязательная колонка без значения по умолчанию так не добавится: старым записям нечего положить в новое поле — нужен DEFAULT или заполнение существующих строк отдельным шагом.
migrate запустил ровно один файл — наш, за 48.72ms. Три миграции каркаса он не
трогал: они уже в журнале. Это и есть обычный рабочий цикл — добавили файл,
выполнили migrate, ничего лишнего не пересоздалось.
DESCRIBE users теперь показывает девять колонок: phone varchar(32), NO в
столбце Null, стоит третьей — сразу после name. Сработал ->after('name').
NOT NULL и пустая таблицаОбязательная колонка добавилась без значения по умолчанию, потому что в таблице
сейчас ноль строк — нечего заполнять. На заполненной таблице так не получится:
старым записям нечего положить в новое поле, поэтому нужен либо DEFAULT, либо
заполнение существующих строк отдельным шагом. Порядок «сначала схема, потом
данные» в учебном проекте спасает именно здесь.
Миграцию можно откатить
INFO Rolling back migrations. 2026_10_17_102101_add_phone_to_users_table .................... 53.27ms DONE INFO Running migrations. 2026_10_17_102101_add_phone_to_users_table .................... 43.53ms DONE
Разбор команды
Показываем, что миграция обратима: откатываем последний батч, проверяем, что колонки нет, и применяем миграцию снова.
php artisan migrate:rollback --step=1| Токен | Зачем он и что меняет |
|---|---|
php artisan | CLI Laravel |
migrate:rollback | выполнить down() у применённых миграций — отменить изменение схемы |
--step=1 | сколько последних батчей откатить; 1 — последний. Совпадает с поведением по умолчанию, но делает намерение явным |
Что произойдёт
Rolling back migrations: файл 2026_10_17_102101_add_phone_to_users_table откатился за 53.27ms, DONE.
На что смотреть
Откатился именно наш файл — последний батч. Каркасные миграции остались применёнными.
Частые ошибки
- Откат на живой базе уничтожает данные колонки вместе с ней:
dropColumn('phone')— это потеря телефонов. На рабочем сервере откатывают редко и имея резервную копию.
mysql -u project_laravel -psecret project_laravel -e "SHOW COLUMNS FROM users LIKE 'phone';"| Токен | Зачем он и что меняет |
|---|---|
mysql | консольный клиент MariaDB |
-u project_laravel | пользователь приложения |
-psecret | пароль слитно с флагом -p |
project_laravel | база, которую открыть при подключении |
-e "SHOW COLUMNS FROM users LIKE 'phone';" | запрос колонок таблицы с фильтром по имени: если колонки нет, запрос не вернёт ни одной строки |
Что произойдёт
Вывода нет вообще: между двумя блоками INFO в захвате пусто. Колонки phone больше нет — down() отработал.
На что смотреть
Пустой вывод — тоже вывод. SHOW COLUMNS печатает заголовок таблицы только тогда, когда есть что печатать; тишина означает «не нашлось».
php artisan migrate| Токен | Зачем он и что меняет |
|---|---|
php artisan | CLI Laravel |
migrate | применить файлы, которых нет в журнале: после отката наш файл снова стал неприменённым |
Что произойдёт
Миграция 2026_10_17_102101_add_phone_to_users_table применилась заново за 43.53ms, DONE.
На что смотреть
Круг замкнулся: колонка вернулась. Миграция — обратимая операция, а не «одноразовый SQL по памяти».
В захвате три команды подряд, и читать их надо вместе с выводом:
php artisan migrate:rollback --step=1— отменил последний батч:Rolling back migrations, наш файл, 53.27msDONE.mysql ... -e "SHOW COLUMNS FROM users LIKE 'phone';"— между двумя блокамиINFOв выводе пусто. Это и есть ответ: запрос не вернул ни одной строки, колонкиphoneбольше нет.down()отработал.php artisan migrate— применил миграцию заново, 43.53msDONE.
Пустой вывод — тоже вывод. SHOW COLUMNS ... LIKE печатает заголовок таблицы
только тогда, когда есть что печатать; тишина означает «не нашлось».
dropColumn('phone') уничтожает и данные колонки. На учебной базе это
демонстрация обратимости; на базе с реальными пользователями откат — потеря
телефонов. Поэтому down() пишут честно, но на рабочем сервере откатывают редко
и только имея резервную копию.
Модель User: класс, который описывает строку таблицы
В блоке ООП (lesson.oop.heroes.06) вы превращали массив данных в объект:
класс со свойствами и методами. Модель — то же самое. Один объект User — одна
строка таблицы users; вызовы вида User::create(...) и User::where(...) —
запросы ко всей таблице.
Связь с таблицей Laravel находит по соглашению: класс User в пространстве
имён App\Models → таблица users. Ни имя таблицы, ни подключение указывать не
нужно.
Что здесь важно:
#[Fillable(['name', 'phone', 'email', 'password'])]— белый список полей, которые разрешено заполнять одним массивом. Поля вне списка при массовом присваивании игнорируются.#[Hidden(['password', 'remember_token'])]— эти поля не попадут в массив или JSON, если модель сериализовать: хеш пароля не должен утекать в ответе.casts()— преобразования значений:email_verified_atчитается как дата, аpasswordпри записи хешируется ('hashed').extends Authenticatable— базовый класс для пользователей, которых можно аутентифицировать по паролю.
Про mass assignment и #[Fillable] подробно поговорим в части 5, когда данные
придут из HTML-формы.
Пробуем модель в tinker
id=1Разбор команды
Пробуем модель в деле: создаём запись через Eloquent и смотрим, какой первичный ключ она получила.
php artisan tinker --execute='$u = App\Models\User::create(["name" => "Тестов Тест Тестович", "phone" => "+7 900 000-00-00", "email" => "test@example.com", "password" => "secret"]); echo "id=" . $u->id, PHP_EOL;'| Токен | Зачем он и что меняет |
|---|---|
php artisan | CLI Laravel |
tinker | интерактивная консоль внутри загруженного приложения: .env прочитан, соединение с базой установлено, все классы доступны |
--execute='...' | выполнить код одной строкой и выйти вместо запуска интерактивной сессии: проверку можно повторить и снять вывод |
'...' | одинарные кавычки shell вокруг кода PHP. Именно одинарные: $u и $u->id не раскрываются как переменные shell и доезжают до PHP как есть |
$u = | переменная PHP, в неё попадает созданная модель |
App\Models\User::create([...]) | статический вызов модели: массив полей превращается в INSERT. Двойные кавычки внутри — строки PHP, их shell не трогает из-за внешних одинарных |
["name" => ..., "phone" => ..., "email" => ..., "password" => ...] | ассоциативный массив «колонка => значение»; все четыре поля есть в #[Fillable], поэтому все четыре и записались |
; | разделитель выражений PHP внутри одной строки |
-> | обращение к свойству объекта: $u->id — первичный ключ вставленной строки |
echo "id=" . $u->id, PHP_EOL; | вывод результата: . — конкатенация строк, запятая передаёт echo второй аргумент, PHP_EOL — перевод строки |
Что произойдёт
Печатает id=1: запись вставлена, объект знает свой первичный ключ (в курсе вы получали его вручную через PDO::lastInsertId()).
На что смотреть
created_at и updated_at модель проставила сама — в массиве мы их не передавали. Пароль ушёл в базу не строкой secret, а bcrypt-хешем: сработал каст 'password' => 'hashed'.
Частые ошибки
- В двойных кавычках shell раскрыл бы $u до запуска PHP, и код приехал бы поломанным: для
--executeберите одинарные кавычки. - Поля вне
#[Fillable]при массовом присваивании молча игнорируются — запись создастся, но не со всеми значениями.
php artisan tinker запускает интерактивную консоль внутри приложения: Laravel
загружен, .env прочитан, соединение с базой установлено, все классы доступны.
Флаг --execute='...' выполняет одну строку и выходит — так удобнее и писать
проверки, и снимать вывод.
Что происходит в этой строке:
App\Models\User::create([...])— статический вызов модели. Массив из четырёх полей превращается вINSERT. Все четыре есть в#[Fillable], поэтому все четыре и записались.echo "id=" . $u->id— после вставки объект знает свой первичный ключ:id=1. В курсе вы получали его вручную черезPDO::lastInsertId().created_atиupdated_atмодель проставила сама: вDESCRIBE usersэти колонки есть, а в массиве мы их не передавали.
В базе лежит не пароль, а его хеш
$2y$12$X8CYf5xpmRVEpuuKp0Et.ehSF7Ic0OMgNYLQTA/JFBKCRfe3X2YWuРазбор команды
Смотрим, что реально лежит в колонке password после создания записи.
php artisan tinker --execute='echo App\Models\User::where("email", "test@example.com")->value("password"), PHP_EOL;'| Токен | Зачем он и что меняет |
|---|---|
php artisan tinker | консоль внутри загруженного приложения |
--execute='...' | одна строка PHP и выход; одинарные кавычки защищают код от подстановок shell |
App\Models\User::where("email", "test@example.com") | условие выборки: колонка и значение; в SQL превратится в prepared statement с плейсхолдером |
->value("password") | вернуть значение одной колонки первой подходящей строки, а не объект модели |
echo ..., PHP_EOL; | напечатать значение и перевод строки |
Что произойдёт
Строка из 60 символов: $2y$12$X8CYf5xpmRVEpuuKp0Et.ehSF7Ic0OMgNYLQTA/JFBKCRfe3X2YWu — хеш, а не переданный нами secret.
На что смотреть
Префикс $2y$ — идентификатор алгоритма bcrypt, 12 — стоимость (число раундов) из BCRYPT_ROUNDS=12 в .env, дальше соль и результат в одной строке. Хеш необратим: из него нельзя получить пароль, можно только перебирать варианты.
Частые ошибки
- Открытый пароль в колонке означает, что каст 'password' => 'hashed' в модели не сработал или значение писали в обход модели (например, строкой SQL).
Мы передали 'password' => 'secret', а запрос value('password') вернул строку
из 60 символов. Разбираем префикс $2y$12$:
$2y$— идентификатор алгоритма bcrypt;12— стоимость, то есть число раундов; ровно столько задано в.envстрокойBCRYPT_ROUNDS=12;- дальше — соль и результат в одной строке.
Хеширование сделал каст 'password' => 'hashed' из casts() модели: значение
превращается в хеш в момент присваивания, до INSERT. В SQL-запрос открытый
пароль не попадает.
Зачем это нужно. Хеш необратим: из $2y$12$... нельзя получить secret, можно
только перебирать варианты и сравнивать. Стоимость 12 делает каждый перебор
заметно дороже, чем «просто быстрый хеш». Если база утечёт, злоумышленник
получит хеши, а не пароли, которыми люди пользуются на других сайтах.
В lesson.http.iii.02 вы собирали форму регистрации, и ведущий отдельно
отметил: пароль уходит в POST открытым текстом. Открытым он и должен ехать — его
защищает транспорт (HTTPS), — но храниться в базе в таком виде он не должен
никогда. А #[Hidden] не даёт хешу вернуться обратно в HTML или JSON.
Что Eloquent на самом деле отправляет в MariaDB
Array( [0] => Array ( [query] => select * from `users` where `email` like ? [bindings] => Array ( [0] => %example% ) [time] => 2.71 [readWriteType] => write ))
Разбор команды
Смотрим, какой SQL Eloquent на самом деле отправляет в MariaDB.
php artisan tinker --execute='DB::enableQueryLog(); App\Models\User::where("email", "like", "%example%")->get(); print_r(DB::getQueryLog());'| Токен | Зачем он и что меняет |
|---|---|
php artisan tinker --execute='...' | одна строка PHP внутри загруженного приложения; одинарные кавычки не дают shell раскрыть $ и подставить свои значения |
DB::enableQueryLog(); | включить журнал запросов текущего соединения — по умолчанию он выключен, и getQueryLog() вернул бы пустой массив |
App\Models\User::where("email", "like", "%example%") | условие: колонка, оператор LIKE, шаблон со знаком % (любое число любых символов) |
->get(); | выполнить запрос и получить коллекцию моделей |
print_r(DB::getQueryLog()); | напечатать журнал: массив записей с ключами query, bindings, time и readWriteType |
Что произойдёт
Один запрос: select * from users where email like ? со значением %example% в bindings; время выполнения — 2.71 ms.
На что смотреть
Значения внутри строки запроса нет: на его месте ?, а само значение едет рядом, в bindings. Это prepared statement — то же, что вы делали руками через prepare() и execute(). Обратные кавычки вокруг users и email — способ SQL отличать имена таблиц и колонок от значений и служебных слов.
Частые ошибки
- Журнал живёт только внутри текущего процесса: в веб-запросе его нужно включать заново.
- Параметром можно подставить только значение: имя колонки, имя таблицы и направление сортировки через ? не передаются, поэтому orderBy() со значением из адресной строки остаётся дырой — нужен белый список.
DB::enableQueryLog() включает журнал запросов, DB::getQueryLog() его
печатает. Один вызов User::where('email', 'like', '%example%')->get() дал
один запрос — select * from `users` where `email` like ? — и отдельно массив
bindings со значением %example%. Время выполнения — 2.71 ms.
Главное: значения внутри строки запроса нет. На его месте ?, а само значение
едет рядом, в bindings. Это prepared statement — ровно то, что вы делали
руками в lesson.db.04: шаблон запроса с маркерами, prepare() и execute()
с параметрами. Laravel не отменяет эту защиту, он забирает себе рутину.
Обратные кавычки вокруг users и email — тот самый «gravis» из lesson.db.04:
так SQL отличает имена таблиц и колонок от значений и от служебных слов.
Параметром можно подставить только значение. Имя колонки, имя таблицы и
направление сортировки (ASC/DESC) через ? не передаются. Именно поэтому в
lesson.sql-injection.i.01 инъекция жила в параметре sort, а защитой стал
белый список полей с проверкой через in_array (lesson.sql-injection.i.03).
В Laravel правило то же: orderBy() со значением, которое пришло прямо из
адресной строки, — дыра; orderBy() со значением, проверенным по белому списку,
— нет.
Убираем тестовую запись
осталось: 0Разбор команды
Убираем тестовую запись и заодно проверяем, сколько строк осталось в таблице.
php artisan tinker --execute='App\Models\User::where("email", "test@example.com")->delete(); echo "осталось: " . App\Models\User::count(), PHP_EOL;'| Токен | Зачем он и что меняет |
|---|---|
php artisan tinker --execute='...' | одна строка PHP внутри приложения и выход |
App\Models\User::where("email", "test@example.com") | условие: какую именно запись удалять |
->delete(); | DELETE FROM users WHERE email = ? для строк, подходящих под условие |
App\Models\User::count() | SELECT count(*): число строк в таблице |
echo "осталось: " . ..., PHP_EOL; | вывод результата; кириллица внутри строки PHP проходит без проблем — база в utf8mb4 |
Что произойдёт
Печатает «осталось: 0» — таблица снова пуста, дальше наполним её сидером.
На что смотреть
Запись с id=1 удалена, но следующая вставка получит id=2: AUTO_INCREMENT не возвращает освободившиеся номера — иначе удалённая запись и новая стали бы неразличимы в логах и ссылках.
Частые ошибки
delete()без условияwhere()удалил бы все строки таблицы.
where(...)->delete() — это DELETE FROM users WHERE email = ?, а count() —
SELECT count(*). Осталось 0: таблица снова пуста, дальше наполним её сидером.
Заодно заметьте деталь, которая объяснит следующий вывод: запись с id=1
удалена, но следующая вставка получит id=2. AUTO_INCREMENT не возвращает
освободившиеся номера — иначе удалённая запись и новая стали бы неразличимы в
логах, ссылках и внешних системах.
Сидер: данные для разработки одной командой
INFO Seeder [database/seeders/UsersSeeder.php] created successfully. Разбор команды
Для пагинации нужны данные: генерируем класс-сидер.
php artisan make:seeder UsersSeeder| Токен | Зачем он и что меняет |
|---|---|
php artisan | CLI Laravel |
make:seeder | генератор файла сидера в database/seeders |
UsersSeeder | имя класса: artisan создаёт файл UsersSeeder.php с заготовкой и методом run() |
Что произойдёт
Создан файл database/seeders/UsersSeeder.php: INFO Seeder [database/seeders/UsersSeeder.php] created successfully.
На что смотреть
run() — единственный метод, который вызывает db:seed; всё наполнение базы живёт в нём.
Частые ошибки
- Имя класса и имя файла должны совпадать:
db:seed --class=...ищет класс по имени.
Список из одной записи пагинацию не покажет, а набирать 12 пользователей руками долго и неповторимо: завтра вы пересоздадите базу и начнёте заново. Сидер — класс, который наполняет базу данными. Запустили команду — получили одинаковый набор на любой машине.
php artisan make:seeder UsersSeeder создал файл
database/seeders/UsersSeeder.php. Приём вам знаком: в lesson.db.04 вы
вставляли 42 ресторана циклом foreach по массиву из парсера. Сидер — тот же
цикл, только он лежит в проекте и вызывается командой db:seed.
UsersSeeder: 12 записей для практики
Разбор по строкам:
run()— единственный метод, который вызываетdb:seed; всё наполнение живёт в нём.$rows— массив из 12 массивов: ФИО, телефон, email. Данные учебные, все адреса в доменеexample.com.foreach ($rows as [$name, $phone, $email])— деструктуризация: три переменные достаются из массива прямо в заголовке цикла.User::create([...])— вставка через модель, а не строкой SQL. Значит, работают и белый список#[Fillable], и касты, и метки времени.'password' => 'secret'— пароль один на всех. В базе он станет bcrypt-хешем (каст'hashed'), но в настоящем проекте так не делают: у каждого пользователя свой пароль. Для учебной базы этого достаточно.
email в таблице уникальный (UNI в DESCRIBE users), поэтому второй запуск
сидера поверх существующих данных нарушит ограничение уникальности, и база
вставку отклонит. Начинают заново обычно так: migrate:fresh, а затем сидер.
Запускаем и смотрим, что попало в базу
INFO Seeding database. Database\Seeders\UsersSeeder ....................................... RUNNING Database\Seeders\UsersSeeder ................................. 3,973 ms DONE
Разбор команды
Запускаем написанный сидер и наполняем базу учебными записями.
php artisan db:seed --class=UsersSeeder| Токен | Зачем он и что меняет |
|---|---|
php artisan | CLI Laravel |
db:seed | команда наполнения базы; без параметров запускает общий DatabaseSeeder |
--class=UsersSeeder | какой именно сидер выполнить: без флага наш UsersSeeder не вызвался бы, потому что его нет в DatabaseSeeder |
Что произойдёт
Database\Seeders\UsersSeeder — RUNNING, затем DONE за 3,973 ms.
На что смотреть
Время съедают не вставки, а 12 вычислений bcrypt со стоимостью 12: хеш намеренно дорогой, иначе его было бы дёшево перебирать.
Частые ошибки
- Второй запуск сидера поверх существующих данных нарушит ограничение уникальности email (UNI в
DESCRIBE users) — база вставку отклонит. Начинают заново так:migrate:fresh, затем сидер.
id name phone email pass2 Иванов Иван Иванович +7 (912) 345-67-89 ivanov@example.com $2y$12$3 Петрова Анна Сергеевна +7 (903) 111-22-33 petrova@example.com $2y$12$4 Сидоров Пётр Алексеевич +7 (926) 555-44-33 sidorov@example.com $2y$12$5 Кузнецова Мария Дмитриевна +7 (916) 200-10-20 kuznetsova@example.com $2y$12$6 Смирнов Алексей Олегович +7 (905) 777-88-99 smirnov@example.com $2y$12$7 Васильева Ольга Игоревна +7 (921) 444-55-66 vasilieva@example.com $2y$12$
Разбор команды
Смотрим, что именно сидер положил в таблицу users.
mysql -u project_laravel -psecret project_laravel -e "SELECT id, name, phone, email, LEFT(password, 7) AS pass FROM users ORDER BY id LIMIT 6;"| Токен | Зачем он и что меняет |
|---|---|
mysql | консольный клиент MariaDB |
-u project_laravel | пользователь приложения |
-psecret | пароль слитно с флагом -p, без пробела |
project_laravel | база, которую открыть при подключении |
-e | флаг: выполнить SQL из следующего аргумента и выйти |
"..." | весь запрос — один аргумент в двойных кавычках: точка с запятой внутри разделяет части SQL, а не команды shell |
SELECT id, name, phone, email | колонки, которые нам интересны: ключ, ФИО, телефон и адрес |
LEFT(password, 7) | первые семь символов хеша: префикс алгоритма и стоимость — весь хеш в терминал не тащим намеренно |
AS pass | имя колонки в выводе для результата LEFT() |
FROM users | таблица, из которой читаем |
ORDER BY id | сортировка по первичному ключу: строки в порядке вставки, а не «как вернула база» |
LIMIT 6 | первые шесть строк из двенадцати |
Что произойдёт
Шесть строк: id от 2 до 7, кириллические ФИО, телефоны вида +7 (912) 345-67-89 (18 символов), адреса в домене example.com, pass у всех $2y$12$.
На что смотреть
id начинается с 2, а не с 1: единицу заняла удалённая тестовая запись, AUTO_INCREMENT её не вернул. Кириллица в name сохранилась как есть — работает utf8mb4, заданный при создании базы.
Частые ошибки
- Без
ORDER BYMariaDB возвращает строки в порядке, который ничем не гарантирован: сравнивать выводы двух запусков станет неудобно.
Сидер отработал за 3,973 ms — и это не вставка такая медленная. Время съедают 12 вычислений bcrypt со стоимостью 12: хеш намеренно дорогой, иначе его было бы дёшево перебирать.
Что видно в первых шести строках из двенадцати:
idначинается с 2, а не с 1: единицу заняла удалённая тестовая запись,AUTO_INCREMENTеё не вернул — ровно то, о чём мы говорили перед сидером;- кириллица в
nameсохранилась как есть: работаетutf8mb4, который мы задали при создании базы; - телефоны вида
+7 (912) 345-67-89— 18 символов, вvarchar(32)помещаются с запасом; LEFT(password, 7)у всех строк даёт$2y$12$— префикс алгоритма и стоимость. Полные хеши в вывод не тащим намеренно.
Контроллер читает из базы, а не из массива
От части 3 изменилась одна строка: вместо массива в коде — запрос к модели.
User::query()— начать построитель запросов. Можно писать сразуUser::orderBy(...), ноquery()делает намерение явным и ровно читается в цепочке.orderBy('name')—ORDER BY name asc. Без сортировки MariaDB возвращает строки в порядке, который ничем не гарантирован, и страницы пагинации будут перемешиваться.paginate(5)— вернуть не все строки, а страницу из пяти. Номер страницы пагинатор берёт сам из?page=в адресной строке. Вlesson.http.i.05вы делали это руками:$_GET['page'],offset = (page - 1) * 50, подстановка вLIMIT.view('users.index', ['users' => $users])— тот же шаблон и то же имя переменной, что в части 3. Шаблону всё равно, откуда приехали данные: из массива или из базы. Это и есть разделение слоёв MVC.
Шаблон: объект вместо массива
Каркас тот же, что в части 3: @extends('layouts.app'), @section('content'),
{{ }} с автоматическим экранированием. Изменилось то, что связано с
источником данных:
{{ $users->total() }}вместоcount($users):$usersтеперь объект-пагинатор, внутри него только пять строк текущей страницы, аtotal()знает обо всех записях;@forelse ($users as $user)перебирает страницу, а не всю таблицу; ветка@emptyрисует строку-заглушку, если записей нет;{{ $user->name }}— обращение к свойству модели. Под капотом это значение колонки из строки таблицы, но для шаблона выглядит как обычное свойство объекта из блока ООП;- ниже таблицы — блок
.pager, собранный из методов пагинатора.
Что мы спрашиваем у пагинатора
?page=2 до разметки .pagerСтили для .pager написаны в части 3 в public/css/app.css. Сама разметка
последнего узла собрана из шести методов пагинатора:
| Метод | Что возвращает | Где у нас используется |
|---|---|---|
total() |
сколько всего записей | (12) в заголовке |
currentPage() |
номер текущей страницы | 1 в подписи «1 / 3» |
lastPage() |
номер последней страницы | 3 в подписи «1 / 3» |
onFirstPage() |
true, если страница первая |
«← назад» без ссылки |
hasMorePages() |
true, если есть следующая |
«вперёд →» ссылкой |
previousPageUrl(), nextPageUrl() |
готовый URL с ?page= |
href ссылок |
Почему разметка пагинации своя
lastPage() — это ваш ceil($count / $perPage) из lesson.http.ii.05, только
считает его пагинатор сам: 12 записей по 5 на страницу → 3 страницы, последняя
неполная.
У пагинатора есть готовый метод links(), который рисует ссылки на все страницы
одной строкой. Мы его не используем: links() приносит собственную
HTML-разметку, рассчитанную на классы Tailwind CSS, а у нас свои стили и свой
класс .pager. Поэтому разметку «назад · 1 / 3 · вперёд» собираем сами из
методов таблицы: их поведение предсказуемо, а внешний вид полностью наш.
Одна страница = два запроса
select count(*) as `aggregate` from `users` | []select * from `users` order by `name` asc limit 5 offset 0 | []
Разбор команды
Смотрим, сколько запросов на самом деле делает одна страница пагинации.
php artisan tinker --execute='DB::enableQueryLog(); App\Models\User::query()->orderBy("name")->paginate(5); foreach (DB::getQueryLog() as $q) { echo $q["query"], " | ", json_encode($q["bindings"]), PHP_EOL; }'| Токен | Зачем он и что меняет |
|---|---|
php artisan tinker --execute='...' | одна строка PHP внутри загруженного приложения; одинарные кавычки не дают shell раскрыть $q |
DB::enableQueryLog(); | включить журнал запросов до выполнения кода — иначе он пуст |
App\Models\User::query() | начать построитель запросов к модели User |
->orderBy("name") | сортировка: в SQL станет order by name asc. Без неё MariaDB возвращает строки в негарантированном порядке и страницы пагинации будут перемешиваться |
->paginate(5); | вернуть страницу из пяти записей; номер страницы пагинатор берёт из ?page= в адресной строке (в консоли её нет, поэтому страница первая) |
foreach (DB::getQueryLog() as $q) | обход журнала: каждый элемент — массив с ключами query, bindings, time и readWriteType |
{ echo $q["query"], " | ", json_encode($q["bindings"]), PHP_EOL; } | тело цикла: напечатать строку SQL, разделитель и значения плейсхолдеров |
$q["query"] | строка запроса, которую Laravel отправил в базу |
json_encode($q["bindings"]) | превратить массив значений в читаемую строку: пустой массив станет [] |
Что произойдёт
Два запроса: select count(*) as aggregate from users и select * from users order by name asc limit 5 offset 0; bindings пусты в обоих случаях — [].
На что смотреть
Первый запрос даёт пагинатору общее число записей: из него lastPage() и наличие следующей страницы. Второй — сами строки: пять записей, начиная с нулевого смещения. Для ?page=2 второй запрос пойдёт с offset 5.
Частые ошибки
- orderBy() со значением, пришедшим прямо из адресной строки, — дыра: имя колонки и направление сортировки через плейсхолдер не передаются, нужен белый список полей.
Тот же приём с журналом запросов: paginate(5) выполнил ровно два запроса.
select count(*) as `aggregate` from `users`— сколько всего строк. Без этого числа пагинатор не знает ниlastPage(), ни есть ли следующая страница.select * from `users` order by `name` asc limit 5 offset 0— сами данные: пять строк, начиная с нулевого смещения.
bindings пусты в обоих случаях: ни сортировка, ни размер страницы не пришли от
пользователя, подставлять нечего. Для ?page=2 второй запрос пойдёт с
offset 5.
Именно эту пару вы собирали руками в lesson.http.ii.05: SELECT COUNT(...)
для числа страниц и LIMIT с OFFSET для среза. Laravel не придумал новый
способ — он спрятал арифметику внутрь объекта.
Проверяем через HTTP
<nav> <a href="/users">Список</a> <a href="/users/create">Добавить</a> </nav></header><main class="page"> <h1>Пользователи <span class="muted">(12)</span></h1> <table class="table"> <thead> <tr> <th>ФИО</th> <th>Телефон</th> <th>Email</th> </tr> </thead> <tbody> <tr> <td>Васильева Ольга Игоревна</td> <td>+7 (921) 444-55-66</td> <td>vasilieva@example.com</td> </tr> <tr> <td>Егорова Татьяна Максимовна</td> <td>+7 (913) 271-82-81</td> <td>egorova@example.com</td> </tr> <tr> <td>Иванов Иван Иванович</td>
Разбор команды
Проверяем страницу списка через HTTP — тот же путь, который проходит браузер: DNS зоны .rs, nginx, PHP-FPM, Laravel, MariaDB.
curl -sS http://project-laravel.rs/users | sed -n "12,40p"| Токен | Зачем он и что меняет |
|---|---|
curl | HTTP-клиент: делает запрос и печатает тело ответа в stdout |
-s | тихий режим: без прогресс-бара, который в пайпе только мешал бы |
-S | но показывать ошибки — флаг работает в паре с -s: без него curl промолчал бы даже при сбое соединения |
http://project-laravel.rs/users | URL: домен зоны .rs из dnsmasq и маршрут приложения; ресурс users при переименовании проекта не меняется |
| | пайп: тело ответа уходит следующей команде, а не в терминал |
sed | потоковый редактор: обрабатывает строки текста по заданному выражению |
-n | не печатать строки по умолчанию — выводим только то, что попросили явно |
"12,40p" | выражение: напечатать строки с 12-й по 40-ю. Кавычки защищают выражение от shell |
Что произойдёт
HTML страницы: навигация со ссылками «Список» и «Добавить», открывающий Пользователи (12)
, таблица с колонками ФИО, Телефон, Email и первые строки — Васильева, Егорова, Иванов.
На что смотреть
(12) в заголовке — total() пагинатора, результат COUNT(*). Порядок по алфавиту подтверждает orderBy('name') вместе с collation utf8mb4_unicode_ci: кириллица сортируется по алфавиту, а не по кодам байтов. Вывод обрезан выражением sed, поэтому третья запись попала в кадр не целиком.
Частые ошибки
-sSпечатает только тело ответа: по нему не отличить 200 от 404 или 500. Статус виден в заголовках (curl -i), а текст ошибки приложения — вstorage/logs/laravel.log.
<td>Морозова Дарья Кирилловна</td><td>+7 (906) 646-46-46</td><td>morozova@example.com</td><td>Николаев Павел Викторович</td><td>+7 (902) 314-15-92</td><td>nikolaev@example.com</td><td>Петрова Анна Сергеевна</td><td>+7 (903) 111-22-33</td><td>petrova@example.com</td>
Разбор команды
Проверяем вторую страницу пагинации и достаём из HTML только ячейки таблицы.
curl -sS "http://project-laravel.rs/users?page=2" | grep -o "<td>[^<]*</td>" | head -9| Токен | Зачем он и что меняет |
|---|---|
curl | HTTP-клиент |
-sS | тихий режим, но с выводом ошибок соединения |
"http://project-laravel.rs/users?page=2" | URL в двойных кавычках: ? — метасимвол подстановки имени файла, а & в URL с несколькими параметрами отправил бы команду в фон. Кавычки обязательны |
| | пайп: HTML уходит в grep |
grep | фильтрация строк по шаблону |
-o | печатать не всю строку, а только совпавший фрагмент: каждая ячейка окажется на своей строке |
"<td>[^<]*</td>" | регулярное выражение: открывающий тег td, затем [^<]* — ноль и более любых символов, кроме < (содержимое ячейки без вложенных тегов), затем закрывающий тег |
| | пайп: найденные ячейки уходят в head |
head -9 | первые девять строк вывода: три записи по три ячейки — ФИО, телефон, email |
Что произойдёт
Девять ячеек — три записи: Морозова Дарья Кирилловна / +7 (906) 646-46-46 / morozova@example.com, Николаев Павел Викторович / +7 (902) 314-15-92 / nikolaev@example.com, Петрова Анна Сергеевна / +7 (903) 111-22-33 / petrova@example.com.
На что смотреть
Это шестая, седьмая и восьмая фамилии по алфавиту — прямое продолжение первой страницы. Шаблон тот же, изменился только offset в запросе: для ?page=2 пагинатор берёт offset 5.
Частые ошибки
- [^<]* не найдёт ячейку с вложенными тегами: если внутри
появится ссылка или span, такая ячейка выпадет из вывода. Путь запроса curl— тот же, что у браузера (lesson.network.02)31листайтеcurlпроходит весь этот путь и возвращается ответом HTML — ровно как браузер.Что видно на первой странице:
<h1>Пользователи <span class="muted">(12)</span></h1>—total()вернул 12, это результатCOUNT(*);- навигация и открывающий
<main class="page">— из макетаlayouts/app.blade.php(часть 3); - строки идут по алфавиту: Васильева, Егорова, Иванов — работает
orderBy('name'), а collationutf8mb4_unicode_ciсортирует кириллицу по алфавиту, а не по кодам байтов; - сам вывод обрезан: команда
sed -n "12,40p"показала строки 12–40 HTML, поэтому третья запись попала в кадр не целиком.
Вторая страница —
?page=2, аgrep -o "<td>[^<]*</td>" | head -9достал девять ячеек, то есть три записи: Морозова, Николаев, Петрова. Это шестая, седьмая и восьмая фамилии по алфавиту — прямое продолжение первой страницы. Шаблон тот же, изменился толькоoffsetв запросе.32листайтеШаг 5 · Список из базыТо же самое в браузере
http://project-laravel.rs/users
12 записей, по 5 на страницу; пагинация своя, на классах .pager. http://project-laravel.rs/users?page=2
Тот же шаблон, другой LIMIT в запросе. Оба снимка — один маршрут и один шаблон, разница только в
?page=: 12 записей по 5 на страницу, то есть страница 1 из 3, а на второй — тот же шаблон и другойLIMITв запросе. Пагинация своя, на классах.pager. Ссылка «← назад» на первой странице не кликабельна:onFirstPage()вернулtrue, и вместо<a>нарисован<span>.33листайтеШаг 5 · Список из базыПриборная панель после переключения
student@mint: ~/recoursia/project-laravel782 мсstudent@mint:~/recoursia/project-laravel$MariaDB ........................................................... 10.11.14 Connection ........................................................... mysql Database ................................................... project_laravel Host ............................................................. 127.0.0.1 Port .................................................................. 3306 Username ................................................... project_laravel URL ........................................................................ Open Connections ........................................................... Tables ................................................................... 9 Total Size ....................................................... 304.00 KB Schema / Table ........................................................ Size project_laravel / cache ........................................... 32.00 KB project_laravel / cache_locks ..................................... 32.00 KB project_laravel / failed_jobs ..................................... 48.00 KB project_laravel / jobs ............................................ 32.00 KB project_laravel / job_batches ..................................... 16.00 KB project_laravel / migrations ...................................... 16.00 KB project_laravel / password_reset_tokens ........................... 16.00 KB project_laravel / sessions ........................................ 48.00 KB project_laravel / users ........................................... 64.00 KB
php artisan db:show после переключенияРазбор команды
Та же приборная панель, что в начале части, — сравниваем соединение до переключения и после.
php artisan db:showТокен Зачем он и что меняет php artisanCLI Laravel db:showпоказать текущее соединение по умолчанию: драйвер, версию СУБД, базу, хост, порт, пользователя, число таблиц и их размеры Что произойдёт
MariaDB 10.11.14, соединение
mysql, база project_laravel, хост 127.0.0.1, порт 3306, пользователь project_laravel; таблиц 9, всего 304.00 KB, из них users — 64.00 KB.На что смотреть
Вместо пустых Host, Port и Username появились значения: приложение действительно ходит по TCP. Таблиц по-прежнему 9, но теперь у них есть размеры — MariaDB их отдаёт, а SQLite нет.
Частые ошибки
- Если данные сохраняются, а вы их не видите (или наоборот),
db:showпоказывает соединение, которое используется на самом деле: нередко проект смотрит не в ту базу или всё ещё в SQLite-файл.
Сравните с началом части. Вместо
SQLite 3.45.1и пустыхHost/Port/Username—MariaDB 10.11.14,127.0.0.1,3306,project_laravel, базаproject_laravel. Таблиц по-прежнему 9, но появились размеры: всего 304.00 KB, из нихusers— 64.00 KB.Эта команда — первый ответ на вопрос «куда приложение вообще пишет?». Если данные сохраняются, а вы их не видите (или наоборот),
db:showпоказывает соединение, которое используется на самом деле. Нередко оказывается, что проект смотрит не в ту базу или всё ещё в SQLite-файл.34листайтеИтогЦепочка части 4: от
.envдо страницыСлой → файл → что передаётся дальше; пилюля над узлом — чем проверено Откат миграции
phoneпроверен отдельно:migrate:rollback --step=1и повторныйmigrate.В числах: 9 таблиц, 12 записей, 3 страницы по 5 записей (последняя неполная), 2 запроса на страницу, 60 символов в хеше пароля, 304 KB на всю базу.
35листайтеИтогПроверьте себя
- Почему приложение не может подключиться к MariaDB как
root, хотя в терминалеsudo mysqlпускает без пароля? - Чем
php artisan migrate:freshотличается отphp artisan migrate, и какая из этих команд уничтожает данные? - Колонку
phoneможно было добавить прямо в миграциюcreate_users_table. Почему мы вместо этого создали новый файл? - В
create([...])пароль передали как'secret', а в таблице лежит$2y$12$.... Кто и в какой момент превратил одно в другое? paginate(5)выполняет два запроса. Какие это запросы и зачем нужен второй?
36листайтеИтогОтветы
- Учётка
root@localhostаутентифицируется плагиномunix_socket: сервер проверяет, что соединение пришло через локальный сокет и что ОС-пользователь — root. Приложение соединяется по TCP127.0.0.1:3306, там проверять нечего —ERROR 1698. Нужен отдельный пользователь с паролем. migrateприменяет только те файлы, которых ещё нет в журналеmigrations;migrate:freshсначала удаляет все таблицы текущего соединения, а затем прогоняет миграции заново. Данные уничтожает вторая команда.- Применённая миграция уже записана в журнал: на вашей машине правка ничего не изменит, на чистой — изменит, и схемы разойдутся при одинаковом коде. История схемы неизменяема, как история коммитов.
- Каст
'password' => 'hashed'в методеcasts()модели: значение хешируется bcrypt при присваивании, доINSERT. Стоимость задаётBCRYPT_ROUNDS=12из.env. select count(*) as `aggregate` from `users`— общее число записей, из него пагинатор знаетlastPage()и наличие следующей страницы; иselect * from `users` order by `name` asc limit 5 offset 0— сами пять строк текущей страницы.
37листайтеЧто дальшеЧто дальше
База подключена, схема описана кодом, данные есть, список показывается страницами. Но приложение пока умеет только читать: добавить, поправить и удалить пользователя нельзя. В макете есть ссылка «Добавить» на
/users/create(её видно в HTML выше), а маршрута за ней пока нет.В части 5 — CRUD: добавить, изменить, удалить:
- ресурсные маршруты — одна строка вместо семи;
- формы добавления и правки,
@csrfи почему POST без токена отвечает 419; - валидация: правила для ФИО, телефона, email и пароля, русские сообщения об ошибках, проверка уникальности email с исключением текущей записи;
- что происходит с паролем при правке, если поле оставили пустым;
- удаление через форму, а не ссылкой, и почему так.
Всё нужное уже готово: модель
Userс белым списком полей, 12 записей в базе и шаблон списка, в который останется добавить кнопки действий.Потренируйтесь сейчасПройдите весь цикл сами: создайте миграцию, которая добавит в
usersколонкуcityтипаstring(64), примените её, посмотритеDESCRIBE users, откатите и примените снова — ровно то, что мы сделали сphone. Затем добавьте города вUsersSeederи пересоздайте базу.Обзор слайдов клик — перейти · O или Esc — закрытьРазбор команды