Типовой сниппет — это копипаста без оптимизации
Представьте: ваш каталог товаров на MODX загружается 5 секунд, а каждый клик по категории — вечность. Причина — неоптимизированные сниппеты, которые делают десятки запросов к базе. Типовая ситуация: разработчик скопировал первый попавшийся код, не подумав о кэшировании. Результат — N+1 запросов, падение производительности и негативный пользовательский опыт. Как это исправить? Использовать правильные паттерны.
Разработка сниппета MODX часто начинается с копирования первого попавшегося примера из документации. Результат — N+1 запросов, отсутствие кэширования и уязвимости, которые превращают простую задачу в тормоз для всего сайта. Мы используем проверенные архитектурные паттерны: единый запрос, кэширование и строгую типизацию параметров. Более 5 лет опыта и более 50 проектов на MODX позволяют нам создавать сниппеты, которые выдерживают тысячи запросов без потери производительности.
Как избежать N+1 запросов в MODX?
Проблема N+1 возникает, когда вы получаете коллекцию ресурсов, а затем для каждого подгружаете TV-значения через getTVValue в цикле. При 100 ресурсах это 101 SQL-запрос. Решение — использовать JOIN или pdoFetch. Нативный MODX API позволяет написать JOIN вручную, но pdoTools делает это автоматически и быстрее. Подробнее о проблеме читайте в статье N+1 query problem.\n
Почему важно кэшировать сниппеты?
Кэширование снижает нагрузку на базу данных и ускоряет страницу. Без кэша каждый вызов сниппета — это SQL-запрос. На проекте с каталогом 5000 товаров исходный сниппет выполнялся 4 секунды. После добавления кэширования время упало до 0.2 секунды для последующих запросов. Кэш нужно инвалидировать при изменении данных или устанавливать время жизни, например, 30 минут.
Структура сниппета с безопасной выборкой
Нативный запрос с JOIN
<?php // Сниппет ProductList // Вызов: [[!ProductList? &category=`5` &limit=`12` &sort=`price`]] // Получить параметры (с default значениями) $categoryId = (int)($scriptProperties['category'] ?? 0); $limit = (int)($scriptProperties['limit'] ?? 10); $offset = (int)($scriptProperties['offset'] ?? 0); $sortField = $scriptProperties['sort'] ?? 'menuindex'; $sortDir = $scriptProperties['sortdir'] ?? 'ASC'; $tpl = $scriptProperties['tpl'] ?? 'productCard'; // Запрос к ресурсам через MODX API $c = $modx->newQuery('modResource'); $c->where([ 'parent' => $categoryId, 'published' => 1, 'deleted' => 0, 'class_key' => 'modDocument', ]); // Получить TV значения через Join $c->innerJoin('modTemplateVarResource', 'TVPrice', [ 'TVPrice.tmplvarid' => $modx->getObject('modTemplateVar', ['name' => 'price'])->id, 'TVPrice.contentid = modResource.id', ]); $c->select('modResource.*, TVPrice.value AS price'); $c->sortby($sortField, $sortDir); $c->limit($limit, $offset); $resources = $modx->getCollection('modResource', $c); if (empty($resources)) return ''; $output = ''; foreach ($resources as $resource) { $data = array_merge($resource->toArray(), [ 'price' => $resource->get('price'), 'link' => $modx->makeUrl($resource->id, '', '', 'full'), 'image' => $resource->getTVValue('product_image'), ]); // Чанк для вывода карточки $output .= $modx->getChunk($tpl, $data); } return $output; Сниппет с pdoTools
<?php // Сниппет ProductSearch с pdoTools if (!$modx->loadClass('pdoFetch', MODX_CORE_PATH . 'components/pdotools/model/pdotools/', false, true)) { return 'pdoTools не установлен'; } $pdoFetch = new pdoFetch($modx, $scriptProperties); $pdoFetch->addWhere([ 'modResource.parent' => (int)($scriptProperties['category'] ?? 0), 'modResource.published' => 1, ]); // TV join $pdoFetch->addTVs('price,product_image,short_description'); $result = $pdoFetch->run(); return $result; Кэширование результатов
<?php // Кэшировать результат на 30 минут $cacheKey = 'products_' . md5(json_encode($scriptProperties)); $cacheOptions = [xPDO::OPT_CACHE_KEY => 'default', xPDO::OPT_CACHE_EXPIRES => 1800]; $cached = $modx->cacheManager->get($cacheKey, $cacheOptions); if ($cached !== null) return $cached; // ... запрос ... $output = generateOutput($resources); $modx->cacheManager->set($cacheKey, $output, 1800, $cacheOptions); return $output; Интеграция с внешним API
<?php // Сниппет WeatherWidget — погода из OpenWeatherMap $city = $scriptProperties['city'] ?? 'Moscow'; $apiKey = $modx->getOption('weather_api_key'); $tpl = $scriptProperties['tpl'] ?? 'weatherWidget'; $cacheKey = 'weather_' . $city; $cached = $modx->cacheManager->get($cacheKey, [xPDO::OPT_CACHE_EXPIRES => 1800]); if ($cached !== null) { return $modx->getChunk($tpl, $cached); } $url = "https://api.openweathermap.org/data/2.5/weather?q={$city}&appid={$apiKey}&units=metric&lang=ru"; $response = file_get_contents($url); if (!$response) return ''; $data = json_decode($response, true); if (!$data || $data['cod'] !== 200) return ''; $weather = [ 'city' => $data['name'], 'temp' => round($data['main']['temp']), 'feels_like' => round($data['main']['feels_like']), 'description' => $data['weather'][0]['description'], 'icon' => "https://openweathermap.org/img/wn/{$data['weather'][0]['icon']}@2x.png", 'humidity' => $data['main']['humidity'], ]; $modx->cacheManager->set($cacheKey, $weather, 1800); return $modx->getChunk($tpl, $weather); Какие параметры передавать в сниппет?
[[!ProductList? &category=`[[*id]]` &limit=`12` &tpl=`productCardTpl` &sort=`price` &sortdir=`ASC` ]] ! перед именем — некэшированный вызов (динамический контент). Без ! — кэшированный (статический блок, одинаковый для всех).
Таблица сравнения подходов
| Аспект | Нативный MODX | pdoTools | С кэшированием |
|---|---|---|---|
| Количество SQL-запросов | N+1 (коллекция + TV) | 1 (один JOIN) | 1 раз, затем из кэша |
| Простота написания | Требуется ручной JOIN | Автоматический подгруз TV | Дополнительно 3 строки |
| Производительность на 1000 ресурсов | ~2–3 с | ~0.3–0.5 с | ~0.01 с после первого запроса |
| Поддержка фильтров | Ручное добавление where | Встроенные addWhere | Не влияет |
Таблица типовых параметров сниппета
| Параметр | Тип | Описание | Default |
|---|---|---|---|
| category | int | ID родительского ресурса | 0 |
| limit | int | Количество выводимых записей | 10 |
| sort | string | Поле сортировки | menuindex |
| sortdir | string | Направление сортировки (ASC/DESC) | ASC |
| tpl | string | Имя чанка для вывода | productCard |
Типичные ошибки при разработке сниппетов
- N+1 запросы: получение TV через getTVValue в цикле. Решение — JOIN или pdoFetch.
- Отсутствие фильтрации входных данных: параметры без приведения типа (категория — строка, а не int).
- Игнорирование кэширования: каждый вызов сниппета грузит БД, даже если данные не изменились.
- Жёстко зашитые ID ресурсов: используйте плейсхолдеры и параметры.
Чек-лист: как не допустить ошибок
- [ ] Использовать prepared statements
- [ ] Приводить все входные параметры к нужному типу
- [ ] Добавить кэширование с уникальным ключом
- [ ] Использовать pdoFetch для сложных выборок
- [ ] Документировать параметры и чанки
Что входит в работу
- Исходный код сниппета с комментариями.
- Документация: описание параметров, примеры вызова, описание логики.
- Настройка кэширования: подбор времени жизни и ключа.
- Инструкция по установке: добавление сниппета, создание чанков, тестирование.
- Гарантия поддержки: если возникнут вопросы, мы поможем в течение месяца после сдачи.
Процесс разработки сниппета под ключ
- Аналитика: изучаем ТЗ, какие данные выводить, откуда брать, условия фильтрации.
- Проектирование: определяем структуру запроса, выбираем метод (нативный или pdoTools), продумываем кэширование.
- Реализация: пишем PHP-код сниппета и чанки шаблонов.
- Тестирование: проверяем на крайних значениях (пустые категории, 0 записей), замеряем время выполнения.
- Деплой: размещаем на боевом сервере, настраиваем кэш, даём документацию по вызову.
Сроки и стоимость
Сроки: от 0,5 дня для простого сниппета до 5 дней для сложного с интеграцией API. Точную оценку даём после знакомства с проектом. Стоимость рассчитывается индивидуально.
Свяжитесь с нами для обсуждения вашего проекта. Закажите разработку сниппета прямо сейчас — получите быстрый и безопасный код. Опыт разработки под MODX — более 5 лет. Пишите, и мы оценим задачу и предложим оптимальное решение.







