Typical snippet is unoptimized copy-paste
Imagine your MODX product catalog takes 5 seconds to load, and every category click feels like an eternity. The cause: unoptimized snippets that make dozens of database queries. Typical scenario: a developer copied the first code they found, without thinking about caching. Result: N+1 queries, performance drop, and poor user experience. How to fix it? Use correct patterns.
MODX snippet development often starts with copying the first example from the documentation. The result: N+1 queries, no caching, and vulnerabilities that turn a simple task into a site-wide bottleneck. We use proven architectural patterns: single query, caching, and strict parameter typing. Over 5 years of experience and more than 50 projects on MODX allow us to create snippets that withstand thousands of requests without performance loss.
How to avoid N+1 queries in MODX?
The N+1 problem occurs when you retrieve a collection of resources and then for each one load TV values via getTVValue in a loop. With 100 resources, that's 101 SQL queries. The solution: use JOIN or pdoFetch. The native MODX API allows manual JOIN, but pdoTools does it automatically and faster. More about the problem at N+1 query problem.
Why is snippet caching important?
Caching reduces database load and speeds up pages. Without cache, every snippet call means an SQL query. On a project with a 5000-item catalog, the original snippet took 4 seconds. After adding caching, the time dropped to 0.2 seconds for subsequent requests. Cache should be invalidated when data changes or set a TTL, e.g., 30 minutes.
Snippet structure with safe querying
Native query with JOIN
<?php // Snippet ProductList // Call: [[!ProductList? &category=`5` &limit=`12` &sort=`price`]] // Get parameters (with defaults) $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'; // Query to resources via MODX API $c = $modx->newQuery('modResource'); $c->where([ 'parent' => $categoryId, 'published' => 1, 'deleted' => 0, 'class_key' => 'modDocument', ]); // Get TV values via 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'), ]); // Chunk for card output $output .= $modx->getChunk($tpl, $data); } return $output; Snippet with pdoTools
<?php // Snippet ProductSearch with pdoTools if (!$modx->loadClass('pdoFetch', MODX_CORE_PATH . 'components/pdotools/model/pdotools/', false, true)) { return 'pdoTools is not installed'; } $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; Caching results
<?php // Cache result for 30 minutes $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; // ... query ... $output = generateOutput($resources); $modx->cacheManager->set($cacheKey, $output, 1800, $cacheOptions); return $output; Integration with external API
<?php // Snippet WeatherWidget — weather from 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); Which parameters to pass to a snippet?
[[!ProductList? &category=`[[*id]]` &limit=`12` &tpl=`productCardTpl` &sort=`price` &sortdir=`ASC` ]] ! before the name — uncached call (dynamic content). Without ! — cached (static block, same for all).
Approach comparison table
| Aspect | Native MODX | pdoTools | With caching |
|---|---|---|---|
| Number of SQL queries | N+1 (collection + TV) | 1 (single JOIN) | 1 time, then from cache |
| Ease of writing | Requires manual JOIN | Automatic TV loading | Additional 3 lines |
| Performance on 1000 resources | ~2-3 s | ~0.3-0.5 s | ~0.01 s after first request |
| Filter support | Manual where addition | Built-in addWhere | Not affected |
Typical snippet parameters table
| Parameter | Type | Description | Default |
|---|---|---|---|
| category | int | ID of parent resource | 0 |
| limit | int | Number of records to display | 10 |
| sort | string | Sort field | menuindex |
| sortdir | string | Sort direction (ASC/DESC) | ASC |
| tpl | string | Name of chunk for output | productCard |
Common mistakes in snippet development
- N+1 queries: getting TV via getTVValue in a loop. Solution: JOIN or pdoFetch.
- Lack of input filtering: parameters without type casting (category as string instead of int).
- Ignoring caching: each snippet call loads the DB, even if data hasn't changed.
- Hardcoded resource IDs: use placeholders and parameters.
Checklist: how to avoid mistakes
- [ ] Use prepared statements
- [ ] Cast all input parameters to required type
- [ ] Add caching with a unique key
- [ ] Use pdoFetch for complex queries
- [ ] Document parameters and chunks
What's included in the work
- Snippet source code with comments.
- Documentation: parameter descriptions, call examples, logic description.
- Caching setup: TTL and key selection.
- Installation instructions: adding the snippet, creating chunks, testing.
- Support guarantee: we'll help with any questions within a month after delivery.
Turnkey snippet development process
- Analysis: study the requirements, what data to output, from where, filtering conditions.
- Design: determine query structure, choose method (native or pdoTools), plan caching.
- Implementation: write PHP snippet code and template chunks.
- Testing: check edge cases (empty categories, 0 records), measure execution time.
- Deployment: place on production server, configure cache, provide call documentation.
Timeline and cost
Timeline: from 0.5 day for a simple snippet to 5 days for a complex one with API integration. We provide an accurate estimate after reviewing the project. Cost is calculated individually.
Contact us to discuss your project. Order snippet development right now — get fast and secure code. Over 5 years of experience with MODX. Reach out, and we'll evaluate the task and propose the optimal solution.







