Сниппеты — это сердце любого проекта на MODX. Они содержат PHP-логику, обрабатывают данные и генерируют динамический контент. За 18 лет работы с MODX я написал сотни сниппетов — от простых до сложных интеграций с API. В этой статье поделюсь практическим опытом создания надёжных и эффективных сниппетов.
Что такое сниппеты в MODX
Определение и назначение
Сниппет — это PHP-код, который выполняется на сервере и возвращает готовый HTML. Это логический слой между данными и представлением.
Принципы работы:
- Получает данные (из базы, API, файлов)
- Обрабатывает данные по бизнес-логике
- Форматирует результат через чанки
- Возвращает готовый HTML
Синтаксис вызова
[[snippet]] <!-- Кэшируемый вызов -->
[[!snippet]] <!-- Некэшируемый вызов -->
[[snippet? ¶m=`value`]] <!-- С параметрами -->
Структура сниппета
<?php
// Получаем параметры
$limit = $scriptProperties['limit'] ?? 10;
$template = $scriptProperties['tpl'] ?? 'default';
// Обрабатываем данные
$results = [];
// ... логика ...
// Возвращаем результат
return implode("\n", $results);
Базовые примеры сниппетов
Пример 1: Список последних статей
Сниппет LatestNews:
<?php
// Получаем параметры
$parent = $scriptProperties['parent'] ?? 1;
$limit = (int)($scriptProperties['limit'] ?? 5);
$tpl = $scriptProperties['tpl'] ?? 'news-item';
$sortBy = $scriptProperties['sortBy'] ?? 'publishedon';
$sortDir = $scriptProperties['sortDir'] ?? 'DESC';
// Получаем статьи
$criteria = $modx->newQuery('modResource');
$criteria->where([
'parent' => $parent,
'published' => 1,
'deleted' => 0,
]);
$criteria->sortby($sortBy, $sortDir);
$criteria->limit($limit);
$articles = $modx->getCollection('modResource', $criteria);
$output = [];
foreach ($articles as $article) {
// Подготавливаем данные для чанка
$data = $article->toArray();
$data['url'] = $modx->makeUrl($article->get('id'));
$data['date_formatted'] = date('d.m.Y', strtotime($data['publishedon']));
// Рендерим чанк
$output[] = $modx->getChunk($tpl, $data);
}
return implode("\n", $output);
?>
Использование:
[[!LatestNews?
&parent=`5`
&limit=`3`
&tpl=`news-item`
]]
Пример 2: Форма обратной связи
Сниппет ContactForm:
<?php
// Проверяем отправку формы
if ($_POST) {
$errors = [];
// Валидация
$name = trim($_POST['name'] ?? '');
$email = trim($_POST['email'] ?? '');
$message = trim($_POST['message'] ?? '');
if (empty($name)) {
$errors['name'] = 'Укажите имя';
}
if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors['email'] = 'Укажите корректный email';
}
if (empty($message)) {
$errors['message'] = 'Введите сообщение';
}
// Если ошибок нет - отправляем
if (empty($errors)) {
$emailBody = "
Новое сообщение с сайта:\n
Имя: {$name}\n
Email: {$email}\n
Сообщение: {$message}\n
";
$sent = mail(
$modx->getOption('emailsender'),
'Новое сообщение с сайта',
$emailBody,
"From: {$email}\r\nReply-To: {$email}"
);
if ($sent) {
return '<div class="alert alert-success">Сообщение отправлено!</div>';
} else {
$errors['general'] = 'Ошибка отправки. Попробуйте позже.';
}
}
// Устанавливаем ошибки в плейсхолдеры
foreach ($errors as $field => $error) {
$modx->setPlaceholder("error.{$field}", $error);
}
// Сохраняем введённые данные
$modx->setPlaceholder('form.name', htmlspecialchars($name));
$modx->setPlaceholder('form.email', htmlspecialchars($email));
$modx->setPlaceholder('form.message', htmlspecialchars($message));
}
// Возвращаем форму
return $modx->getChunk($scriptProperties['tpl'] ?? 'contact-form');
?>
Пример 3: Счётчик просмотров
Сниппет ViewCounter:
<?php
$resourceId = $scriptProperties['id'] ?? $modx->resource->get('id');
$showViews = (bool)($scriptProperties['showViews'] ?? true);
// Получаем текущее количество просмотров
$viewsTV = $modx->getObject('modTemplateVar', ['name' => 'views']);
if (!$viewsTV) {
// Создаём TV-поле если не существует
$viewsTV = $modx->newObject('modTemplateVar');
$viewsTV->set('name', 'views');
$viewsTV->set('caption', 'Количество просмотров');
$viewsTV->set('type', 'number');
$viewsTV->set('default_text', '0');
$viewsTV->save();
}
$currentViews = $viewsTV->getValue($resourceId) ?: 0;
// Увеличиваем счётчик (только для уникальных посещений)
$sessionKey = "viewed_resource_{$resourceId}";
if (!isset($_SESSION[$sessionKey])) {
$newViews = (int)$currentViews + 1;
$viewsTV->setValue($resourceId, $newViews);
$_SESSION[$sessionKey] = true;
$currentViews = $newViews;
}
// Возвращаем количество просмотров
if ($showViews) {
$plural = $modx->runSnippet('declension', [
'count' => $currentViews,
'forms' => 'просмотр,просмотра,просмотров'
]);
return "{$currentViews} {$plural}";
}
return '';
?>
Работа с базой данных
Простые запросы
<?php
// Получение одного ресурса
$resource = $modx->getObject('modResource', 123);
if ($resource) {
echo $resource->get('pagetitle');
}
// Получение нескольких ресурсов
$resources = $modx->getCollection('modResource', [
'parent' => 5,
'published' => 1
]);
foreach ($resources as $resource) {
echo $resource->get('pagetitle') . "\n";
}
?>
Сложные запросы с xPDO
<?php
// Создаём запрос
$query = $modx->newQuery('modResource');
// Условия WHERE
$query->where([
'published' => 1,
'deleted' => 0,
'parent:IN' => [5, 10, 15]
]);
// JOIN с TV-полями
$query->leftJoin('modTemplateVarResource', 'TV', [
'TV.contentid = modResource.id',
'TV.tmplvarid' => 5 // ID TV-поля
]);
// Сортировка
$query->sortby('publishedon', 'DESC');
$query->sortby('menuindex', 'ASC');
// LIMIT и OFFSET
$query->limit(10, 0);
// Выбираем поля
$query->select([
'modResource.*',
'TV.value as tv_value'
]);
// Выполняем запрос
$resources = $modx->getCollection('modResource', $query);
?>
Работа с пользователями
<?php
// Получение текущего пользователя
$user = $modx->getUser();
if ($user && !$user->hasSessionContext('web')) {
return 'Пользователь не авторизован';
}
// Проверка принадлежности к группе
if ($user->isMember('Administrators')) {
echo 'Администратор';
}
// Получение профиля пользователя
$profile = $user->getOne('Profile');
$fullname = $profile->get('fullname');
$email = $profile->get('email');
// Расширенные поля профиля
$extended = $profile->get('extended');
$phone = $extended['phone'] ?? '';
?>
Продвинутые техники
Кэширование в сниппетах
<?php
$cacheKey = 'expensive_operation_' . md5(serialize($scriptProperties));
$cacheExpire = 3600; // 1 час
// Пробуем получить из кэша
$cached = $modx->cacheManager->get($cacheKey);
if ($cached !== null) {
return $cached;
}
// Выполняем дорогую операцию
$result = performExpensiveOperation();
// Сохраняем в кэш
$modx->cacheManager->set($cacheKey, $result, $cacheExpire);
return $result;
function performExpensiveOperation() {
// Имитация тяжёлых вычислений
sleep(2);
return 'Результат сложной операции';
}
?>
Работа с файлами и изображениями
<?php
// Обработка загруженного изображения
if ($_FILES['image']) {
$uploadDir = MODX_ASSETS_PATH . 'images/uploads/';
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
$file = $_FILES['image'];
// Проверяем тип файла
if (!in_array($file['type'], $allowedTypes)) {
return 'Недопустимый тип файла';
}
// Генерируем уникальное имя
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
$filename = uniqid() . '.' . $extension;
$filepath = $uploadDir . $filename;
// Перемещаем файл
if (move_uploaded_file($file['tmp_name'], $filepath)) {
// Создаём миниатюру
$modx->loadClass('modPhpThumb', MODX_CORE_PATH . 'model/phpthumb/', true, true);
$phpthumb = new modPhpThumb($modx);
$phpthumb->config = array_merge($phpthumb->config, [
'allow_src_above_docroot' => true,
'zc' => 1,
'w' => 300,
'h' => 300
]);
$thumb_path = $uploadDir . 'thumb_' . $filename;
$phpthumb->generateThumbnail($filepath, $thumb_path);
return 'Файл загружен: ' . $filename;
}
}
?>
Интеграция с API
<?php
// Получаем данные о погоде
function getWeather($city) {
$apiKey = 'your_api_key';
$url = "http://api.openweathermap.org/data/2.5/weather?q={$city}&appid={$apiKey}&units=metric&lang=ru";
// Кэшируем на 30 минут
$cacheKey = 'weather_' . md5($city);
$cached = $modx->cacheManager->get($cacheKey);
if ($cached !== null) {
return $cached;
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200) {
$data = json_decode($response, true);
$result = [
'temperature' => round($data['main']['temp']),
'description' => $data['weather'][0]['description'],
'humidity' => $data['main']['humidity'],
'icon' => $data['weather'][0]['icon']
];
// Кэшируем на 30 минут
$modx->cacheManager->set($cacheKey, $result, 1800);
return $result;
}
return ['error' => 'Ошибка получения данных о погоде'];
}
// Использование
$city = $scriptProperties['city'] ?? 'Moscow';
$weather = getWeather($city);
if (isset($weather['error'])) {
return $weather['error'];
}
return $modx->getChunk($scriptProperties['tpl'] ?? 'weather', $weather);
?>
Обработка параметров
Валидация и очистка параметров
<?php
// Функция для безопасной обработки параметров
function getParam($props, $key, $default = '', $type = 'string') {
$value = $props[$key] ?? $default;
switch ($type) {
case 'int':
return (int)$value;
case 'bool':
return in_array(strtolower($value), ['1', 'true', 'yes', 'on']);
case 'array':
if (is_string($value)) {
return array_map('trim', explode(',', $value));
}
return is_array($value) ? $value : [];
case 'email':
return filter_var($value, FILTER_VALIDATE_EMAIL) ?: $default;
case 'url':
return filter_var($value, FILTER_VALIDATE_URL) ?: $default;
case 'string':
default:
return is_string($value) ? trim($value) : $default;
}
}
// Использование
$limit = getParam($scriptProperties, 'limit', 10, 'int');
$parents = getParam($scriptProperties, 'parents', [], 'array');
$showImages = getParam($scriptProperties, 'showImages', false, 'bool');
$template = getParam($scriptProperties, 'tpl', 'default');
?>
Конфигурация по умолчанию
<?php
// Настройки по умолчанию
$defaultConfig = [
'limit' => 10,
'offset' => 0,
'parents' => [],
'templates' => [],
'sortBy' => 'publishedon',
'sortDir' => 'DESC',
'includeContent' => false,
'processTVs' => true,
'tpl' => 'item',
'wrapperTpl' => '',
'emptyTpl' => 'empty',
'debug' => false
];
// Объединяем с переданными параметрами
$config = array_merge($defaultConfig, $scriptProperties);
// Дополнительная обработка
$config['limit'] = max(1, min(100, (int)$config['limit'])); // от 1 до 100
$config['parents'] = is_array($config['parents']) ? $config['parents'] : explode(',', $config['parents']);
$config['templates'] = is_array($config['templates']) ? $config['templates'] : explode(',', $config['templates']);
?>
Отладка сниппетов
Логирование
<?php
// Функция для отладочных сообщений
function debugLog($message, $level = 'INFO') {
global $modx;
if ($modx->getOption('log_level') == xPDO::LOG_LEVEL_DEBUG) {
$modx->log($level, '[MySnippet] ' . $message);
}
}
// Использование
debugLog('Сниппет запущен с параметрами: ' . print_r($scriptProperties, true));
try {
$result = doSomething();
debugLog('Операция выполнена успешно');
return $result;
} catch (Exception $e) {
debugLog('Ошибка: ' . $e->getMessage(), 'ERROR');
return 'Произошла ошибка';
}
?>
Режим отладки
<?php
$debug = $scriptProperties['debug'] ?? $modx->getOption('debug', null, false);
if ($debug) {
$startTime = microtime(true);
$startMemory = memory_get_usage();
}
// Основная логика сниппета
$result = performWork();
if ($debug) {
$endTime = microtime(true);
$endMemory = memory_get_usage();
$debugInfo = [
'Время выполнения: ' . round(($endTime - $startTime) * 1000, 2) . ' мс',
'Потребление памяти: ' . round(($endMemory - $startMemory) / 1024, 2) . ' КБ',
'Параметры: ' . print_r($scriptProperties, true)
];
$result .= '<div class="debug-info"><pre>' . implode("\n", $debugInfo) . '</pre></div>';
}
return $result;
?>
Обработка ошибок
<?php
function handleError($message, $showToUser = false) {
global $modx;
// Логируем ошибку
$modx->log(modX::LOG_LEVEL_ERROR, "[MySnippet] {$message}");
if ($showToUser && $modx->getOption('debug')) {
return "<div class='error'>Ошибка: {$message}</div>";
}
return ''; // В продакшене не показываем ошибки пользователям
}
// Обёртка для безопасного выполнения
try {
// Проверяем обязательные параметры
if (empty($scriptProperties['required_param'])) {
throw new Exception('Не указан обязательный параметр required_param');
}
// Выполняем основную логику
$result = mainLogic();
return $result;
} catch (Exception $e) {
return handleError($e->getMessage(), true);
}
?>
Оптимизация производительности
Ленивая загрузка
<?php
class LazySnippet {
private $modx;
private $config;
private $cache = [];
public function __construct($modx, $config) {
$this->modx = $modx;
$this->config = $config;
}
// Ленивое получение ресурсов
public function getResources() {
if (!isset($this->cache['resources'])) {
$query = $this->modx->newQuery('modResource');
// ... настройка запроса
$this->cache['resources'] = $this->modx->getCollection('modResource', $query);
}
return $this->cache['resources'];
}
// Ленивое получение TV-полей
public function getTVs($resourceId) {
$key = "tvs_{$resourceId}";
if (!isset($this->cache[$key])) {
$this->cache[$key] = $this->modx->getObject('modResource', $resourceId)->getTemplateVars();
}
return $this->cache[$key];
}
}
// Использование
$snippet = new LazySnippet($modx, $config);
$resources = $snippet->getResources();
?>
Пакетная обработка
<?php
// Получаем все необходимые ID одним запросом
$resourceIds = [1, 2, 3, 4, 5];
// ❌ Неэффективно - много запросов
/*
foreach ($resourceIds as $id) {
$resource = $modx->getObject('modResource', $id);
// обработка
}
*/
// ✅ Эффективно - один запрос
$resources = $modx->getCollection('modResource', ['id:IN' => $resourceIds]);
// Индексируем по ID для быстрого доступа
$resourcesById = [];
foreach ($resources as $resource) {
$resourcesById[$resource->get('id')] = $resource;
}
// Теперь можем быстро получать ресурсы по ID
foreach ($resourceIds as $id) {
if (isset($resourcesById[$id])) {
$resource = $resourcesById[$id];
// обработка
}
}
?>
Распространённые паттерны
Паттерн “Фабрика чанков”
<?php
class ChunkFactory {
private $modx;
public function __construct($modx) {
$this->modx = $modx;
}
public function render($template, $data, $prefix = '') {
// Добавляем префикс к ключам
if ($prefix) {
$prefixedData = [];
foreach ($data as $key => $value) {
$prefixedData[$prefix . '.' . $key] = $value;
}
$data = $prefixedData;
}
return $this->modx->getChunk($template, $data);
}
public function renderList($items, $template, $wrapper = '', $empty = '') {
if (empty($items)) {
return $wrapper ? $this->modx->getChunk($wrapper, ['items' => $empty]) : $empty;
}
$output = [];
foreach ($items as $item) {
$output[] = $this->render($template, $item);
}
$result = implode("\n", $output);
return $wrapper ? $this->modx->getChunk($wrapper, ['items' => $result]) : $result;
}
}
// Использование
$factory = new ChunkFactory($modx);
return $factory->renderList($articles, 'news-item', 'news-wrapper', 'Новостей нет');
?>
Паттерн “Строитель запросов”
<?php
class QueryBuilder {
private $modx;
private $query;
public function __construct($modx, $class = 'modResource') {
$this->modx = $modx;
$this->query = $modx->newQuery($class);
}
public function where($conditions) {
$this->query->where($conditions);
return $this;
}
public function sort($field, $direction = 'ASC') {
$this->query->sortby($field, $direction);
return $this;
}
public function limit($limit, $offset = 0) {
$this->query->limit($limit, $offset);
return $this;
}
public function joinTV($tvName, $alias = null) {
$alias = $alias ?: $tvName;
$this->query->leftJoin('modTemplateVarResource', $alias, [
"{$alias}.contentid = modResource.id"
]);
$this->query->leftJoin('modTemplateVar', "{$alias}TV", [
"{$alias}TV.id = {$alias}.tmplvarid",
"{$alias}TV.name" => $tvName
]);
return $this;
}
public function get() {
return $this->modx->getCollection('modResource', $this->query);
}
}
// Использование
$builder = new QueryBuilder($modx);
$resources = $builder
->where(['published' => 1])
->joinTV('price')
->sort('publishedon', 'DESC')
->limit(10)
->get();
?>
Заключение
Сниппеты — мощный инструмент для создания динамического функционала в MODX. Основные принципы написания качественных сниппетов:
- Разделяйте логику и представление — PHP в сниппетах, HTML в чанках
- Валидируйте входные данные — никогда не доверяйте пользовательскому вводу
- Используйте кэширование для дорогих операций
- Обрабатывайте ошибки корректно
- Оптимизируйте запросы к базе данных
- Документируйте код и параметры
- Тестируйте в разных условиях
Правильно написанные сниппеты делают MODX мощной платформой для любых задач — от простых сайтов до сложных веб-приложений.
Изучаете элементы MODX? Читайте обзор «Элементы MODX: шаблоны, чанки, сниппеты» — полное руководство по архитектуре системы.
Полезные ресурсы
Чек-лист качественного сниппета
- Валидация всех входных параметров
- Обработка ошибок и исключений
- Оптимизированные запросы к БД
- Кэширование дорогих операций
- Отладочная информация в dev-режиме
- Документация по параметрам
- Безопасность (защита от XSS, SQL-инъекций)
- Совместимость с кэшированием MODX