Привет! Меня зовут Артём, я бэкенд-разработчик. 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 на страницу даже при большом объёме данных.
Пока нет комментариев
Будьте первым, кто оставит комментарий к этой статье