Все, что вы хотели знать о процессорах, но боялись спросить
Про процессоры у нас написано много, но вот этот комментарий натолкнул меня на мысль, что некоторые вообще не представляют себе что такое MODX-процессоры и с чем их едят. Учитывая то, что практически все наши разработки (в том числе и сборка ShopModxBox) основываются на работе процессоров, я решил написать эту статью, в которой постараюсь максимально подробно раскрыть тему процессоров. Если вы не понимаете процессоров, у вас никогда не получится нормально тюнинговать сборку ShopModxBox под себя, так что советую максимально четко изучить данный материал (для этого будет приведено множество примеров). Обязательно попробуйте выполнить представленные примеры самостоятельно и понять как они работают. Освоите — многое для вас станет понятней и проще.
Сразу скажу, что понимание принципов ООП сильно поможет вам в освоении этого материала, так что если у кого пока нет знаний в php-ООП, советую к изучению вот эту страничку.
Для начала выполним простейший скрипт (здесь и далее скрипты выполнять будем в компоненте Console в админке ShopModxBox (чтобы точно все примеры работали)).
<?php
print '<pre>';
ini_set('display_errors', 1);
$modx->switchContext('web');
$action = 'web/catalog/products/getdata';
$ns = 'modxsite';
$params = array(
"limit" => 6,
);
if(!$response = $modx->runProcessor($action,
$params
, array(
'processors_path' => $modx->getObject('modNamespace', $ns)->getCorePath().'processors/',
))){
print "Не удалось выполнить процессор";
return;
}
print_r($response->getResponse());
В ответ мы получим примерно такой ответ:
Array
(
[success] => 1
[message] =>
[count] => 6
[total] => 6
[limit] => 6
[page] => 0
[object] => Array
(
[134] => Array
(
[id] => 134
[type] => document
[contentType] => text/html
[pagetitle] => Toshiba Satellite C50-A-K7K 15,6"
[longtitle] => .......................
Наблюдательные читатели могли заметить схожесть параметров в примере с вызовом процессора в смарти, а именно:
$action = 'web/catalog/products/getdata';
$ns = 'modxsite';
$params = array(
"limit" => 6,
);
и
{processor action="web/catalog/products/getdata" ns="modxsite" params="limit=`6`" assign=result}
И это не случайно. Фактически, вызывая процессор в Смарти, мы вызываем представленный код, а точнее метод $modx->runProcessor($processor, $params, $options);
Так или иначе, и в одном и в другом случае мы получим ответ в одном и том же формате — массиве (на самом деле ответ может быть не только в виде массива, но мы будем рассматривать здесь стандартный ответ).
Для начала разберем вызов процессора.
$action = 'web/catalog/products/hot/getdata'; $action — это путь до вызываемого процессора. В нашем случае это вот этот процессор. На то, в какой папке будет выполняться поиск процессора, отвечает элемент processors_path, который содержит путь до папки процессоров.
Здесь для нас важную роль играет параметр $ns = 'modxsite'; Это название пространства имен MODX-а (как правило пространства имен создаются автоматически при установке компонентов, а управление ими доступно в главном меню Настройки — Пространства имен).
В нашем случае мы получаем путь до процессоров неймспейса modxsite (в целом, можно использовать синоним Компонент, но всетаки компонент и неймспейс — это не одно и то же). Бывает, что у нас вызываются процессоры и из других компонентов, например здесь вызывается процессор из компонента basket, который, как наверняка многие знают, отвечает за работу корзины.
И третий параметр — $params, который содержит передаваемые в процессор параметры. Внутри процессора эти параметры будут доступны через метод $this->getProperty() или в массиве $this->properties. В нашем случае, передав в процессор параметр 'limit' => 6, мы ему «указали», что надо получить максимум 6 записей.
А теперь попробуйте в своем Смарти-шаблоне прописать такой код:
{processor action="web/catalog/products/getdata" ns="modxsite" params="limit=`6`" assign=result}
<pre>
{print_r($result, true)}
</pre>
Теперь на своей странице, где будет вызван этот код, вы получите такой же ответ, как и в админке в консоли. Вот именно с этим ответом чаще всего и приходится работать, в нем содержится информация о полученных данных и их, как правило, мы используем для того же вывода списка товаров.
Давайте рассмотрим данные ответа.
Сначала стандартные элементы:
- success => 0 || 1. Флаг успешного или не успешного выполнения процессора. Под не успешным подразумевается случай, когда процессор выполнен, но в нем возникли ошибки (например, мы прописали проверку каких-то обязательных полей, и обрабатывая запрос при отсутствии необходимых данных возвращаем ошибку с сообщением заполнить необходимые поля).
- message. Сообщение, возвращаемое процессором (чаще всего методами
$this->success($message)или$this->failure($message)). Сообщение может отсутствовать. - count — количество полученных записей.
- total — количество всего записей, соответствующих условиям выборки данных.
- limit — лимит на количество получаемых записей.
- object — массив данных полученных записей.
Это были стандартные поля, которые как правило возвращаются любым MODX-процессором. Параметр page — это уже добавленный нами в процессоры компонента modxsite, чтобы удобней было работать с постраничностью. Общее количество записей (total), количество записей на одну страницу (limit) и номер текущей страницы (page) — это все то, что нам нужно знать для формирования постраничности. К слову, если вызывать шаблон постраничности pagination.tpl, передав в него полученный ответ процессора $result, вам будет сформирован HTML-код постраничности. Пример реализации можно подсмотреть здесь.
Так откуда же растут ноги у этих процессоров?
На самом деле все MODX-процессоры так или иначе берут начало от главного класса — modProcessor. Все базовые MODX-процессоры прописаны в modprocessor.class.php. Перечислим их:
-
modProcessor. Этот класс является абстрактным (объявлен как
abstract class modProcessor), то есть его нельзя вызывать напрямую, можно только расширить его другим классом и вызывать уже тот класс. В нем прописаны все базовые методы. Разберем основные:- setProperty(v). Устанавливает свойства процессора (добавляет в массив
$this->propertiesпеременные со своими значениями (за один вызов только одно значение)). properties = $this->getProperties(); - getProperty(default = null). Получает свойство процессора.
- **unsetProperty(key)**. [Удаляет](https://github.com/modxcms/revolution/blob/8efb61f5d1bb30c5df2ed9eba803aa5b4e805774/core/model/modx/modProcessor.class.php#L66) свойство процессора. Это имеет смысл когда вы хотите избежать переопределение какого-либо свойства объекта. Дело в том, что все передаваемые в процессор параметры попадает в его свойства `this->properties
, и, к примеру, если у вас выполняется обновление объекта в рамках update-процессора [modObjectUpdateProcessor](https://github.com/modxcms/revolution/blob/8efb61f5d1bb30c5df2ed9eba803aa5b4e805774/core/model/modx/modprocessor.class.php#L757) (который мы чуть подробней рассмотрим ниже), в свойства полученного объекта передаются полученные свойства процессора. К примеру, если вызовем процессор на обновление документа так:this->unsetProperty('pagetitle');` Таким образом даже если кто-то и передаст в запрос параметр pagetitle, он будет удален из параметров. - setProperties($properties). Устанавливает сразу несколько свойств процессора.
- getProperties(). Получает все параметры процессора (на самом деле возвращает массив
$this->properties). - **setDefaultProperties(array properties)](https://github.com/modxcms/revolution/blob/8efb61f5d1bb30c5df2ed9eba803aa5b4e805774/core/model/modx/modProcessor.class.php#L232), устанавливает параметры процессора, но с той лишь разницей, что он не замещает уже имеющиеся параметры. То есть если, к примеру, при вызове процессора был передан параметр sort, а в процессоре вызывается
$this->setDefaultProperties(array('sort' => 'id',));, то переданный параметр sort не будет затерт устанавливаемым дефолтным значением. - checkPermissions() Проверяет доступ к вызываемому процессору. К примеру, в нем можно прописать так:
public function checkPermissions() { return $this->modx->user->id && parent::checkPermissions(); }Таким образом только если пользователь будет авторизован (объект пользователя
$modx->userсодержит значение id), а так же родительский процессор вернет успех на проверкуcheckPermissions(), тогда только будет возвращеноtrue, и значит процессор может выполняться. Иначе будет возвращеноAccess denied.- initialize(). В этом методе как правило прописывается проверка передаваемых данных, установка дефолтовых значений и т.п. В нем позволительно возвращать не только
true|false, но и просто текстовое сообщение. Данное сообщение будет так же расцениваться как ошибка и будет содержаться в параметреmessageописанного выше формата ответа. Рассмотрим довольно типичный код:
public function initialize(){ $this->setDefaultProperties(array( "subject" => "Заказ звонка с сайта", )); if(!$this->getProperty('name')){ $this->addFieldError('name', 'Не указано имя'); } if(!$this->getProperty('phone')){ $this->addFieldError('phone', 'Не указан телефон'); } // Если есть ошибки, возвращаем ошибку с сообщением if($this->hasErrors()){ return "Не все обязательные поля заполнены"; } return parent::initiaize(); }Здесь мы установили значение по умолчанию «subject» => «Заказ звонка с сайта» (Оно может быть переопределено входящим параметром в вызове процессора или в методе
initialize()расширяющего процессора), затем проверили значения name и phone, чтобы были заполнены, и в случае если какой-то из них не заполнен ($this->addFieldError()добавляет ошибку, а$this->hasErrors()проверяет есть ли ошибки в процессоре), вернули ошибку.- success(object = null). Возвращает успешный ответ выполнения процессора (в массиве ответа тогда параметр success содержит true).
- failure(object = null). Возвращает ошибку.
Методы success() и failure() как правило возвращаются в методе process().
- hasErrors() как и говорилось выше, проверяет есть ли ошибка в процессоре или нет.
Здесь есть одно очень важное замечание: как я уже не раз говорил, у процессоров нет собственного объекта обработки ошибок, поэтому, если у вас вазывается сразу несколько процессоров, и в каком-то из них будет ошибка, то все последующие процессоры при проверке
$this->hasErrors()будут возвращать ошибку, так как на все один единый объект —$modx->error. Так что желательно после вызова процессора выполнять сброс ошибок$modx->error->reset(). В случае, если процессор вызывается в Smarty-шаблоне, такой сброс ошибок не требуется, так как сброс прописан в самом смарти-плагине (modxSmarty v1.0.0+).- addFieldError(message = ''). Добавляет множественное сообщение об ошибке (удобно, к примеру, при проверке нескольких полей формы).
- getLanguageTopics(). Здесь мы можем указать массив словарей, которые нужно будет MODX-у инициализировать перед выполнением процессора.
- process(). Этот метод в базовом классе абстрактный, что обязывает в расширяющих процессорах прописать собственный метод
process()с пользовательским кодом. Если этот метод не прописать, будет возвращена фатальная ошибка. К примеру, в расширяющем его процессоре modObjectCreateProcessor прописан свой методprocess(), так что если ваш процессор расширяет его, то методprocess()уже не обязательно прописывать. - run(). Это самый главный метод процессора, который задает общую логику работы процессора, выполняя основные его методы в нужном порядке и обрабатывая ошибки. Давайте рассмотрим его код внимательней с комментариями.
- setProperty(v). Устанавливает свойства процессора (добавляет в массив
public function run() {
// Проверяем права на выполнение
if (!$this->checkPermissions()) {
// Если прав нет, ответ будет содержать ошибку доступа
$o = $this->failure($this->modx->lexicon('permission_denied'));
// Права есть, выполняем основной код
} else {
// Получаем массив словарей, если указан
$topics = $this->getLanguageTopics();
foreach ($topics as $topic) {
// Подгружаем словари
$this->modx->lexicon->load($topic);
}
// Выполняем инициализацию процессора
$initialized = $this->initialize();
// Если инициализация не вернула четко истину,
// то ответ будет содержать ошибку
if ($initialized !== true) {
$o = $this->failure($initialized);
// иначе успех
}
else {
$o = $this->process();
}
}
// Получаем объект ответа процессора
$response = new modProcessorResponse($this->modx,$o);
// Возвращаем ответ
return $response;
}
Этот базовый метод задает стандарт выполнения любого MODX-процессора и его ответа. Метод run() в принципе не принято переопределять (это тот один из немногих случаев, когда я бы методу задал атрибут final).
Вот, собственно, отсюда и пляшут все расширяющие процессоры. Давайте продолжим перечислять основные.
- modObjectProcessor расширяет modProcessor и так же является абстрактным классом, то есть его нельзя вызывать напрямую. Он устанавливаем базовые свойства для нескольких типовых дочерних классов, выполняющими действия с xPDO-объектами:
- modObjectGetListProcessor. Получает массив xPDO-объектов (к примеру, массив пользователей).
- modObjectCreateProcessor. Создает xPDO-объект (к примеру, новый документ).
- modObjectUpdateProcessor. Обновляет xPDO-объект.
- modObjectDuplicateProcessor. Создает копию xPDO-объекта.
- modObjectRemoveProcessor. Удаляет xPDO-объект.
- modObjectSoftRemoveProcessor. Обновляет xPDO-объект, отмечая его как удаленный (устанавливает свойства
deleted => 1). - modObjectExportProcessor Экспорт xPDO-объекта в XML (для последующего скачивания).
- modObjectImportProcessor. Импорт xPDO-объекта их XML.
Для всех Object-процессоров важен параметр $classKey, который должен содержать название xPDO-класса (в дальнейшем объекта). К примеру, если вы вызываете modObjectCreateProcessor, который должен в итоге создать новый объект пользователя (modUser), то надо в этом параметре прописать значение 'modUser'. Смотрите как это сделано в системном modUserCreateProcessor.
К слову, последние два процессора помогли бы вот в этом вопросе, но, к сожалению, в этих процессорах не прописана подгрузка зависимых объектов, что не позволяет выгрузить документ товара вместе со всеми данными товара. На досуге подумаю на счет такого механизма.
Советую посмотреть вот этот ресурс: fossies.org/dox/modx-2.3.3-pl/modprocessor_8class_8php.html

Вообще, если освоить навигацию по тому ресурсу и хорошенько покопаться, можно много всего найти и узнать. К примеру, можно ощутить, что MODX со своими процессорами — это целая вселенная!
Здесь видна лишь маленькая часть имеющихся в MODX-е процессоров, которые так или иначе все являются предками главного класса modProcessor. Это процессоры, которые отвечают за создание/редактирование/удаление и т.п. практически всех сущностей в нем (документы, пользователи, контексты, настройки, ТВ-параметры и т.д. и т.п.). Это реально очень мощный механизм, который никак нельзя обходить стороной. Любителям сниппетов я бы сказал так: сам механизм управления сущностями в MODX-е не построен на сниппетах или типа того. Только процессоры. Поэтому если вы хотите разрабатывать действительно мощные веб-проекты, без процессоров вам никак не обойтись (хотя правильней сказать одними сниппетами вам не обойтись).
Ну а теперь рассмотрим процессоры из компонента modxsite (напомню, что практически все наши getdata-процессоры в сборке основаны на них).
Самый основной из них — modSiteWebGetlistProcessor. Он расширяет родной MODX-овый процессор modObjectGetListProcessor, но несколько переопределяет и дополняет его логику работы. К примеру, в нем предусмотрено кеширование результатов выборки. Если передать в вызов параметр cache => 1, то результаты выборки будут закешированы, и при повторном вызове данные будут браться из кеша, а не опять формировать запрос к базе данных, выполнять его и обрабатывать. Тут оговорюсь, что в формировании ключа кеша учитываются все входящие параметры процессора, так что для запросов, к примеру, с разными параметрами page будут сформированы разные кеш-результаты. В целом этот процессор можно особо не копать (достаточно просто знать что он есть, какие параметры принимает и что возвращает), ибо логика в нем местами запутанная, но если у вас хорошие знания php и вы планируете создавать не один проект на базе ShopModxBox, то тогда поковырять его хорошенько будет очень даже полезно.
Второй процессор — modSiteWebGetdataProcessor, расширяющий modSiteWebGetlistProcessor. Этот процессор, в отличие от своего родителя (который сам по себе на самом деле редко используется), оперирует не с xPDO-объектами, а с чистыми данными из таблицы указанного в $classKey классе. Объясню: modSiteWebGetlistProcessor получает коллекцию объектов методом $modx->modx->getCollection(), то есть не просто получает данные из БД, а создает на основе этих данных xPDO-объекты. Это может быть необходимо, чтобы проверить права пользователя на эти объекты средствами MODX-а, которые требуют наличия самого объекта (а не данных его в БД, на основе которых на самом деле на уровне БД нельзя выполнить проверки (во всяком случае родной механизм MODX-а этого не предусматривает)). Но инициализация объектов требует во много раз больше ресурсов, чем просто получить данные этих объектов из БД, поэтому чаще всего мы используем именно getdata-процессор, если нам нужны просто данные записей и мы знаем заранее, что они не требуют специальных проверок на доступ (к примеру, если каталог публичный, без всяких лишних требований к пользователям, то нам просто надо получить данные этих записей и все. Без инициализации объектов все будет выполнено гораздо быстрее).
Другое важное отличие getdata-процессора от getlist-процессора — это обработка множественных записей TV-полей (в случае выполнения выборки их в запросе) и набивка этих данных в уникальный элемент документа в общем массиве данных. Просто, насколько наверняка многим известно, данные TV-параметров страниц находятся в отдельной таблице от документов и связь их один-ко-многим, то есть на одну запись документа может содержаться несколько записей TV-полей. Здесь все эти данные TV-полей будут набиты в массивы tvs для каждого документа в отдельности.
В скором времени скорее всего этот блок кода перекочует в свой, более узкопрофильный процессор для получения документов — modSiteWebResourcesGetdataProcessor
А теперь давайте закрепим наши теоретические знания практическими, выполнив несколько упражнений.
1. Создадим новый документ. Для этого воспользуемся родным MODX-процессором resource/create. Все исполняемые процессоры самого MODX-а находятся в папке MODX_PROCESSORS_PATH
<?php
print '<pre>';
ini_set('display_errors', 1);
$modx->switchContext('web');
$modx->setLogTarget('HTML');
$action = 'resource/create';
$ns = '';
$params = array(
"pagetitle" => "New document",
"content" => "some content",
);
if(!$response = $modx->runProcessor($action,
$params
, array(
'processors_path' => $ns ? $modx->getObject('modNamespace', $ns)->getCorePath().'processors/' : null,
))){
print "Не удалось выполнить процессор";
return;
}
print_r($response->getResponse());
Выполним его. Если у вас все хорошо выполнилось, вы получите ответ, содержащий success => 1 и id созданного документ, типа такого:
Array
(
[success] => 1
[message] =>
[total] => 0
[errors] => Array
(
)
[object] => Array
(
[id] => 155
)
)
Иначе будет ошибка, например такая:
Array
(
[success] =>
[message] =>
[total] => 2
[errors] => Array
(
[0] => Array
(
[id] => uri
[msg] => Ресурс с идентификатором 155 уже использует URI new-document.html. Пожалуйста, введите уникальный псевдоним или используйте «Заморозить URI», чтобы вручную заменить его.
)
[1] => Array
(
[id] => alias
[msg] => Ресурс с идентификатором 155 уже использует URI new-document.html. Пожалуйста, введите уникальный псевдоним или используйте «Заморозить URI», чтобы вручную заменить его.
)
)
[object] => Array
(
)
)
2. Обновим существующий документ.
Для этого нам надо будет вызвать процессор resource/update. Единственно, в случае с обновлением документа не достаточно будет передать только его id-шник и изменяемые поля, так как в процессоре часть кода основывается на передаваемых данных, а не на данных полученного объекта документа, но это не беда.
<?php
print '<pre>';
ini_set('display_errors', 1);
$modx->switchContext('web');
$modx->setLogTarget('HTML');
$action = 'resource/update';
$ns = '';
$doc_id = 1;
$params = array_merge($modx->getObject('modResource', $doc_id)->toArray(), array(
"pagetitle" => "New pagetitle",
));
if(!$response = $modx->runProcessor($action,
$params
, array(
'processors_path' => $ns ? $modx->getObject('modNamespace', $ns)->getCorePath().'processors/' : null,
))){
print "Не удалось выполнить процессор";
return;
}
print_r($response->getResponse());
3. Получим данные товаров в каталоге Для этого вызовем процессор web/catalog/products/getdata самой сборки ShopModxBox.
<?php
print '<pre>';
ini_set('display_errors', 1);
$modx->switchContext('web');
$modx->setLogTarget('HTML');
$action = 'web/catalog/products/getdata';
$ns = 'modxsite';
$params = array(
"limit" => 3,
"sort" => "createdon",
"dir" => "desc",
);
if(!$response = $modx->runProcessor($action,
$params
, array(
'processors_path' => $ns ? $modx->getObject('modNamespace', $ns)->getCorePath().'processors/' : null,
))){
print "Не удалось выполнить процессор";
return;
}
print_r($response->getResponse());
В ответ вы должны получить массив данных товаров (максимум трех по условию запроса), отсортированных по дате создания в обратном порядке.
На этом на сегодня все. А в качестве домашнего задания попробуйте проследить всю родословную процессора из последнего примера, и понять как формируется запрос на получение данных и какие таблицы он затрагивает.
