2014-05-15

Оптимизация чтения ресурсов MODX Revolution

Однажды передо мной встала задача по аудиту и оптимизации сайта с большим количеством страниц на MODX Revolution. Сайт периодически "падал" и анализ запросов с помощью тайминга MODX показал очень плохие результаты - в момент генерации кеша сайт выполнял порядка 900-1000 запросов к базе данных. При этом, при созданном кеше, на каждой странице продолжало выполняться порядка 100 запросов из-за использования в некоторых местах некешируемых вызовов сниппетов и чанков.

Тайминг

В MODX есть специальные некешируемые теги для измерения производительности:
[^qt^] - Время запросов к БД
[^q^] - Количество запросов к БД
[^p^] - Время парсинга страницы
[^t^] - Итоговое время парсинга/генерации страницы
[^s^] - Источник: база данных или кеш

Генерация меню

Создание меню было возложено на Wayfinder. В первую очередь, я включил параметр hideSubMenus, но это не помогло разогнать сайт и запросов все равно было много. Затем был испытан getResources, но он так же делал много запросов.
Это популярные сниппеты для чтения ресурсов, удобные, настраиваемые и даже кеширующие данные, но все же изучение их параметров и попытки настроить не помогли избавиться от кучи запросов, так что было принято решение написать свое простое оптимальное решение.

Оптимизация

Как известно, из mysql оптимальней извлекать порции данных одним запросом, чем по отдельности.
Будем использовать классы modResource, modTemplateVar и modTemplateVarTemplate, соответственно для чтения ресурсов и tv-параметров.

получаем значения переменной по имени для всех ресурсов:

$tv = $modx->getObject('modTemplateVar', array('name'=>'test2'));
// или так можно получить несколько переменных:
// $tvs = $modx->getCollection('modTemplateVar', array('name:in'=>array('test1', 'test2')));
// получаем значения переменной для всех ресурсов
$rs = $tv->getMany('TemplateVarResources');
foreach ($rs as $r)
{
    // id, tmplvarid, contentid и value
    echo $r->get('value').', ';
}

получаем значения переменной по id для всех ресурсов:

$tvrs = $modx->getCollection('modTemplateVarResource', array('tmplvarid' => 2));
foreach ($tvrs as $tvr)
{
    echo $tvr->get('value').', ';
}

получаем ресурсы, при желании их можно отфильтровать по значениям tv-переменных:

$q = $modx->newQuery('modResource');
$q->where(array(
    'context_key' => 'web',
    //'context_key:in' => array('web', 'mgr'),
));
// после подключения таблицы с tv-переменными, мы можем отфильтровать ресурсы с помощью $q->where(...)
//$q->leftJoin('modTemplateVarResource', 'tvr', 'tvr.contentid = modResource.id');
$ress = $modx->getCollection('modResource', $q);

получение связанных объектов:

$criteria = array();
$criteria['TemplateVarResources.tmplvarid'] = 2;
/*
$criteria = $modx->newQuery('modResource', $criteria);
$criteria->prepare();
print $criteria->toSQL()."\n";
*/
$ress = $modx->getCollectionGraph('modResource', '{"TemplateVarResources":{}}', $criteria);
foreach ($ress as $res) {
    foreach ($res->getMany('TemplateVarResources') as $tvr) {
        echo $tvr->get('value').', ';
    }
}

чтение дерева дочерних ресурсов с помощью xPDOCriteria и связанных tv-переменных:

$res_criteria = array(
    'deleted' => '0',
    'published' => '1',
    'hidemenu' => '0',
);
// $child_ids - массив id ресурсов для извлечения
$res_criteria = $modx->newQuery('modResource', $res_criteria);
$res_criteria->where(array('modResource.id:IN' => $child_ids));
$res_criteria->sortby('modResource.menuindex', 'ASC');
$ress = $modx->getCollection('modResource', $res_criteria);
$tree = array();
$ids = array();
foreach ($ress as $res)
{
    $tree[$res->get('parent')][$res->get('id')] = $res->toArray();
    $ids[$res->get('id')] = $res->get('id');
}

$tv_name = 'img';
$tv_criteria = array(
    'tmplvarid' => 3, // чтение по id tv-переменной
    'contentid:IN' => $ids,
);
//$tv_criteria = $modx->newQuery('modTemplateVarResource', $tv_criteria);
$tvrs = $modx->getCollection('modTemplateVarResource', $tv_criteria);
$tvs = array();
foreach ($tvrs as $tvr)
{
    $tvs[$tvr->get('contentid')][$tv_name] = $tvr->get('value');
}

В итоге, приведенные выше запросы позволяют резко сократить кол-во запросов к базе данных на странице и, следовательно, улучшить отзывчивость сайта.

Полный исходный код снипета для быстрой генерации меню размещен на github https://github.com/asvavilov/modx-tree