Новая структура таблиц магазина
Мы стартуем с момента окончания прошлого курса, поэтому я предлагаю вам скачать архив с кодом после его завершения.
Внутри есть директория 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.
Итоговый коммит с текущими изменениями.
1
👍
👎
❤️
🔥
😮
😢
😀
😡
415
14.08.2023 11:04:03
15 комментариев
NightRider
Странно... секцию 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 говорит
Василий, спасибо!
Извини, тупанул.
bezumkin.ru
Личный сайт Василия Наумкина
Прямой эфир
Дмитрий
08.02.2025 09:09:01
Спасибо за ответ. Есть желание разобраться самому. Прочитал все ваши статьи и понял, что VESP перспе...
Василий Наумкин
04.02.2025 19:27:08
Я таким давно не занимаюсь и с MODX не работаю.
Попробуйте обратиться к ребятам с modx.pro.
Василий Наумкин
23.12.2024 05:33:00
В MODX сначала создали проблему, автоматически генерируя адреса, а потом "решили" заморозкой.
Так ч...
Дмитрий
14.12.2024 09:10:38
Василий, прошу прощения, тупанул, не разобрался сразу. Фреймворк отличный! "Чистый лист" на vue, рис...
Василий Наумкин
05.12.2024 20:01:14
В итоге основная ошибка была в неправильном общем root в Nginx, из-за чего запросы не улетали на фай...
Василий Наумкин
01.07.2024 11:56:41
Да, верно, именно так.
А в контроллере, скорее всего, ловить данные методом post.
Василий Наумкин
26.06.2024 09:38:15
О, точно, вылезает если не залогинен.
Спасибо, исправил!
Уровни подписки
Спасибо!
500 ₽ в месяц
Эта подписка ничего не даёт, просто возможность сказать спасибо за мои заметки. Подписчики отмечаются зелёненьким цветом в комментариях.
Большое спасибо!
1 000 ₽ в месяц
И эта подписка не даёт ничего, кроме оранжевого цвета в комментариях и возможности сказать спасибо, но уже большое!