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

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

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

Привет! Меня зовут Артём, я бэкенд-разработчик. MODX Revolution — CMS с одной из самых гибких архитектур среди open-source решений. Но эта свобода имеет обратную сторону: новичок может написать сниппет, который загружает в память тысячи объектов xPDO, и не заметить этого до момента, когда хостер отключит сайт за превышение лимита памяти.

В этой статье разберу три типичные проблемы с памятью в MODX и покажу, как их правильно решать.

Проблема 1: getCollection загружает ВСЁ

Самый распространённый антипаттерн — использование getCollection для выборки всех ресурсов:

// Каталог с 5000 товарами
$products = $modx->getCollection('modResource', [
    'parent' => 15,
    'published' => 1,
    'template' => 8
]);

foreach ($products as $product) {
    echo $product->get('pagetitle');
    echo $product->getTVValue('price');
}

Метод getCollection создаёт xPDO-объект для каждого ресурса. Каждый объект xPDO содержит: все поля ресурса (30+ полей), все TV-параметры (соединение с таблицей modTemplateVarResource), связи с другими объектами (parent, children, template). При 5000 товаров это означает 5000 полноценных объектов в памяти, каждый из которых весит примерно 20-50KB. Итого: 100-250MB RAM на один вызов сниппета.

На хостинге с лимитом 256MB это оставляет мало места для остального кода. Если на странице ещё 5-6 сниппетов с аналогичными выборками, сервер гарантированно упадёт с PHP Fatal Error: Allowed memory size exhausted.

Решение: getIterator вместо getCollection

xPDO предоставляет метод getIterator, который загружает объекты по одному (или небольшими батчами) из базы данных. Это похоже на cursor в SQL — вы не храните все строки в памяти, а читаете их по мере необходимости:

$iterator = $modx->getIterator('modResource', [
    'parent' => 15,
    'published' => 1,
    'template' => 8
]);

foreach ($iterator as $product) {
    echo $product->get('pagetitle');
    echo $product->getTVValue('price');
    // Объект освобождается после каждой итерации
    unset($product);
}

Расход памяти падает с 100-250MB до 5-10MB, потому что в каждый момент времени в памяти находится только один объект. Производительность немного ниже (больше SQL-запросов), но для каталогов с тысячами товаров это приемлемая жертва.

Проблема 2: getTVValue внутри цикла

Каждый вызов $product->getTVValue('price') в MODX Revolution генерирует отдельный SQL-запрос к таблице modx_site_tmplvar_contentvalues. Если у вас 5000 товаров и для каждого вы вызываете 3 TV-поля, это 15 000 дополнительных запросов. Помимо нагрузки на БД, каждый результат запроса кэшируется внутри xPDO, что снова приводит к росту потребления памяти.

Решение — JOIN TV-значения в основном запросе:

// Получаем TV ID по имени
$tv = $modx->getObject('modTemplateVar', ['name' => 'price']);
$tvId = $tv ? $tv->get('id') : 0;

// Один запрос с JOIN
$query = $modx->newQuery('modResource');
$query->where([
    'parent' => 15,
    'published' => 1,
    'template' => 8
]);
$query->leftJoin('modTemplateVarResource', 'TVPrice', 
    "TVPrice.contentid = modResource.id AND TVPrice.tmplvarid = {$tvId}");
$query->select([
    'modResource.id',
    'modResource.pagetitle',
    'TVPrice.value AS tv_price'
]);

$iterator = $modx->getIterator('modResource', $query);
foreach ($iterator as $product) {
    $row = $product->toArray();
    echo $row['pagetitle'] . ' - ' . $row['tv_price'];
}

Теперь вместо 15 000 запросов у нас один запрос с JOIN. Память — по-прежнему минимальная благодаря итератору. Этот паттерн я использую во всех каталогах, где нужно вывести TV-поля.

Проблема 3: $modx->getCacheManager и парсинг чанков

Третья скрытая утечка памяти — парсинг чанков в цикле. Если вы вызываете $modx->getChunk('productRow', $placeholders) для каждого товара, MODX каждый раз компилирует чанк из базы данных, проверяет условия ([[+price:gt=0]]), обрабатывает фильтры и создаёт временные объекты.

// BAD: getChunk в цикле
foreach ($iterator as $product) {
    $html .= $modx->getChunk('productRow', $product->toArray());
}

// GOOD: fetch single chunk template, parse manually
$tpl = $modx->getObject('modChunk', ['name' => 'productRow']);
$tplContent = $tpl ? $tpl->getContent() : '';

foreach ($iterator as $product) {
    $output = $tplContent;
    foreach ($product->toArray() as $key => $value) {
        $output = str_replace('[[+' . $key . ']]', $value, $output);
    }
    $html .= $output;
}

Второй подход работает в 3-5 раз быстрее, потому что мы один раз загружаем шаблон чанка и вручную подставляем плейсхолдеры. Да, мы теряем поддержку фильтров вывода (:lt, :ellipsis и т.д.), но для критичных участков кода это разумная жертва.

Бонус: PDO::FETCH_MODE для максимальной скорости

Если вам не нужны xPDO-объекты вообще (например, вы формируете JSON для API), можно запросить сырые данные напрямую через PDO:

$stmt = $modx->query("
    SELECT r.id, r.pagetitle, tvr.value AS price
    FROM modx_site_content r
    LEFT JOIN modx_site_tmplvar_contentvalues tvr 
        ON tvr.contentid = r.id AND tvr.tmplvarid = {$tvId}
    WHERE r.parent = 15 AND r.published = 1
    ORDER BY r.id
");

$data = [];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
    $data[] = $row;
}
echo json_encode($data);

Это самый быстрый способ получить данные из MODX — без xPDO, без объектов, без кэша. Расход памяти минимальный. Используйте его для API-эндпоинтов, экспорта данных и AJAX-запросов.

MODX — отличная система, но она требует понимания того, как работает xPDO под капотом. Не используйте getCollection для больших выборок, JOIN-ите TV-значения, и избегайте парсинга чанков в циклах. Эти три правила помогут держать потребление памяти в пределах 20-30MB на страницу даже при большом объёме данных.

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

Комментарии

0
0 / 2000

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

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

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

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

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

25 мая 2026 8 мин

Сложности с разделами каталога в Битрикс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 мин