Мы стартуем с момента окончания прошлого курса, поэтому я предлагаю вам скачать архив с кодом после его завершения.
Внутри есть директория docker
, там нужно запустить docker-compose
и можно работать. Более подробно про работу с Docker можно прочитать в отдельной заметке, там много комментариев с решением типичных вопросов.
Если же у вас остались данные от первого курса, то предлагаю обнулить вашу базу и пересоздать всё заново. Для этого можно или откатить все миграции, или просто вручную грохнуть БД. Проще всего удалить docker/db
и перезапустить контейнер, он создаст новую базу.
Также я еще обновил php и node зависимости, и внёс вот это в core/phinx.php
:
'templates' => [
'style' => 'up_down',
],
Теперь новые миграции сразу содержат методы up()
и down()
, вместо change()
.
Из старых миграций оставляем только 20210322072616_files.php
и 20210322072635_users.php
, остальные удаляем.
Заходим в контейнер php-fpm
, переходим в терминал и создаём новую миграцию composer db:create Languages
Дальше меняем даты 3х файлов миграций таким образом, чтобы они выстроились в следующем порядке: files
, languages
, users
.
Миграция таблицы Languages
очень простая, но к ней мы будем привязывать остальные таблицы, в которых будет ссылка на язык. Именно поэтому она должна быть выполнена перед users.
public function up(): void
{
$this->schema->create(
'languages',
function (Blueprint $table) {
$table->char('lang', 2)->primary();
$table->string('title')->unique();
$table->unsignedInteger('rank')->nullable()->index();
$table->boolean('active')->default(true)->index();
}
);
}
public function down(): void
{
$this->schema->drop('languages');
}
В миграции Users оставляем создание групп и токенов и добавляем множество новых колонок пользователям:
$this->schema->create(
'users',
function (Blueprint $table) {
$table->id();
$table->string('username')->unique();
$table->string('fullname')->nullable();
$table->string('password');
// Колонка для сброса пароля
$table->string('tmp_password')->nullable();
// Соль хэширования для старых юзеров MODX
$table->string('salt')->nullable();
$table->string('email')->nullable();
$table->string('phone')->nullable();
// Пол юзера, компания, адрес и т.д.
$table->tinyInteger('gender')->nullable();
$table->string('company')->nullable();
$table->string('address')->nullable();
$table->string('country')->nullable();
$table->string('city')->nullable();
$table->string('zip')->nullable();
// А вот и колонка с языком пользователя
$table->string('lang', 2)->nullable();
// Связь с группой
$table->foreignId('role_id')
->constrained('user_roles')->cascadeOnDelete();
// Связь с файлом картинки-аватарки
$table->foreignId('file_id')->nullable()
->constrained('files')->nullOnDelete();
// Статусы юзера: активирован, заблокирован
$table->boolean('active')->default(true);
$table->boolean('blocked')->default(false);
// Комментарий про юзера
$table->text('comment')->nullable();
// Связь этой записи с таблицей MODX
$table->unsignedInteger('remote_id')->nullable()->index();
$table->timestamps();
// Составной интекс на статус юзера
$table->index(['active', 'blocked']);
// Связь с таблицей языков
$table->foreign('lang')
->references('lang')
->on('languages')
->nullOnDelete();
}
);
Обратите на связь users
с languages
внешним ключом.
В Eloquent нет функции для удобного создания таких связей по строкам, он умеет их делать только по id
(метод foreignId()
) или uuid
(соответственно foreignUuid()
), поэтому здесь мы создаём ключик вручную.
Именно из-за этой связи на момент создания таблицы юзеров мы уже должны создать таблицу languages
.
Саму модель User
мы будем обновлять позже, вместе с переносом юзеров из MODX.
Магазин у нас мультиязычный, таблица с языками уже прудусмотрена, осталось только прикрутить к ней категории и товары.
Мне не нравится практика копирования товаров в MODX по разным контекстам, это глупо и неэффективно, но ничего не поделать - там так принято. В VespShop мы это легко исправим.
Для этого вынесем текстовые колонки в отдельные таблицы и будем их подтягивать при выводе пользователю. Это может показаться немного сложным, но на самом деле, довольно удобно - особенно если правильно прописать все связи на уровне бд.
Создаём новую миграцию для категорий composer db:create Categories
:
public function up(): void
{
$this->schema->create(
'categories',
static function (Blueprint $table) {
$table->id();
// Связь с таблицой файлов - категории могут иметь картинку
$table->foreignId('file_id')->nullable()
->constrained('files')->nullOnDelete();
// Псевдоним и адрес категории
$table->string('alias');
$table->string('uri')->unique();
// Статус и сортировка
$table->boolean('active')->default(true);
$table->unsignedInteger('rank')->default(0);
// Связь с таблицей MODX
$table->unsignedInteger('remote_id')->nullable()->index();
$table->timestamps();
$table->index(['uri', 'active']);
}
);
// А вот здесь мы сразу же меняем только что созданную таблицу, чтобы
// добавить внешний ключ на саму себя!
// Категории могут быть вложены друг в друга
$this->schema->table(
'categories',
static function (Blueprint $table) {
// Добавляем новую колонку с указанием в таблицу
// с указанием внешнего ключа на саму себя
$table->foreignId('parent_id')->nullable()->after('id')
->constrained('categories')->nullOnDelete();
$table->index(['parent_id', 'rank']);
}
);
// И, наконец, таблица переводов категорий
$this->schema->create(
'category_translations',
static function (Blueprint $table) {
// Связь с категорией
$table->foreignId('category_id')
->constrained('categories')->cascadeOnDelete();
// Связь с языком
$table->char('lang', 2);
// Всего 2 текстовые колонки
$table->string('title');
$table->text('description')->nullable();
// Индексы и ключи
$table->primary(['category_id', 'lang']);
$table->unique(['lang', 'title']);
$table->foreign('lang')
->references('lang')
->on('languages')
->cascadeOnDelete();
}
);
}
Как вам, а?
В отличие от того же MODX, мы везде прописываем внешние ключи, поэтому:
nullOnDelete()
.Здесь всё примерно так же, как и у категорий. Делаем composer db:create Products
:
public function up(): void
{
$this->schema->create(
'products',
static function (Blueprint $table) {
$table->id();
// Связь с категорией
// При удалении категории будут удалены и дочерние товары
$table->foreignId('category_id')
->constrained('categories')->cascadeOnDelete();
// Псевдоним и полный адрес, включая путь категории
$table->string('alias');
$table->string('uri')->unique();
// Свойства товара
$table->decimal('price')->default(0);
$table->decimal('old_price')->default(0);
$table->string('article')->nullable();
$table->decimal('weight')->nullable();
$table->boolean('new')->default(false)->index();
$table->boolean('popular')->default(false)->index();
$table->boolean('favorite')->default(false)->index();
$table->string('made_in')->nullable();
$table->json('colors')->nullable();
$table->json('variants')->nullable();
// Статус и сортировка
$table->boolean('active')->default(true);
$table->unsignedInteger('rank')->default(0);
// Связь с таблицей MODX
$table->unsignedInteger('remote_id')->nullable()->index();
$table->timestamps();
$table->index(['uri', 'active']);
$table->index(['category_id', 'rank']);
}
);
// Таблица переводов товаров
$this->schema->create(
'product_translations',
static function (Blueprint $table) {
$table->foreignId('product_id')
->constrained('products')->cascadeOnDelete();
$table->char('lang', 2);
// Здесь у нас 4 текстовых колонки
$table->string('title')->nullable();
$table->string('subtitle')->nullable();
$table->text('description')->nullable();
$table->text('content')->nullable();
$table->primary(['product_id', 'lang']);
}
);
}
Таблица связи товаров с категориями, так известная как "мультикатегории":
public function up(): void
{
$this->schema->create(
'product_categories',
static function (Blueprint $table) {
$table->foreignId('product_id')
->constrained('products')->cascadeOnDelete();
$table->foreignId('category_id')
->constrained('categories')->cascadeOnDelete();
$table->unsignedInteger('rank')->default(0);
$table->primary(['product_id', 'category_id']);
}
);
}
Насколько я помню, изначально miniShop2 разрабатывался для реализации конкретного проекта и там этот функционал был востребован. Но лично мне идея включать один товар в разные категории кажется неправильной.
Ведь, чтобы на сайте не было дубликатов, у каждого товара всё равно один уникальный url
, и, соответственно, одна главная категория.
Поэтому включать эту таблицу в новый VespShop мне не хочется, но раз уж она есть в исходном miniShop2... Да и в табачном магазине, который я переносил, она тоже использовалась.
В общем, пусть будет и здесь.
Дальше таблица связи товаров с файлами, то есть галерея товара:
public function up(): void
{
$this->schema->create(
'product_files',
static function (Blueprint $table) {
$table->foreignId('product_id')
->constrained('products')->cascadeOnDelete();
$table->foreignId('file_id')
->constrained('files')->cascadeOnDelete();
$table->boolean('active')->default(true)->index();
$table->unsignedInteger('rank')->default(0)->index();
// Связь с файлом в MODX
$table->unsignedInteger('remote_id')->nullable()->index();
$table->primary(['product_id', 'file_id']);
}
);
}
И, наконец, связи товаров между собой.
Из опыта работы с miniShop2 я вынес, что разные типы связи товаров больше путают, нежели помагают, поэтому здесь у нас простая связь один к одному:
public function up(): void
{
$this->schema->create(
'product_links',
static function (Blueprint $table) {
$table->foreignId('product_id')
->constrained('products')->cascadeOnDelete();
$table->foreignId('link_id')
->constrained('products')->cascadeOnDelete();
$table->foreignId('rank')->default(0)->index();
$table->primary(['product_id', 'link_id']);
}
);
}
По этой таблице мы можем выбирать как дочерние товары по отношению к product_id
, так и родительские по отношению к link_id
.
Если вам нужно больше - вы легко можете добавить еще и колонку с какием-то идентификатором связи рядом, главное, не забудьте включить её в primary key
.
Запускаем наши новые миграции и проверяем как они откатываются, ошибок быть не должно.
В итоге, вот мои миграции и таблицы:
А вот и схема всех связей между таблицами:
Можете её интерактивно понажимать вот здесь - https://drawsql.app/teams/bezumkins-team/diagrams/vesp-shop
Дальше мы будем обновлять старые модели и создавать новые, для работы с этими таблицами. А проверять их работу будем через импорт старых данных из miniShop2.
Итоговый коммит с текущими изменениями.
Странно... секцию templates в файл core/phinx.php добавил, но новые миграции по умолчанию всё равно содержат метод change, вместо up и down
Надо еще обновить зависимости composer, чтобы скачался
vesp/core
2.9.0, а вместе с ним иphinx
13.4, который поддерживает эту настройку.Василий, вопрос от "особо одаренного".) Предыдущий курс выполнял прямо на сервере, а сейчас в Докере. Т.е. можно сказать, что делаю "с нуля". Как понимаю в этом случае мне не нужно обнулять или сносить БД?
И второй вопрос - поскольку тестовый магазин грузится с кучей товаров и категорий - наверное не стоит после его первичного запуска в Докере выполнять composer db:migrate, composer db:seed? Наверное нужно дальше, как написано в этом уроке, сначала удалить старые миграции (кроме 20210322072616_files.php и 20210322072635_users.php), создать новые (languages, categories, products, product_categories, product_files, product_links) и только после всего этого выполнить миграцию?
Да, сносить просто нечего.
В заметке речь о том, что я беру старые миграции от предыдущего курса и работаю с ними. Думал, так будет наглядее.
Итоговый результат всегда можно посмотреть по ссылке в конце заметки. Сама заметка - просто развёрнутое объяснение для него.
Странно. Сначала я по итогам урока выполнил composer db:migrate и composer db:rollback-all . И все выполнилось без ошибок. А вот когда снова попробовал выполнить миграции - посыпались ошибки.
Затем я опять скачал архив, запустил все заново и снова выполнил урок и теперь ошибки пошли на этапе composer db:rollback-all . В коде вряд ли мог ошибиться, все внимательно смотрел по коммиту.
Что-то у тебя там не то.
Я зашёл в релизы и скачал архив с итоговым кодом этого урока
Зашёл в директорию docker, скопировал
.env.dist
в.env
, поправил параметрCOMPOSE_PROJECT_NAME
, и запустил контейнерыPHP установил зависимости при запуске
Дальше проверил
migrate
иrollback-all
Ошибок нет. Так что, попробуй удалить БД и накатить миграции заново из репозитория, там всё в порядке, я проверил.
Т. е. имеешь в виду что нужно обновить зависимости - compooser update ?
А как это правильно делать в Докере? Удалить контейнер mariadb-1 ?
Нет, просто показываю, что но всё рабаотает само. Без ошибок.
Остановить контейнеры. Удалить директорию
docker/mariadb
и запустить контейнеры заново.Я скачал с итоговым кодом урока. Выполнил composer db:migrate, composer db:rollback-all затем опять composer db:migrate. Все проходит без ошибок как у тебя. Дальше я попробовал засеять базу (чтобы получить юзеров и войти в админку) и тут получил ошибки. Но как понимаю это потому, что я поторопился и на этом этапе еще не предусмотрено засевание базы?
В этом уроке такого и не предлагается. Тут изменены таблицы, а сиды - не изменены. Логично, что они не будут работать.
Проходи в следующую заметку.
Спасибо за помощь! На всякий случай уточню один момент - может кто еще столкнется. При "физическом" удалении директории с БД (
docker/mariadb
) - контейнеры у меня потом не запускаются. Выдается ошибка. Может это особенности работы Докера в WSL Windows. Опытным путем пробовал удалить контейнерmariadb-1
в самом Докере - при последующем запуске все восстанавливается.Странно, ведь этой директории нет при первом запуске, когда проект только скачан из репозитория. Как-то же она создаётся без ошибки.
Может и правда особенность WSL.
Василий, добрый день!
У меня стояла какая-то старая версия Докера и Весп-шоп установился, сейчас поставил версию Docker Engine 24.0.5 как у тебя и сейчас не получается поставить:
Получается, что в docker-compose.yml перестали работать переменные. Подскажи, что можно сделать, может дело в версии Docker Compose?
Файл
.env
у тебя вообще есть? Ты переименовал его из.env.dist
? Все переменные - там.У меня
docker-compose -v
говоритВасилий, спасибо!
Извини, тупанул.