Все, что вы хотели знать о процессорах, но боялись спросить

Про процессоры у нас написано много, но вот этот комментарий натолкнул меня на мысль, что некоторые вообще не представляют себе что такое 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(k,k,v). Устанавливает свойства процессора (добавляет в массив $this->properties переменные со своими значениями (за один вызов только одно значение)). thisвнутриобъекта—этомагическаяпеременная,ссылающаясянасамобъект.Кпримеру,есливыхотитевнутрипроцессораполучитьегосвойства,вывнемпрописываетеthis внутри объекта — это магическая переменная, ссылающаяся на сам объект. К примеру, если вы хотите внутри процессора получить его свойства, вы в нем прописываете `properties = $this->getProperties();
    • getProperty(k,k,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) (который мы чуть подробней рассмотрим ниже), в свойства полученного объекта передаются полученные свойства процессора. К примеру, если вызовем процессор на обновление документа так: modx>runProcessor(resource/update,array(id=>1,pagetitle=>newpagetitle));,тобудетполученобъектдокументасid=>1,иегозаголовок(pagetitle)измененнапереданныйвпроцессорпараметрpagetitle=>newpagetitle.Таквот,еслимывсвоемпроцессорехотимизбежатьтого,чтоктотопередастновыйзаголовокиизаголовокдокументаизменится,мывфункцииinitialize()можемпрописатьmodx->runProcessor('resource/update', array('id'=> 1, 'pagetitle' => 'new pagetitle'));`, то будет получен объект документа с `id => 1`, и его заголовок (pagetitle) изменен на переданный в процессор параметр `'pagetitle' => 'new pagetitle'`. Так вот, если мы в своем процессоре хотим избежать того, что кто-то передаст новый заголовок и и заголовок документа изменится, мы в функции `initialize()` можем прописать `this->unsetProperty('pagetitle');` Таким образом даже если кто-то и передаст в запрос параметр pagetitle, он будет удален из параметров.
    • setProperties($properties). Устанавливает сразу несколько свойств процессора.
    • getProperties(). Получает все параметры процессора (на самом деле возвращает массив $this->properties).
    • **setDefaultProperties(array properties=array()).[ТакжекакиsetProperties(properties = array())**. [Так же как и setProperties(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(msg=,msg = '',object = null). Возвращает успешный ответ выполнения процессора (в массиве ответа тогда параметр success содержит true).
    • failure(msg=,msg = '',object = null). Возвращает ошибку.

    Методы success() и failure() как правило возвращаются в методе process().

    • hasErrors() как и говорилось выше, проверяет есть ли ошибка в процессоре или нет.

    Здесь есть одно очень важное замечание: как я уже не раз говорил, у процессоров нет собственного объекта обработки ошибок, поэтому, если у вас вазывается сразу несколько процессоров, и в каком-то из них будет ошибка, то все последующие процессоры при проверке $this->hasErrors() будут возвращать ошибку, так как на все один единый объект — $modx->error. Так что желательно после вызова процессора выполнять сброс ошибок $modx->error->reset(). В случае, если процессор вызывается в Smarty-шаблоне, такой сброс ошибок не требуется, так как сброс прописан в самом смарти-плагине (modxSmarty v1.0.0+).

    • addFieldError(key,key,message = ''). Добавляет множественное сообщение об ошибке (удобно, к примеру, при проверке нескольких полей формы).
    • getLanguageTopics(). Здесь мы можем указать массив словарей, которые нужно будет MODX-у инициализировать перед выполнением процессора.
    • process(). Этот метод в базовом классе абстрактный, что обязывает в расширяющих процессорах прописать собственный метод process() с пользовательским кодом. Если этот метод не прописать, будет возвращена фатальная ошибка. К примеру, в расширяющем его процессоре modObjectCreateProcessor прописан свой метод process(), так что если ваш процессор расширяет его, то метод process() уже не обязательно прописывать.
    • run(). Это самый главный метод процессора, который задает общую логику работы процессора, выполняя основные его методы в нужном порядке и обрабатывая ошибки. Давайте рассмотрим его код внимательней с комментариями.
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 Процессоры MODX

Вообще, если освоить навигацию по тому ресурсу и хорошенько покопаться, можно много всего найти и узнать. К примеру, можно ощутить, что MODX со своими процессорами — это целая вселенная!

Процессоры 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());

В ответ вы должны получить массив данных товаров (максимум трех по условию запроса), отсортированных по дате создания в обратном порядке.

На этом на сегодня все. А в качестве домашнего задания попробуйте проследить всю родословную процессора из последнего примера, и понять как формируется запрос на получение данных и какие таблицы он затрагивает.