На прошлом уроке мы дошли до запуска проекта со стандартными таблицами и данными, теперь давайте разберёмся, откуда они взялись.
В директории core/db
есть 2 поддиректории: migrations
и seeds
. В первой, понятно, миграции, а во второй скрипты для "засеивания" начальных данных. Для работы используется Phinx, конфигурация которого хранится в core/phinx.php
- там нам ничего менять не нужно, все настройки берутся из настроек окружения .env
.
Миграциями называют изменение структуры базы данных без потери её консистентности. Каждая миграция может быть отменена, при этом она возвращает структуру БД к предыдущему виду. На продакшене, конечно же, никто миграции не откатывает - это нужно только на время разработки.
Для запуска миграций в Vesp мы используем команду composer db:migrate
, для отката composer db:rollback
.
Теперь давайте посмотрим на сами файлы миграций.
Название файла состоит из отметки времени и произвольного имени, которое будет использоваться для работы. Временной отпечаток сортирует миграции в порядке их создания, чтобы они всегда выполнялись последовательно.
Vesp уже содержит 2 миграции: таблицы файлов и пользователей.
<?php
use Illuminate\Database\Schema\Blueprint;
use Vesp\Services\Migration;
// Класс должен расширять Vesp\Services\Migration
class Files extends Migration
{
// Функция внесения изменений
public function up(): void
{
// Создаём таблицу для файлов
$this->schema->create(
'files',
function (Blueprint $table) {
$table->id(); // Первичный ключ Unsigned Big Integer с автоинкрементом
// Дальше колонки Varchar
$table->string('file');
$table->string('path');
// Следующие 2 колонки могут быть null при создании записи
// То есть, они не являются обязательными
$table->string('title')->nullable();
$table->string('type')->nullable();
$table->smallInteger('width')->unsigned()->nullable();
$table->smallInteger('height')->unsigned()->nullable();
$table->integer('size')->unsigned()->nullable();
// колонка с типом JSON появилась только в MySQL 5.7
$table->json('metadata')->nullable();
// Специальная команда Eloquent, которая добавляет 2 колонки:
// created_at и updated_at для сохранения времени создания и
// последнего изменения записи
$table->timestamps();
}
);
}
// Функция отката изменений
public function down(): void
{
// Просто удаляем таблицу
$this->schema->drop('files');
}
}
Вам не нужно запоминать типы колонок, здесь и далее везде прекрасно работает автодополнение IDE.
Но крайне желательно знать типы данных MySQL, все они перечислены в документации.
Итак, при запуске composer db:migrate
Phinx смотрит на имеющиес миграции и выполняет новые. Как он знает, какие миграции уже были выполнены? Очень просто - он сохраняет их имена в таблице app_migrations
.
Теперь давайте посмотрим на вторую миграцию, она создаёт сразу 3 таблицы, и связывает их друг с другом.
<?php
use Illuminate\Database\Schema\Blueprint;
use Vesp\Services\Migration;
class Users extends Migration
{
public function up(): void
{
// Первым делом создаём таблицу для хранения групп пользователей
$this->schema->create(
'user_roles',
function (Blueprint $table) {
// Первичный ключ
$table->id();
// Название группы должно быть уникальным
$table->string('title')->unique();
// Права доступа группы могут быть null
$table->json('scope')->nullable();
// Стандартные колонки с датами
$table->timestamps();
}
);
// Теперь таблица пользователей
$this->schema->create(
'users',
function (Blueprint $table) {
$table->id();
// Уникальное имя пользователя
$table->string('username')->unique();
// Обязательный пароль (мы его захэшируем, конечно)
$table->string('password');
// Следующие 2 колонки не обязательны
$table->string('fullname')->nullable();
$table->string('email')->nullable();
// Связь с таблицей группы пользователей (расскажу ниже)
$table->foreignId('role_id')
->constrained('user_roles')->onDelete('cascade');
// Статус записи индексируется отдельно
$table->boolean('active')->default(true)->index();
$table->timestamps();
}
);
// Таблица "сессий" пользователя
$this->schema->create(
'user_tokens',
function (Blueprint $table) {
// Здесь первичным ключом является строчный токен
$table->string('token')->primary();
// Свзяь с таблицей пользователей
$table->foreignId('user_id')
->constrained('users')->onDelete('cascade');
// Срок годности токена
$table->timestamp('valid_till')->index();
// Ip адрес пользователя
$table->string('ip', 16)->nullable();
// Статус записи
$table->boolean('active')->default(true);
$table->timestamps();
// Комбинированный индекс для быстрого поиска
// активных токенов пользователя
$table->index(['token', 'user_id', 'active']);
}
);
}
// Функция отката изменений
public function down(): void
{
// Просто удаляем все 3 таблицы в обратном порядке
$this->schema->drop('user_tokens');
$this->schema->drop('users');
$this->schema->drop('user_roles');
}
}
В этой миграции мы впервые видим связь таблиц между собой. Разберём на примере.
В таблице user_roles
объявлен первичный ключ через $table->id()
- на уровне MySQL это колонка id:
Такой вид колонки id
- самый распространённый для идентификации конкретной записи. В дальнейшем по ней можно искать записи, обновлять и удалять.
Логично, что если мы хотим связать какую-то другу таблицу с этой - мы будем привязывать её именно к id
. Eloquent предлагает для этого очень удобный синтаксис:
$table->foreignId('role_id')
->constrained('user_roles')
->onUpdate('restrict')
->onDelete('cascade');
foreignId
создаёт колонку role_id
(имя произвольное, но Eloquent очень просит, чтобы заканчивалось на _id
) ровно того же типа UNSIGNED BIGINT, а constrained
указывает на связь с таблицей user_roles
. Эта запись буквально говорит, что role_id
обязательно содержит id
из таблицы user_roles
.
PhpMyAdmin даже делает значения таких колонок ссылками со всплывающими подсказками о родительском значении:
Теперь вы просто не сможете сохранить в БД пользователя с неправильной группой - MySQL выдаст ошибку.
Командами onUpdate
и onDelete
мы указываем, что делать с юзерами при изменении родительской записи из группы: restrict
означает не делать ничего, а cascade
- делать тоже самое. В нашем случае указано, что при удалении записи из user_roles
, будут удалены все пользователи этой группы. Автоматически, на уровне БД.
Таким образом, связав эти 2 таблицы, мы точно знаем, что пользователей без группы быть не может - MySQL не позволит сохранить такую запись, а при удалении группы удалит всех её пользователей.
Более подробно про связи можно почитать документацию MySQL.
Seed - это семя или семечко по-английски. В терминологии миграций это скрипты, которые вносят какие-то данные в таблицы, чтобы не делать этого вручную. Запускаются командой composer db:seed
.
В нашем случае сиды создают начальные группы и пользователей. Для этого выполняются 2 скрипта: core/db/seeds/UserRoles.php
и core/db/seeds/Users.php
.
Вот создание групп пользователей:
<?php
use Phinx\Seed\AbstractSeed;
use App\Models\UserRole;
class UserRoles extends AbstractSeed
{
public function run(): void
{
// Просто массив с группами
$roles = [
'Administrator' => [
// И их правами
'scope' => ['profile', 'users'],
],
'User' => [
'scope' => ['profile'],
],
];
// Дальше проходим по массиву
foreach ($roles as $title => $data) {
// Если группы с таким нзавнием нет
if (!$group = UserRole::query()->where('title', $title)->first()) {
// То создаём
$group = new UserRole(['title' => $title]);
}
// И обновляем ей свойства
$group->fill($data);
$group->save();
}
}
}
Сиды используют модели Eloquent, которые мы будем рассматривать на следующем уроке. Но, как вы понимаете, в этих скриптах можно делать что угодно с данными, не только добавлять, но и удалять или менять.
Создание стандартных пользователей отличается только наличием еще одного метода:
public function getDependencies(): array
{
return ['UserRoles'];
}
Это означает, что пользователи должны создаваться после создания их групп. Иначе мы бы получили ошибку от MySQL, помните?
Ну вот, а так мы указываем, что скрипт Users требует обязательного выполнения UserRoles перед ним.
Мы рассмотрели базовые миграции и сиды Vesp. Нужно учесть, что они не выполняются сразу после создания пакета, вам нужно запускать их вручную.
Так сделано потому, что Vesp не требует от вас пользоватся именно этой структурой таблиц. Вы можете изменить пользователей, добавить им полное имя, дату рождения, номер телефона и только после этого создавать таблицы.
Более того, структура таблиц не может быть неизменнной по ходу разработки проекта, так что мы напишем и запустим еще несколько миграций.
На следующем уроке будет изучать модели Eloquent для работы с нашими таблицами.
Классно, особенно про
_id
придумано. Про типы json тоже узнал впервые, нужно будет разобраться в их преимуществах.Eloquent вообще толковый, после него xPDO невозможно использовать.
Василий, мы изучаем Eloquent 8.x версию?
Да, верно, в зависимостях у
vesp-core
прописан Eloquent 8.Заметил что в только что установленной Весте в миграции отсутствует ->onUpdate('restrict') Я так понимаю это самому необходимо дополнить?
Не надо, оно по умолчанию так - я просто чуть более подробно написал.
Насколько я понял, таблицы в БД создаются на основе файлов миграции. При этом в файле миграции (users) у первичного ключа (как и у любых других данных) не указываются никакие доп. атрибутов. Значит Eloquent (или Phinx отвечающий за миграции) понимает, что по умолчанию запись первичного ключа $table->id(); должна сопровождаться атрибутами: BIGINT, UNSIGNED и AUTO_INCREMENT?
Да, верно.
Если пройти по цепочке методов
Illuminate\Database\Schema::Blueprint
, начиная сid()
, то увидишь вот это:public function id($column = 'id') { return $this->bigIncrements($column); } public function bigIncrements($column) { return $this->unsignedBigInteger($column, true); } public function unsignedBigInteger($column, $autoIncrement = false) { return $this->bigInteger($column, $autoIncrement, true); } public function bigInteger($column, $autoIncrement = false, $unsigned = false) { return $this->addColumn('bigInteger', $column, compact('autoIncrement', 'unsigned')); }
Последний метод и добавляет колонку в схему с нужными параметрами.
Спасибо, разбираюсь