Загрузка
Все статьи

MODX: процессоры и кастомные таблицы — архитектура без костылей

MODX Revolution позволяет писать кастомные процессоры для любой логики, но большинство разработчиков превращают их в спагетти-код с прямыми SQL-запросами. Мы выстроили архитектуру на основе xPDO-моделей, кастомных процессоров с валидацией и отдельного слоя репозиториев. Результат: поддержка проекта перестала быть кошмаром.

Достался мне проект на MODX Revolution — интернет-магазин с кастомной логикой заказов, интеграцией с 1С и кучей пользовательских таблиц. Предыдущий разработчик делал всё через модификаторы в шаблонах и прямые $modx->query() в сниппетах. Когда мне нужно было добавить поле к заказу, я потратил два дня, просто чтобы найти, где оно формируется.

После этого я решил выстроить нормальную архитектуру. Вот что из этого вышло.

Типичные антипаттерны в MODX

Вот что я видел в 90% MODX-проектов, которые мне попадались:

Первое — бизнес-логика в чанках и шаблонах. Когда в чанке order-row.tpl стоит PHP-код с условием расчёта скидки — это приговор к поддержке. Шаблон должен только рендерить данные, а не вычислять их.

Второе — прямые SQL-запросы через $modx->query(). Да, xPDO иногда тормозит на сложных выборках, но прямой SQL означает, что ты теряешь кэширование, события и возможность отлаживать через логи MODX.

Третье — монолитные сниппеты на 500+ строк. Один сниппет OrderProcessor, который и валидирует, и сохраняет, и отправляет email, и вызывает 1С. Если упало на отправке email — заказ уже сохранён, но пользователь видит ошибку. Классика.

xPDO-модель для кастомной таблицы

Первым делом описываем схему. Файл core/components/myshop/model/schema/myshop.mysql.schema.xml:




    
        
        
        
        
        
        

        
            
        

        
        
    

После генерации модели получаем PHP-класс OrderItem с которым можно работать через xPDO:

$item = $modx->newObject('OrderItem');
$item->set('order_id', $orderId);
$item->set('product_id', $productId);
$item->set('quantity', 3);
$item->set('price', 1500.00);
$item->set('created_at', date('Y-m-d H:i:s'));
$item->save();

Плюс xPDO автоматически работает с кэшем MODX, логирует запросы и поддерживает связи между таблицами.

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

Процессоры в MODX — это контроллеры, которые обрабатывают AJAX-запросы. Создаём процессор для создания заказа: core/components/myshop/processors/mgr/order/create.class.php

class ShopOrderCreateProcessor extends modObjectCreateProcessor
{
    public $classKey = 'ShopOrder';
    public $objectType = 'myshop.order';
    public $languageTopics = ['myshop:default'];

    public function beforeSet()
    {
        $userId = (int) $this->getProperty('user_id');
        $items  = $this->getProperty('items', []);

        if (empty($userId)) {
            return $this->failure('Укажите пользователя');
        }

        if (!is_array($items) || count($items) === 0) {
            return $this->failure('Заказ должен содержать хотя бы один товар');
        }

        foreach ($items as $item) {
            if (empty($item['product_id']) || empty($item['quantity'])) {
                return $this->failure('Некорректные данные товара');
            }
            if ($item['quantity'] < 1 || $item['quantity'] > 100) {
                return $this->failure('Количество должно быть от 1 до 100');
            }
        }

        $this->setProperty('status', 'new');
        $this->setProperty('created_at', date('Y-m-d H:i:s'));
        $this->setProperty('total', $this->calculateTotal($items));

        return true;
    }

    public function afterSave()
    {
        $items = $this->getProperty('items', []);
        $orderId = $this->object->get('id');

        foreach ($items as $itemData) {
            $orderItem = $this->modx->newObject('OrderItem');
            $orderItem->set('order_id', $orderId);
            $orderItem->set('product_id', $itemData['product_id']);
            $orderItem->set('quantity', $itemData['quantity']);
            $orderItem->set('price', $itemData['price']);
            $orderItem->set('created_at', date('Y-m-d H:i:s'));
            $orderItem->save();
        }

        return parent::afterSave();
    }

    private function calculateTotal(array $items): float
    {
        $total = 0.0;
        foreach ($items as $item) {
            $total += (float) $item['price'] * (int) $item['quantity'];
        }
        return round($total, 2);
    }
}

Вызов процессора из фронтенда или сниппета:

$response = $modx->runProcessor('mgr/order/create', [
    'user_id' => $userId,
    'items'   => [
        ['product_id' => 15, 'quantity' => 2, 'price' => 1200.00],
        ['product_id' => 23, 'quantity' => 1, 'price' => 3500.00],
    ],
], ['processors_path' => $modx->getOption('core_path') . 'components/myshop/processors/']);

if ($response->isError()) {
    return $response->getMessage();
}
$order = $response->getObject();

Структура каталога компонента

Мы выстроили такую структуру, и она нам отлично служит:

core/components/myshop/ — модели, процессоры, сервисы
model/ — xPDO-схема и сгенерированные классы
processors/ — mgr/ (админка) и web/ (фронтенд)
services/ — репозитории и бизнес-логика
lexicon/ — языковые файлы
assets/components/myshop/ — JS, CSS, шаблоны

Главный инсайт: MODX не заставляет тебя писать плохой код. Это просто фреймворк, и архитектура зависит от разработчика. Если навести порядок с процессорами и xPDO-моделями — MODX-проект может быть таким же поддерживаемым, как и любой другой.

Поделиться статьёй

Комментарии

0
0 / 2000

Пока нет комментариев

Будьте первым, кто оставит комментарий к этой статье

Читайте также

MODX Revolution: сниппеты, которые вызывают leak памяти на каждой странице

Сайт на MODX Revolution ест по 128MB RAM на каждую страницу — это не норма. Разбираю, почему getCollection с большими выборками убивает память, как правильно использовать xPDO для постраничной загрузки данных, и почему простой сниппет вывода ресурсов может съесть весь сервер.

6 мая 2026 6 мин

Сложности с разделами каталога в Битрикс24 через REST API: игра с деревом

Обновляете товар через API, передаете SECTION_ID, но он не появляется в нужной папке. В этой статье я объясняю разницу между простым SECTION_ID и сложным деревом инфоблоков, как правильно выстроить иерархию и почему метод crm.product.update не умеет создавать папки.

5 июля 2026 1 мин

CMake: модульная архитектура для большого C++ проекта — без ad-hoc include путей

Как мы реорганизовали CMake-структуру проекта на 15+ модулей, избавились от хаотичных include_directories и перешли на современный CMake с target-based зависимостями.

20 июня 2026 10 мин