Skip to content
MiniShop3
MiniShop3
Современный компонент интернет-магазина для MODX 3
  1. Компоненты
  2. MiniShop3
  3. Разработка
  4. Сервисный слой

Сервисный слой

MiniShop3 использует сервисный слой для отделения бизнес-логики от ORM моделей. Это архитектурное решение улучшает тестируемость, переиспользуемость и расширяемость кода.

Архитектурный паттерн

Принцип разделения

В классическом подходе (который использовался в miniShop2) бизнес-логика размещалась непосредственно в моделях:

php
// miniShop2 - "толстые" модели
class msProduct extends modResource
{
    public function duplicate($options = []) { /* 200+ строк логики */ }
    public function getPrice() { /* логика расчёта цены */ }
    public function updateImages() { /* работа с изображениями */ }
}

В MiniShop3 модели отвечают только за работу с базой данных (xPDO ORM), а вся бизнес-логика вынесена в сервисы:

php
// MiniShop3 - "тонкие" модели + сервисы
class msProduct extends modResource
{
    // Только xPDO связи и базовые операции с БД
}

class ProductService
{
    public function duplicate($product, $options) { /* логика */ }
    public function processForDisplay($product) { /* логика */ }
}

Преимущества

АспектТолстые моделиСервисный слой
ТестированиеСложно мокатьЛегко изолировать
ПереиспользованиеПривязано к ORMНезависимые сервисы
РасширениеНаследованиеЗамена через DI
Размер файлов1000+ строк100-300 строк

Компромисс с архитектурой MODX

Архитектура MODX повсеместно использует бизнес-логику в моделях (modResource, modUser и др.). Поскольку некоторые модели MiniShop3 расширяют классы MODX (msProduct extends modResource), они обязаны следовать аналогичным паттернам.

Поэтому модели MiniShop3 содержат ссылки на сервисы для вызова методов бизнес-логики — это прагматичный компромисс для совместимости с экосистемой MODX.

DI Container

MiniShop3 использует встроенный DI-контейнер MODX ($modx->services) для регистрации и получения сервисов.

Регистрация сервисов

Все сервисы регистрируются через ServiceRegistry при инициализации компонента:

php
// bootstrap.php
$serviceRegistry = new \MiniShop3\ServiceRegistry($modx);
$serviceRegistry->register();

Получение сервисов

php
// Через DI контейнер (рекомендуется)
$orderService = $modx->services->get('ms3_order_service');
$authManager = $modx->services->get('ms3_auth_manager');
$cart = $modx->services->get('ms3_cart');

// Через главный класс MiniShop3
$ms3 = $modx->services->get('ms3');
$ms3->cart->add($productId, 1);
$ms3->order->submit();

Ленивая загрузка

Сервисы создаются только при первом обращении:

php
// Сервис ещё не создан
$modx->services->add('ms3_order_service', function() use ($modx) {
    // Эта функция выполнится только при первом get()
    return new \MiniShop3\Services\Order\OrderService($modx);
});

// Первое обращение - создание объекта
$service1 = $modx->services->get('ms3_order_service');

// Повторное обращение - тот же объект (singleton)
$service2 = $modx->services->get('ms3_order_service');
// $service1 === $service2

Конвенция именования

Все сервисы MiniShop3 используют префикс ms3_:

php
'ms3_order_service'      // OrderService
'ms3_auth_manager'       // AuthManager
'ms3_cart'               // Cart controller
'ms3_product_image'      // ProductImageService

Это предотвращает конфликты с другими компонентами MODX.

Карта сервисов

Контроллеры (Controllers)

Контроллеры — это высокоуровневые сервисы, управляющие основными сущностями магазина.

КлючКлассНазначение
ms3_cartControllers\Cart\CartУправление корзиной
ms3_orderControllers\Order\OrderОформление заказа
ms3_customerControllers\Customer\CustomerРабота с покупателем
php
// Пример использования
$ms3 = $modx->services->get('ms3');

// Добавление в корзину
$result = $ms3->cart->add($productId, 2, ['color' => 'red']);

// Оформление заказа
$result = $ms3->order->submit();

// Получение данных покупателя
$customer = $ms3->customer->getFields();

Сервисы товаров (Product)

КлючКлассНазначение
ms3_product_data_serviceServices\Product\ProductDataServiceРабота с данными товара
ms3_product_imageServices\Product\ProductImageServiceОбработка изображений товара
php
$productService = $modx->services->get('ms3_product_data_service');
$imageService = $modx->services->get('ms3_product_image');

Сервисы покупателя (Customer)

КлючКлассНазначение
ms3_auth_managerServices\Customer\AuthManagerАутентификация
ms3_register_serviceServices\Customer\RegisterServiceРегистрация
ms3_email_verification_serviceServices\Customer\EmailVerificationServiceВерификация email
ms3_sms_verification_serviceServices\Customer\SmsVerificationServiceВерификация по SMS
ms3_rate_limiterServices\Customer\RateLimiterОграничение частоты запросов
ms3_customer_duplicate_checkerServices\CustomerDuplicateCheckerПроверка дубликатов
ms3_customer_factoryServices\CustomerFactoryФабрика покупателей
php
// Аутентификация
$authManager = $modx->services->get('ms3_auth_manager');
$customer = $authManager->authenticate([
    'email' => 'user@example.com',
    'password' => 'secret123'
]);

// Создание токена
$token = $authManager->createToken($customer, 'api', 86400);

// Регистрация провайдера аутентификации
$authManager->registerProvider(new SmsAuthProvider($modx));

Сервисы заказа (Order)

Архитектура фасада

Контроллер Order.php является фасадом — он сохраняет все публичные методы для обратной совместимости, но делегирует логику в специализированные сервисы. Это позволяет переопределять отдельные части логики без модификации всего контроллера.

КлючКлассНазначение
ms3_order_serviceServices\Order\OrderServiceОбщая бизнес-логика заказов
ms3_order_draft_managerServices\Order\OrderDraftManagerCRUD черновиков заказов
ms3_order_cost_calculatorServices\Order\OrderCostCalculatorРасчёт стоимости
ms3_order_field_managerServices\Order\OrderFieldManagerCRUD полей + валидация
ms3_order_address_managerServices\Order\OrderAddressManagerРабота с адресами клиентов
ms3_order_user_resolverServices\Order\OrderUserResolverРезолвинг MODX пользователей
ms3_order_submit_handlerServices\Order\OrderSubmitHandlerОформление заказа
ms3_order_logServices\Order\OrderLogServiceЛогирование изменений заказа
ms3_order_statusServices\Order\OrderStatusServiceСмена статуса + уведомления
php
// Получение сервисов напрямую
$draftManager = $modx->services->get('ms3_order_draft_manager');
$costCalculator = $modx->services->get('ms3_order_cost_calculator');
$submitHandler = $modx->services->get('ms3_order_submit_handler');

// Работа с черновиком
$draft = $draftManager->getOrCreateDraft($token, 'web');
$draftManager->attachCustomer($draft, $customerId);

// Расчёт стоимости
$cost = $costCalculator->calculate($order);

// Логирование
$logService = $modx->services->get('ms3_order_log');
$logService->addEntry($order, 'status_changed', ['old' => 1, 'new' => 2]);

Сервисы корзины (Cart)

Архитектура фасада

Контроллер Cart.php также является фасадом. Он использует OrderDraftManager для общих операций с черновиками (корзина и заказ используют одну модель msOrder).

КлючКлассНазначение
ms3_cart_item_managerServices\Cart\CartItemManagerCRUD товаров в корзине, валидация, расчёт итогов
php
$itemManager = $modx->services->get('ms3_cart_item_manager');

// Добавление товара
$result = $itemManager->addItem($draft, $productId, $count, $options);

// Изменение количества
$itemManager->updateItemCount($draft, $key, $newCount);

// Расчёт итогов корзины
$status = $itemManager->calculateStatus($draft);

Разделение ответственности Cart и Order:

OrderDraftManager    — жизненный цикл черновика (общий для Cart и Order)
CartItemManager      — операции с позициями корзины (Cart-specific)
OrderCostCalculator  — расчёт стоимости заказа (Order-specific)
OrderFieldManager    — поля заказа (Order-specific)

Сервисы доставки и оплаты

КлючКлассНазначение
ms3_delivery_serviceServices\Delivery\DeliveryServiceСпособы доставки
ms3_payment_serviceServices\Payment\PaymentServiceСпособы оплаты

Сервисы категорий (Category)

КлючКлассНазначение
ms3_category_serviceServices\Category\CategoryServiceРабота с категориями
ms3_category_option_serviceServices\Category\CategoryOptionServiceОпции категорий

Сервисы опций (Option)

КлючКлассНазначение
ms3_option_serviceServices\Option\OptionServiceEAV система опций

Сервисы конфигурации

КлючКлассНазначение
ms3_config_managerServices\ConfigManagerУправление конфигурацией
ms3_field_config_managerServices\FieldConfigManagerКонфигурация полей
ms3_config_serviceServices\ConfigServiceФасад над менеджерами
ms3_grid_configServices\GridConfigServiceКонфигурация гридов

Сервисы уведомлений

КлючКлассНазначение
ms3_notificationsNotifications\NotificationManagerЦентр уведомлений
ms3_notification_configServices\Notification\NotificationConfigServiceКонфигурация уведомлений

Утилиты

КлючКлассНазначение
ms3_token_serviceServices\TokenServiceОперации с токенами
ms3_imageServices\ImageServiceОбработка изображений (Intervention Image)
ms3_vendor_serviceServices\Vendor\VendorServiceРабота с производителями

Замена сервисов

MiniShop3 позволяет заменять стандартные сервисы своими реализациями через конфигурационные файлы.

Приоритет загрузки

  1. Встроенные классы — базовые реализации компонента
  2. Основной конфигcore/config/ms3.services.php
  3. Конфиги компонентовcore/config/ms3.services.d/*.php (в алфавитном порядке)

Каждый следующий уровень переопределяет предыдущий.

Создание конфигурации

Скопируйте пример конфигурации:

bash
cp core/components/minishop3/config/ms3.services.example.php \
   core/config/ms3.services.php

Пример переопределения

php
<?php
// core/config/ms3.services.php

return [
    // Кастомная корзина с промокодами
    'ms3_cart' => [
        'class' => \MyProject\Controllers\PromoCart::class,
        'interface' => null,
    ],

    // Кастомный обработчик оформления заказа (интеграция с CRM)
    'ms3_order_submit_handler' => [
        'class' => \MyProject\Services\CRMOrderSubmitHandler::class,
        'interface' => null,
    ],

    // Кастомный сервис доставки (СДЭК)
    'ms3_delivery_service' => [
        'class' => \MyProject\Services\CdekDeliveryService::class,
        'interface' => null,
    ],
];

Пример кастомного OrderSubmitHandler с интеграцией CRM:

php
<?php
namespace MyProject\Services;

use MiniShop3\Services\Order\OrderSubmitHandler;

class CRMOrderSubmitHandler extends OrderSubmitHandler
{
    public function submit(
        \MiniShop3\Model\msOrder $draft,
        array $orderData,
        ?\MiniShop3\Model\msCustomer $customer
    ): array {
        // Своя логика ДО оформления
        $this->validateWithCRM($orderData);

        // Вызов родительского метода
        $result = parent::submit($draft, $orderData, $customer);

        // Своя логика ПОСЛЕ успешного оформления
        if ($result['success']) {
            $this->syncOrderToCRM($result['data']['order_id']);
        }

        return $result;
    }

    protected function syncOrderToCRM(int $orderId): void
    {
        // Отправка в CRM
    }
}

Требования к кастомным классам

Кастомный класс должен:

  1. Существовать и быть доступным через автозагрузчик
  2. Наследовать базовый класс MiniShop3
php
<?php
namespace MyProject\Controllers;

use MiniShop3\Controllers\Cart\Cart;

class PromoCart extends Cart
{
    public function add(int $id, int $count = 1, array $options = []): array
    {
        // Проверка промокода
        $promoCode = $_SESSION['promo_code'] ?? null;
        if ($promoCode) {
            $options['promo_code'] = $promoCode;
        }

        // Вызов родительского метода
        return parent::add($id, $count, $options);
    }

    public function applyPromoCode(string $code): array
    {
        // Кастомная логика промокодов
        $_SESSION['promo_code'] = $code;
        return $this->success('Промокод применён');
    }
}

Валидация классов

ServiceRegistry автоматически проверяет:

  • Существование классаclass_exists()
  • Наследованиеis_subclass_of($className, $baseClass)
  • Интерфейсыclass_implements() (если указано)

При ошибке валидации используется стандартный класс + запись в лог:

[MiniShop3 ServiceRegistry] Class 'MyProject\Cart' must extend MiniShop3\Controllers\Cart\Cart, using fallback

Расширение сторонними компонентами

Если вы разрабатываете компонент, расширяющий MiniShop3, используйте директорию ms3.services.d/ для бесконфликтной интеграции.

Структура

core/config/ms3.services.d/
├── 01-base.php           # Базовые переопределения (приоритет 01)
├── 50-mypromocode.php    # Компонент промокодов (приоритет 50)
├── 50-cdekdelivery.php   # Компонент доставки СДЭК (приоритет 50)
└── 99-override.php       # Финальные переопределения (приоритет 99)

Преимущества

  • Нет конфликтов — каждый компонент в своём файле
  • Управление приоритетом — через имена файлов (алфавитный порядок)
  • Независимая установка — компоненты не перезаписывают конфиги друг друга

Пример конфига компонента

php
<?php
// core/config/ms3.services.d/50-mypromocode.php

return [
    'ms3_cart' => [
        'class' => \MyPromoCode\Controllers\PromoCart::class,
    ],
];

Логирование

ServiceRegistry логирует все загруженные конфиги:

[MiniShop3 ServiceRegistry] Loaded 1 service(s) from addon: 50-mypromocode.php
[MiniShop3 ServiceRegistry] Using custom class: MyPromoCode\Controllers\PromoCart

Примеры использования

Кастомный расчёт стоимости доставки

php
<?php
namespace MyProject\Services;

use MiniShop3\Services\Delivery\DeliveryService;
use MiniShop3\Model\msOrder;

class CustomDeliveryService extends DeliveryService
{
    public function calculateCost(msOrder $order): float
    {
        $baseCost = parent::calculateCost($order);

        // Бесплатная доставка от 5000 руб
        if ($order->get('cart_cost') >= 5000) {
            return 0;
        }

        // Наценка для тяжёлых заказов
        if ($order->get('weight') > 10000) {
            $baseCost *= 1.5;
        }

        return $baseCost;
    }
}

Интеграция с внешней CRM

php
<?php
namespace MyProject\Services;

use MiniShop3\Services\Order\OrderService;
use MiniShop3\Model\msOrder;

class CRMOrderService extends OrderService
{
    public function handleOrderSave(msOrder $order, ?bool $cacheFlag = null): bool
    {
        $result = parent::handleOrderSave($order, $cacheFlag);

        // Отправка в CRM при смене статуса
        if ($order->isDirty('status_id')) {
            $this->syncToCRM($order);
        }

        return $result;
    }

    protected function syncToCRM(msOrder $order): void
    {
        $client = new \GuzzleHttp\Client();
        $client->post('https://crm.example.com/api/orders', [
            'json' => [
                'order_id' => $order->get('id'),
                'status' => $order->get('status_id'),
                'total' => $order->get('cost'),
            ]
        ]);
    }
}

Кастомный провайдер аутентификации

php
<?php
namespace MyProject\Auth;

use MiniShop3\Controllers\Auth\AuthProviderInterface;
use MiniShop3\Model\msCustomer;
use MODX\Revolution\modX;

class TelegramAuthProvider implements AuthProviderInterface
{
    protected modX $modx;

    public function __construct(modX $modx)
    {
        $this->modx = $modx;
    }

    public function getName(): string
    {
        return 'telegram';
    }

    public function supports(array $credentials): bool
    {
        return isset($credentials['telegram_hash'])
            && isset($credentials['telegram_id']);
    }

    public function authenticate(array $credentials): ?msCustomer
    {
        // Проверка подписи Telegram
        if (!$this->verifyTelegramHash($credentials)) {
            return null;
        }

        // Поиск или создание покупателя
        return $this->modx->getObject(msCustomer::class, [
            'telegram_id' => $credentials['telegram_id']
        ]);
    }

    protected function verifyTelegramHash(array $data): bool
    {
        // Логика проверки подписи Telegram
        return true;
    }
}

Регистрация провайдера:

php
$authManager = $modx->services->get('ms3_auth_manager');
$authManager->registerProvider(new TelegramAuthProvider($modx));

Системные настройки

НастройкаПо умолчаниюОписание
ms3_services_configcore/config/ms3.services.phpПуть к основному конфигу
ms3_services_addons_dircore/config/ms3.services.d/Директория конфигов компонентов

Устранение проблем

Сервис не найден

Service 'ms3_my_service' not found

Решения:

  1. Убедитесь, что ServiceRegistry::register() вызывается при инициализации
  2. Проверьте правильность ключа сервиса (с префиксом ms3_)
  3. Проверьте, что класс существует и доступен автозагрузчику

Кастомный класс не применяется

[MiniShop3 ServiceRegistry] Class 'MyClass' must extend BaseClass, using fallback

Решения:

  1. Убедитесь, что кастомный класс наследует базовый класс MiniShop3
  2. Проверьте namespace и use-директивы
  3. Очистите кеш MODX

Конфликт компонентов

Если два компонента переопределяют один сервис, применится тот, чей файл загружается последним (по алфавиту).

Решение: Используйте числовые префиксы для управления порядком:

  • 10-base-component.php — загрузится первым
  • 90-override-component.php — загрузится последним и победит