Типовий снипет — це копіпаста без оптимізації
Уявіть: ваш каталог товарів на 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.
Чому важливо кешувати снипети?
Кешування зменшує навантаження на базу даних і прискорює сторінку. Без кешу кожен виклик снипета — це 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=ua"; $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 років. Пишіть, і ми оцінимо завдання та запропонуємо оптимальне рішення.







