Skip to content
  1. Компоненты
  2. MiniShop3
  3. Разработка
  4. Модели и схема БД

Модели и схема базы данных ​

Модели MiniShop3 — классы xPDO в namespace MiniShop3\Model.

Основные классы ​

php
use MiniShop3\Model\msProduct;
use MiniShop3\Model\msProductData;
use MiniShop3\Model\msOrder;
use MiniShop3\Model\msCustomer;

// Получение товара
$product = $modx->getObject(msProduct::class, $id);

// Получение данных товара
$data = $product->getOne('Data');

// Получение заказа с адресом
$order = $modx->getObject(msOrder::class, $id);
$address = $order->getOne('Address');

// Получение покупателя
$customer = $modx->getObject(msCustomer::class, ['email' => $email]);

Связи между моделями ​

getOne() — для 1:1 и N:1, getMany() — для 1:N.

МодельАлиасТипВозвращает
msProductData1:1данные товара (msProductData)
msProductOptions1:Nзначения опций
msProductCategories1:Nдополнительные категории (msCategoryMember), composite
msProductCategoryN:1родительскую категорию (msCategory)
msProductDataCategories1:Nдополнительные категории того же товара
msProductDataFiles1:Nфайлы галереи
msOrderAddress1:1адрес заказа (msOrderAddress)
msOrderProducts1:Nтовары в заказе
msOrderCustomerN:1покупателя (msCustomer)
msCustomerTokens1:Nтокены авторизации
msCustomerCustomerGroupN:1группу покупателя
msCustomerAddressCustomerN:1покупателя — обратная связь от адреса
msCustomerTokenCustomerN:1покупателя — обратная связь от токена

Связи Addresses у msCustomer нет. Адреса читают прямым запросом к msCustomerAddress по customer_id. См. Работа с покупателем.

Файл схемы ​

XML-схема описывает модели для генератора xPDO:

core/components/minishop3/schema/minishop3.mysql.schema.xml
АтрибутЗначение
packageMiniShop3\Model
baseClassxPDO\Om\xPDOObject
platformmysql
version3.0

Таблицы создаёт не XML

Структуру таблиц задают карты моделей в src/Model/mysql/ и миграции Phinx. XML нужен только генератору моделей, в установке он не участвует и местами уже разошёлся с картой — например по уникальности индекса на фамилию покупателя (разбор). Сверяйте поля по карте модели.

Таблицы базы данных ​

Товары ​

МодельТаблицаОписание
msProductsite_contentТовар (расширяет modResource)
msProductDatams3_productsДанные товара (цена, артикул, вес)
msProductFilems3_product_filesФайлы галереи товара
msProductOptionms3_product_optionsЗначения опций товара
msLinkms3_linksТипы связей товаров
msProductLinkms3_product_linksСвязи между товарами

msProductData — основные поля ​

ПолеТипОписание
idintID (совпадает с ID ресурса)
articlevarchar(50)Артикул
pricedecimal(12,2)Цена
old_pricedecimal(12,2)Старая цена
stockdecimal(13,3)Остаток на складе
weightdecimal(13,3)Вес
imagevarchar(255)Основное изображение
thumbvarchar(255)Превью изображения
preview_file_idintID файла галереи для главного превью (без смены порядка галереи)
vendor_idintID производителя
made_invarchar(100)Страна производства
newtinyint(1)Флаг «Новинка»
populartinyint(1)Флаг «Популярный»
favoritetinyint(1)Флаг «Избранное»
tagsjsonТеги
colorjsonЦвета
sizejsonРазмеры

Категории ​

МодельТаблицаОписание
msCategorysite_contentКатегория (расширяет modResource)
msCategoryMemberms3_product_categoriesСвязь товар-категория
msCategoryOptionms3_category_optionsОпции категории

Заказы ​

МодельТаблицаОписание
msOrderms3_ordersЗаказ
msOrderAddressms3_order_addressesАдрес доставки заказа
msOrderProductms3_order_productsТовары в заказе
msOrderLogms3_order_logsИстория изменений заказа
msOrderStatusms3_order_statusesСтатусы заказов

msOrder — основные поля ​

ПолеТипОписание
idintID заказа
user_idintID пользователя MODX
customer_idintID покупателя (msCustomer)
tokenvarchar(128)Токен сессии
uuidchar(36)UUID заказа
numvarchar(20)Номер заказа
costdecimal(12,2)Общая стоимость
cart_costdecimal(12,2)Стоимость товаров
delivery_costdecimal(12,2)Стоимость доставки
weightdecimal(13,3)Общий вес
status_idintID статуса
delivery_idintID способа доставки
payment_idintID способа оплаты
contextvarchar(100)Контекст MODX
order_commenttextКомментарий к заказу
idempotency_keyvarchar(128)Ключ идемпотентности (программные заказы)
createdondatetimeДата создания
updatedondatetimeДата обновления

Покупатели ​

МодельТаблицаОписание
msCustomerms3_customersПокупатель магазина
msCustomerGroupms3_customer_groupsГруппы покупателей (сегментация, ACL каталога)
msCustomerAddressms3_customer_addressesСохранённые адреса покупателя
msCustomerTokenms3_customer_tokensТокены авторизации

msCustomerGroup — основные поля ​

ПолеТипОписание
idintID группы
namevarchar(255)Название группы
user_group_idintID группы пользователей MODX (принципал для modAccessResourceGroup)
activetinyint(1)Активна
created_attimestampДата создания
updated_attimestampДата обновления

msCustomer — основные поля ​

ПолеТипОписание
idintID покупателя
user_idintID связанного modUser (опционально)
first_namevarchar(191)Имя
last_namevarchar(191)Фамилия. В XML-схеме индекс по этому полю помечен уникальным, но таблицу создаёт миграция из карты модели, где уникальности нет, — в базе индекс обычный (разбор)
phonevarchar(50)Телефон
emailvarchar(191)Email
passwordvarchar(255)Хеш пароля
is_activetinyint(1)Активен
is_blockedtinyint(1)Заблокирован
email_verified_atdatetimeДата подтверждения email
orders_countintКоличество заказов
total_spentdecimal(12,2)Сумма всех заказов
last_order_atdatetimeДата последнего заказа
privacy_accepted_atdatetimeДата принятия политики

Отличие от miniShop2

В miniShop2 покупатель — это modUser. В MiniShop3 покупатель — отдельная сущность msCustomer, связь с modUser необязательна.

Доставка и оплата ​

МодельТаблицаОписание
msDeliveryms3_deliveriesСпособы доставки
msPaymentms3_paymentsСпособы оплаты
msDeliveryMemberms3_delivery_paymentsСвязь доставка-оплата

Отгрузки ​

Таблицы без xPDO-моделей: доступ через PdoShipmentStore и DI-ключ ms3_shipment_lifecycle (ShipmentLifecycleService). Миграции Phinx: create_shipments, create_shipment_events.

ТаблицаОписание
ms3_shipmentsОдна отгрузка на заказ (uniq_shipment_order)
ms3_shipment_eventsИдемпотентность webhook: provider_event_id на отгрузку

ms3_shipments — поля ​

ПолеТипОписание
idintID отгрузки
order_idint unsignedID заказа (unique)
delivery_idint unsignedID способа доставки
statusvarchar(32)preparing, shipped, in_transit, delivered, cancelled, returned, failed
tracking_numbervarchar(191)Трек-номер
external_idvarchar(191)ID у провайдера
providervarchar(191)Класс/имя провайдера
carriervarchar(191)Перевозчик
shipped_atint unsignedUnix-время отправки
delivered_atint unsignedUnix-время доставки
last_event_idvarchar(191)Последний provider_event_id
metatextJSON-метаданные
createdon / updatedonint unsignedUnix-время

При ms3_shipment_enabled=1 статус отгрузки переносит в заказ ShipmentLifecycleService: shipped → ms3_status_sent, cancelled/failed → ms3_status_canceled. Для delivered / in_transit смотрите настройки ms3_shipment_on_*_status. Сам статус заказа применяет OrderStatusService, но соответствие статусов живёт в сервисе отгрузок: чтобы задать своё, подмените ms3_shipment_lifecycle.

Попытки оплаты ​

Таблицы без xPDO-моделей. Доступ через PdoPaymentAttemptStore и DI-ключ ms3_payment_lifecycle (PaymentLifecycleService). Миграция Phinx: create_payment_attempts.

ТаблицаОписание
ms3_payment_attemptsПопытка оплаты заказа
ms3_payment_attempt_eventsИдемпотентность webhook: provider_event_id на попытку и тип события

ms3_payment_attempts — поля ​

ПолеТипОписание
idintID попытки
order_idint unsignedID заказа
payment_method_idint unsignedID способа оплаты
providervarchar(191)Класс или имя провайдера
external_idvarchar(191)ID у провайдера
statusvarchar(32)pending, authorized, paid, failed, cancelled, refunded, partially_refunded. По умолчанию pending
amountdecimal(13,3)Сумма
currencyvarchar(8)Валюта, по умолчанию RUB
payloadtextДанные провайдера
refunded_amountdecimal(13,3)Сумма возврата
refund_external_idvarchar(191)ID возврата у провайдера
refundedonint unsignedUnix-время возврата
createdon / updatedonint unsignedUnix-время

Уникальный ключ: payment_method_id, provider, external_id.

ms3_payment_attempt_events — поля ​

ПолеТипОписание
idintID события
attempt_idint unsignedID попытки
event_typevarchar(32)Тип события
provider_event_idvarchar(191)ID события у провайдера
createdonint unsignedUnix-время

Уникальный ключ: attempt_id, event_type, provider_event_id.

Производители ​

МодельТаблицаОписание
msVendorms3_vendorsПроизводители товаров

Опции ​

МодельТаблицаОписание
msOptionms3_optionsСправочник опций
msOptionGroupms3_option_groupsГруппы опций (заменили использование modCategory для группировки, начиная с 1.11.0)

Конфигурация полей ​

МодельТаблицаОписание
msModelFieldms3_model_fieldsНастройки полей моделей
msModelFieldSectionms3_model_field_sectionsСекции полей
msGridFieldms3_grid_fieldsКонфигурация колонок гридов: customers, orders, order_products, vendors, category-products, deliveries
msProductFieldms3_product_fieldsПоля товара. Строку создаёт ExtraFieldsService вместе со своим полем, читают ConfigService и FieldConfigManager
msPageSectionms3_page_sectionsСекции страниц. Обслуживает /api/mgr/config/page-fields/{page_key}
msExtraFieldms3_extra_fieldsДополнительные поля

Уведомления ​

МодельТаблицаОписание
msNotificationConfigms3_notification_configsКонфигурация уведомлений

Примеры работы с моделями ​

Создание заказа ​

Так заказ создавать не стоит

Пример ниже показывает, как устроены записи заказа в базе, и только. Собранный так заказ не прошёл валидацию, не получил расчёт стоимости и доставки, не поднял события и не отправил уведомления.

Для создания заказов из кода есть сервис ms3_programmatic_order (ProgrammaticOrderService). Он строит черновик и проводит его через обычное оформление. По idempotency_key не создаёт дубль при повторном вызове.

php
$result = $modx->services->get('ms3_programmatic_order')->create([
    'idempotency_key' => 'crm-order-15', // обязателен
    'products' => [
        ['product_id' => $productId, 'count' => 2],
    ],
    'delivery_id' => $deliveryId,
    'payment_id' => $paymentId,
]);
php
use MiniShop3\Model\msOrder;
use MiniShop3\Model\msOrderProduct;

// Создание заказа
$order = $modx->newObject(msOrder::class);
$order->fromArray([
    'customer_id' => $customerId,
    'user_id' => $modx->user->get('id') ?: 0,
    'status_id' => $modx->getOption('ms3_status_new'),
    'delivery_id' => $deliveryId,
    'payment_id' => $paymentId,
    'context' => $modx->context->key,
]);
$order->save();

// Добавление товара в заказ
$orderProduct = $modx->newObject(msOrderProduct::class);
$orderProduct->fromArray([
    'order_id' => $order->get('id'),
    'product_id' => $productId,
    'name' => $productName,
    'price' => $price,
    'count' => $count,
    'cost' => $price * $count,
    'options' => $options,
]);
$orderProduct->save();

Работа с покупателем ​

php
use MiniShop3\Model\msCustomer;
use MiniShop3\Model\msCustomerAddress;

// Поиск или создание покупателя
$customer = $modx->getObject(msCustomer::class, ['email' => $email]);
if (!$customer) {
    $customer = $modx->newObject(msCustomer::class);
    $customer->fromArray([
        'email' => $email,
        'first_name' => $firstName,
        'phone' => $phone,
    ]);
    $customer->save();
}

// Добавление адреса
$address = $modx->newObject(msCustomerAddress::class);
$address->fromArray([
    'customer_id' => $customer->get('id'),
    'city' => $city,
    'street' => $street,
    'building' => $building,
]);
$address->save();

// Получение всех адресов покупателя — прямым запросом:
// связи Addresses у msCustomer нет
$addresses = $modx->getIterator(msCustomerAddress::class, [
    'customer_id' => $customer->get('id'),
]);

Итерация по записям ​

getIterator() достаёт записи по одной и не держит всю выборку в памяти. На больших наборах это предпочтительнее getCollection():

php
use MiniShop3\Model\msOrder;

foreach ($modx->getIterator(msOrder::class, ['status_id' => 2]) as $order) {
    // Обработка заказа
}

getIterator() и классы без своей таблицы

getCollection() и getObject() сами добавляют в условие фильтр по class_key. Это делает addDerivativeCriteria(). Более того, msProduct и msCategory дублируют эту защиту в своих loadCollection() и load(). getIterator() не проходит ни через один из этих методов: он создаёт итератор напрямую, и фильтра не будет.

Для msProduct и msCategory это критично: своей таблицы у них нет, они живут в site_content вместе со всеми ресурсами MODX. Без фильтра выборка захватит чужие записи. Первая же из них даст ошибку вида «Instantiated a derived class msProduct that is not a subclass of the requested class msCategory».

Поэтому при getIterator() для таких классов class_key задавайте вручную:

php
$criteria = [
    'class_key' => msProduct::class, // обязательно
    'parent' => $categoryId,
];

foreach ($modx->getIterator(msProduct::class, $criteria) as $product) {
    // …
}

Это не касается моделей со своей таблицей: msOrder, msCustomer, msProductData и остальных из таблиц выше.

Архитектура моделей ​

Модель отвечает за таблицу и связи. Бизнес-логика живёт в сервисах. См. Сервисный слой.

Полностью «тонкими» модели не стали: msProduct и msCategory расширяют modResource, а MODX держит бизнес-логику в моделях. Поэтому модель вправе вызвать сервис:

php
// src/Model/msProductData.php
class msProductData extends xPDOSimpleObject
{
    public function getPrice($data = [])
    {
        if (empty($data)) {
            $data = $this->toArray();
        }

        return $this->getProductDataService()->getModifiedPrice($this, $data);
    }
}

Регенерация моделей ​

Модели создаются вручную

В MiniShop3 модели не генерируются автоматически из XML-схемы через buildModel(). Они создаются и поддерживаются вручную в директории src/Model/.

Это позволяет:

  • добавлять свои методы в модели;
  • использовать полную типизацию PHP 8;
  • иметь полный контроль над кодом моделей.

Не запускайте buildModel(): это перезапишет методы, добавленные вручную.

Структура файлов модели ​

src/Model/
├── msProduct.php           # Основной класс модели
├── mysql/
│   └── msProduct.php       # MySQL-специфичный маппинг

Пример модели ​

php
<?php
namespace MiniShop3\Model;

use MODX\Revolution\modResource;

class msProduct extends modResource
{
    // Свои методы модели. Не переопределяйте getPrice(): он уже есть
    // у msProduct с другой подписью и прогоняет цену через модификаторы
    public function getRawPrice(): float
    {
        $data = $this->getOne('Data');
        return $data ? (float) $data->get('price') : 0.0;
    }
}

Пример маппинга ​

php
<?php
namespace MiniShop3\Model\mysql;

class msProduct extends \MiniShop3\Model\msProduct
{
    public static $metaMap = [
        'package' => 'MiniShop3\\Model',
        'version' => '3.0',
        'extends' => 'MODX\\Revolution\\modResource',
        'fields' => [
            'class_key' => 'MiniShop3\\Model\\msProduct',
        ],
        // ... остальные метаданные
    ];
}

Миграции Phinx ​

Схему базы данных меняют миграции Phinx:

core/components/minishop3/migrations/
├── 20251020000000_initial_schema.php
├── 20251021000000_seed_page_sections.php
├── 20251022000000_seed_product_fields.php
├── 20251023000000_seed_order_statuses.php
└── ...

Запуск миграций ​

Руками запускать их обычно не нужно. При установке и обновлении пакета миграции применяет резолвер resolver_02_migrations. При работе с дополнительными полями их применяет ExtraFieldsService. Оба используют окружение production — оно же стоит в phinx.php по умолчанию.

Для разработки миграции прогоняют из CLI:

bash
cd /path/to/modx
php core/components/minishop3/vendor/bin/phinx migrate -c core/components/minishop3/phinx.php

Пример миграции ​

php
<?php
use Phinx\Migration\AbstractMigration;

class CreateCustomersTable extends AbstractMigration
{
    public function change()
    {
        $table = $this->table('ms3_customers', [
            'engine' => 'InnoDB',
            'collation' => 'utf8mb4_unicode_ci',
        ]);

        $table
            ->addColumn('email', 'string', ['limit' => 191])
            ->addColumn('first_name', 'string', ['limit' => 191, 'null' => true])
            ->addColumn('phone', 'string', ['limit' => 50, 'null' => true])
            ->addIndex(['email'], ['unique' => true])
            ->create();
    }
}

Диаграмма связей ​

Товар ​

Заказ ​

ms3_shipments / ms3_shipment_events — PDO-таблицы (не классы MiniShop3\Model\*).

Линия msCustomer → msCustomerAddress — это связь в базе через customer_id, а не алиас схемы: пройти её можно только в обратную сторону, $address->getOne('Customer').