
События сниппета msProducts
События для интеграции внешних пакетов (ms3Variants, msBrands и др.) в сниппет msProducts без модификации кода ядра MiniShop3.
Параметр usePackages
Для активации загрузки данных внешнего пакета укажите его имя в параметре сниппета:
{'msProducts' | snippet : [
'parents' => 0,
'usePackages' => 'ms3Variants,msBrands'
]}Плагины проверяют наличие своего пакета в этом параметре и загружают данные только при необходимости.
msOnProductsLoad
Вызывается после загрузки списка товаров из БД, до их обработки. Предназначен для bulk-загрузки дополнительных данных одним запросом — это позволяет избежать проблемы N+1.
Параметры
| Параметр | Тип | Описание |
|---|---|---|
rows | array | Массив товаров. Изменения применяются только через returnedValues['rows'] — PHP-ссылка на этом уровне не сохраняется (см. ниже) |
productIds | array | Массив ID товаров [1, 2, 3, ...] |
usePackages | array | Список запрошенных пакетов ['ms3Variants', 'msBrands'] |
scriptProperties | array | Все параметры вызова сниппета |
После события строки списка обновляются из returnedValues['rows'] (EventGate::applyReturnedArray). List в rows заменяет весь массив товаров, assoc-пatch отдельной строки — через msOnProductPrepare.
returnedValues
<?php
switch ($modx->event->name) {
case 'msOnProductsLoad':
$values = &$modx->event->returnedValues;
// Пример: пометить все товары без цены
$rows = $scriptProperties['rows'];
foreach ($rows as $i => $row) {
if (empty($row['price'])) {
$rows[$i]['price_hidden'] = true;
}
}
$values['rows'] = $rows;
break;
}Базовый пример
<?php
switch ($modx->event->name) {
case 'msOnProductsLoad':
// Проверяем, запрошен ли наш пакет
$usePackages = $scriptProperties['usePackages'] ?? [];
if (!in_array('myPackage', $usePackages)) {
return;
}
// Загружаем данные для ВСЕХ товаров одним запросом
$productIds = $scriptProperties['productIds'];
// Ваша логика загрузки данных
$myData = loadDataForProducts($productIds);
// Сохраняем в eventData для использования в msOnProductPrepare
$modx->eventData['myPackage'] = [
'dataMap' => $myData,
];
break;
}Пример: загрузка вариантов товаров (ms3Variants)
<?php
switch ($modx->event->name) {
case 'msOnProductsLoad':
// Проверяем, запрошен ли наш пакет
$usePackages = $scriptProperties['usePackages'] ?? [];
if (!in_array('ms3Variants', $usePackages)) {
return;
}
// Проверяем, установлен ли пакет
if (!$modx->services->has('ms3variants')) {
return;
}
// Загружаем варианты для ВСЕХ товаров одним запросом
$productIds = $scriptProperties['productIds'];
$variantService = $modx->services->get('ms3variants_variant_service');
// Сохраняем в eventData для msOnProductPrepare
$modx->eventData['ms3variants'] = [
'variantsMap' => $variantService->getVariantsForProducts($productIds, ['active' => true]),
];
break;
}msOnProductPrepare
Вызывается при подготовке каждого товара к рендерингу. Используется для присоединения данных, загруженных в msOnProductsLoad, к конкретному товару.
Параметры
| Параметр | Тип | Описание |
|---|---|---|
row | array | Данные товара. Изменения применяются только через returnedValues['row'] — PHP-ссылка на этом уровне не сохраняется (см. ниже) |
productId | int | ID товара |
idx | int | Порядковый номер товара в выборке |
После события строка обновляется из returnedValues['row'] (patch или list-замена, как в import).
<?php
switch ($modx->event->name) {
case 'msOnProductPrepare':
$modx->event->returnedValues = [
'row' => ['badge' => 'sale'],
];
break;
}Мутация $scriptProperties['row'] по ссылке не работает
Хотя row помечен как «ссылка» на стороне вызова (EventGate::invokeRaw(..., ['row' => &$rows[$k], ...])), PHP-ссылка не переживает merge-конвейер MODX-ядра (modElement::getProperties() → modX::invokeEvent(), оба используют array_merge(), который разыменовывает ссылки). Прямое изменение $scriptProperties['row']['field'] = ... внутри плагина не доходит до шаблона — нужно обязательно выставлять $modx->event->returnedValues['row'], как показано ниже.
Базовый пример
<?php
switch ($modx->event->name) {
case 'msOnProductPrepare':
// Если данных нет — пакет не был запрошен или не установлен
$myData = $modx->eventData['myPackage']['dataMap'] ?? null;
if ($myData === null) {
return;
}
$productId = $scriptProperties['productId'];
// Присоединяем данные к товару — только через returnedValues,
// прямая мутация $scriptProperties['row'] по ссылке до шаблона не доходит
if (isset($myData[$productId])) {
$modx->event->returnedValues = [
'row' => ['my_field' => $myData[$productId]],
];
}
break;
}Пример: присоединение вариантов (ms3Variants)
<?php
switch ($modx->event->name) {
case 'msOnProductPrepare':
// Если данных нет — пакет не был запрошен
$variantsMap = $modx->eventData['ms3variants']['variantsMap'] ?? null;
if ($variantsMap === null) {
return;
}
$productId = $scriptProperties['productId'];
// Присоединяем варианты — только через returnedValues (см. предупреждение выше)
if (isset($variantsMap[$productId])) {
$row = [
'variants' => $variantsMap[$productId],
'variants_count' => count($variantsMap[$productId]),
'variants_json' => json_encode($variantsMap[$productId]),
'has_variants' => true,
];
} else {
$row = [
'variants' => [],
'variants_count' => 0,
'variants_json' => '[]',
'has_variants' => false,
];
}
$modx->event->returnedValues = ['row' => $row];
break;
}Полный пример плагина интеграции
Пример плагина, добавляющего бейджи "Новинка" к товарам, созданным за последние 7 дней.
<?php
/**
* Плагин: Бейджи товаров
* События: msOnProductsLoad, msOnProductPrepare
*/
switch ($modx->event->name) {
case 'msOnProductsLoad':
// Проверяем параметр
$usePackages = $scriptProperties['usePackages'] ?? [];
if (!in_array('msBadges', $usePackages)) {
return;
}
$rows = $scriptProperties['rows'];
$weekAgo = strtotime('-7 days');
// Определяем новинки (созданы за последние 7 дней)
$newProducts = [];
foreach ($rows as $row) {
if (strtotime($row['createdon']) > $weekAgo) {
$newProducts[$row['id']] = true;
}
}
// Сохраняем для msOnProductPrepare
$modx->eventData['msBadges'] = [
'newProducts' => $newProducts,
];
break;
case 'msOnProductPrepare':
$badgesData = $modx->eventData['msBadges'] ?? null;
if ($badgesData === null) {
return;
}
$productId = $scriptProperties['productId'];
$row = $scriptProperties['row']; // читаем, не мутируем — для записи нужен returnedValues
$badges = [];
// Бейдж "Новинка"
if (!empty($badgesData['newProducts'][$productId])) {
$badges[] = [
'type' => 'new',
'label' => 'Новинка',
'color' => '#28a745',
];
}
// Бейдж "Скидка" (если есть старая цена)
if (!empty($row['old_price']) && $row['old_price'] > $row['price']) {
$discount = round((($row['old_price'] - $row['price']) / $row['old_price']) * 100);
$badges[] = [
'type' => 'sale',
'label' => "-{$discount}%",
'color' => '#dc3545',
];
}
$modx->event->returnedValues = [
'row' => [
'badges' => $badges,
'badges_json' => json_encode($badges),
'has_badges' => !empty($badges),
],
];
break;
}Использование в шаблоне
{'msProducts' | snippet : [
'parents' => 0,
'usePackages' => 'msBadges',
'tpl' => 'tpl.msProducts.badges'
]}Чанк tpl.msProducts.badges:
<div class="product-card">
{if $has_badges}
<div class="product-badges">
{foreach $badges as $badge}
<span class="badge" style="background: {$badge.color}">
{$badge.label}
</span>
{/foreach}
</div>
{/if}
<h3>{$pagetitle}</h3>
<div class="price">{$price} руб.</div>
</div>Передача данных между событиями
Используйте $modx->eventData для передачи данных между событиями:
// В msOnProductsLoad — сохраняем
$modx->eventData['myPackage'] = [
'dataMap' => $loadedData,
'settings' => $mySettings,
];
// В msOnProductPrepare — читаем
$dataMap = $modx->eventData['myPackage']['dataMap'] ?? null;
$settings = $modx->eventData['myPackage']['settings'] ?? [];Изоляция данных
Используйте имя вашего пакета как ключ в eventData, чтобы избежать конфликтов с другими плагинами.
Производительность
События спроектированы для оптимальной производительности:
- msOnProductsLoad — вызывается один раз для загрузки данных всех товаров
- msOnProductPrepare — вызывается для каждого товара, но только присоединяет уже загруженные данные
Это позволяет избежать проблемы N+1 запросов:
❌ Без bulk-загрузки: 1 запрос на список + N запросов на варианты = O(N+1)
✅ С bulk-загрузкой: 1 запрос на список + 1 запрос на все варианты = O(2)