Достался мне проект на 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-проект может быть таким же поддерживаемым, как и любой другой.
Пока нет комментариев
Будьте первым, кто оставит комментарий к этой статье