"Классные" процессоры в MODX 2.2


Предлагаю вам свой очень вольный перевод записи из блога Mark Hamstra о новых процессорах, основанных на классах. Я буду называть их «классными» процессорами — так короче и точнее отражает их суть.

Одно из изменений в MODX 2.2 это новая, полностью переделанная система процессоров, основанных на классах («классные» процессоры), позволяющие вам cущественно упростить создание процессоров для компонентов. Как и любая обновка — эта позволяет вам использовать несколько новых фокусов.

Я использовал эти процессоры в своем новом проекте, и теперь хочу распространить эти наработки где только можно.

Что такое «классные» процессоры?Если вы уже создавали компоненты для MODX, то вы знаете, что такое процессор — это php файл, который запускается AJAX коннектором (или modX::runProcessor), делает, все что от него требуется и возвращает ответ в виде JSON строки.

«Классные» процессоры упрощают эту работу. Например, вы можете использовать подобный код для обновления какого-то элемента:
<?php
class fdmPriceCodeUpdateProcessor extends modObjectUpdateProcessor {
    public $classKey = 'fdmPriceCode';
    public $languageTopics = array('frontdeskman:pricing');
    public $objectType = 'fdmp';
}
return 'fdmPriceCodeUpdateProcessor';
… и MODX сделает всю работу за вас.

Что делает modObjectUpdateProcessor?Несмотря на минимум кода, который мы использовали, для его корректого выполнения происходит много всяких неочевидных процессов.

Разбираясь в классе modProcessor, я нашел 14 этапов для выполнения modObjectUpdateProcessor
  1. Стартует ваш процессор, устанавливаются его свойства.
  2. Запускается функция checkPermissions(), который вы можете использовать для проверки любых прав доступа.
  3. Запускается функция getLanguageTopics(), который возвращает массив языковых словарей, затем они загружаются.
  4. Стартует функция initialize(): она берет первичный ключ (из публичной переменной primaryKeyField, обычно это «id»), затем достает объект нужного класса по этому ключу, и, если это наследник modAccessibleObject, проверяет юзера на соответствие политике «save». Если initialize() не возвращает true — запрос отменяется, с ошибкой.
  5. Стартует beforeSet(): по умолчанию он просто проверяет, не установлено ли ошибок (например, так — $this->addFieldError('fieldname','error')). Если эта функция не возвращает true — запрос завершается с ошибкой.
  6. Вызывается fromArray() из объекта (он доступен через $this->object) для присвоения поученных при запуске свойств.
  7. Запускается beforeSave(). Как и beforeSet(), этот метод проверяет наличие ошибок. Если он не возвращает true — запрос возвращает сообщение об ошибке.
  8. Объект вызывает validate(). Если эта функция не возвращает положительное значение — выставляются ошибки в соответствие с проверенными значениями.
  9. Если вы выставили переменную beforeSaveEvent в своем классе, вызывается выставленное событие (invoke event) и если не возвращается true — сохранение не происходит. Учитывая, что fireBeforeSaveEvent() отмечена как публичная, обычно вам не нужно выставлять эту переменную.
  10. Вызывается saveObject(), которая просто делает save() объекта. В соответствии с комменариями в коде, эта функция может быть переназначена для каких-то особенных изменений объекта.
  11. Запускается fireAfterSaveEvent(), которая вызывает событие, указанное в переменной afterSaveEvent класса.
  12. Срабатывает logManagerAction(), для записи стандартного сообщения в Журнал системы MODX (вы его найдете в «Отчеты» → «Журнал системы управления»).
  13. Возвращается сообщение об успешном завершении процесса через функцию cleanup(). Она, на самом деле, просто оборачивает $modx->error->success/failure, который использовался в процессорах до версии 2.2.
  14. Ответ (успех или ошибка) парсится в объект modProcessorResponse и возвращается вам.
Обратите внимание, как много в списке жирного текста. Это функции, или переменные, которые доступны вам для переназначения и расширения, чтобы вы могли сделать, что вам нужно. Запомните, что каждый «классный» процессор (create, update, getlist, remove и get) имеет собственные методы. Если вы используете IDE типа PhpStorm — вы получите подсказки в коде. Если нет — погружайтесь в исходник.

Расширяем наш процессор для обработки чекбоксовКогда вы сделали замечательное окно на modExt/ExtJS и прилепили туда активный чекбокс (xtype: 'checkbox') — оно может работать не так, как вы задумали. Возможно, ваша схема БД ожидает булево true или false (я знаю — у меня так!), которое будет сохранено в БД в поле с типом tinyint 1 или 0. А вот форма вам отправит поле чекбокса как «on», если он отмечен, или пустоту — если нет. Оба-на…

К счастью, у нас есть много функций, чтобы расширить процессор update и привести значение вредного чекбокса к нужному виду, для сохранения в объект через fromArray() как полагается.

Посмотрите снова на список из 14 шагов. Метод fromArray() запускается на 6м этапе, значит, нам надо поменять значение чекбокса до него. Как вы смотрите на «BeforeSet», в 5м этапе? По моему, должно сработать!

Если вы не особо знакомы с объектно-ориентированным программированием (ООП), то вся эта писанина может показаться вам непонятной фигней. На самом деле, мы просто переписываем родные методы стандартного процессора своими, чтобы изменить его как нам надо. В интернете навалом книжек по ООП в PHP, которые вы можете почитать, если интересно.

Итак, теперь мы меняем наш первый пример, чтобы изменить значение чекбокса при помощи «beforeSet»:
class fdmPriceCodeUpdateProcessor extends modObjectUpdateProcessor {
    public $classKey = 'fdmPriceCode';
    public $languageTopics = array('frontdeskman:pricing');
    public $objectType = 'fdmp';
 
    public function beforeSet() {
        $this->setProperty('active',($this->getProperty('active') == 'on'));
        return parent::beforeSet();
    }
}
return 'fdmPriceCodeUpdateProcessor';
Здесь мы использовали тернарный оператор вместе с функциями getProperty и setProperty от процессора. Но если вам нравится более подробный код — можно написать иначе, на результат это никак не влияет:
/* ... */
    public function beforeSet() {
        $active = $this->getProperty('active');
        if ($active == 'on') { $active = true; }
        else  { $active = false; }
        $this->setProperty('active',$active);
        return parent::beforeSet();
    }
/* ... */

Мы можем сделать что-то подобное и с процессором remove, для предотвращения удаления элемента, если он все еще связан с другим элементом.
Например, следующий процессор не удалит объект «fdmCharacteristic», если тот еще связан с объектами «fdmRoomTypeCharacteristic».
<?php
class CharacteristicRemoveProcessor extends modObjectRemoveProcessor {
    public $classKey = 'fdmCharacteristic';
    public $languageTopics = array('frontdeskman:property');
    public $objectType = 'rtc';
     
    public function beforeRemove() {
        $chars = $this->modx->getCount('fdmRoomTypeCharacteristic',array('characteristic' => $this->getProperty('id')));
        if ($chars > 0) {
            return $this->modx->lexicon('rtc.remove.roomtypesStillLinked');
        }
        return parent::beforeRemove();
    }
}
return 'CharacteristicRemoveProcessor';
Последний пример на сегодня расширяет modObjectGetListProcessor для присоединения (join) таблицы и фильтрации по ее свойствам. Мы этого добиваемся изменением функции prepareQueryBeforeCount(). Так же, мы присваиваем еще немного переменных.
<?php
class RoomTypeGetListProcessor extends modObjectGetListProcessor {
    public $classKey = 'fdmRoomType';
    public $languageTopics = array();
    public $defaultSortField = 'name';
    public $defaultSortDirection = 'ASC';
    public $objectType = 'fdmRoomType';
 
    /**
     * @param xPDOQuery $c
     * @return \xPDOQuery
     */
    public function prepareQueryBeforeCount(xPDOQuery $c) {
        $query = $this->getProperty('property');
        if (!empty($query) && is_numeric($query)) {
            $c->innerJoin('fdmRoomTypeProperty','RTP','fdmRoomType.id = roomtype');
            $c->where(array(
                'RTP.property' => (int)$query
            ));
        }
        return $c;
    }
}
return 'RoomTypeGetListProcessor';
Замечание про чекбоксы в modExtЯ использовал тип «checkbox», хотя в MODX с версии 2.1 есть тип «xcheckbox», который присылает значение (1 или 0, если мне не изменяет память). Не знаю, почему я не использовал его, но наверняка «классный» процессор будет работать и с ним. Просто не проверял, и я люблю все контролировать.

«Просто интересно… Вы работаете над компонентом по управлению недвижимостью?»Как вы догадались?! :)

Что меня выдало: объект «fdmRoomType», «fdmRoomTypeCharacteristic» или «fdmProperty»? :)

Супер-пупер компонент, над которым я сейчас работаю называется FrontDeskMan и я надеюсь закончить его в течении нескольких недель. Он не будет доступен как Open Source, но наверняка мы будем продавать на енго лицензии и поддерживать его, как расширение для индустрии туризма и развлечений в моем совместном проекте с Jared Loman.
Вы уже можете готовить клиентов местных отелей к новому вебсайту с интегрированным управлением комнатами и бронированием, когда мы стартуем!
21 june 2012, 09:15    Василий Наумкин   G+  
8    3100 0

Comments (48)

  1. Василий Наумкин 21 june 2012, 12:25 # 0
    Перевел, как смог. Пишите ошибки в комментах — буду править.
    1. Andrew Vakulenko 21 june 2012, 12:30 # 0
      во втором абзаце «чущественно»
      1. Василий Наумкин 21 june 2012, 12:31 # 0
        fixed.
      2. Andrew Vakulenko 21 june 2012, 12:41 # 0
        после 14 пункта в абзаце:
        «каждый „классный“ процессор (create, update, getlist, remove и get) имеет собственные и методы» — думаю «и» не нужно >> «имеет свои собственные методы»
        1. Василий Наумкин 21 june 2012, 12:43 # 0
          Спасибо!
          Дочитайте до конца и давайте все правки сразу, списком =)
          1. Andrew Vakulenko 21 june 2012, 12:59 # 0
            ок! :)
            все дочитал, перевод удачный, сравнивал некоторые отрывки с оригиналом, в общем понравилось, спасибо! :)
            хотя, признаюсь, материал понятен пока на 70-80%, нужно будет еще почитать и вернуться, когда будет писать свои компоненты :)
            пока увидел только еще одно исправление — «Когда вы сделали замечательно окно на modExt/ExtJS» — наверно, «замечаетльное»
      3. Виталий Киреев 21 june 2012, 15:17 # 0
        Не пойму как вообще debug проводить. Вроде все делал по туториалу Doodles, только заменяя значения на свои таблицы и классы. В итоге табличка выводится, но пустая. Обращение к connector.php возвращает {«total»:«0»,«results»:[]}.
        Плюс еще у меня $objectType никак не влияет на значения в лексиконе. Задаю к примеру $objectType = 'my_cmp.message' и $_lang['my_cmp.message_id'] = 'ID сообщения'. В табличке вывожу заголовок header: _('id') — получается пустой. А если выводить header: _('my_cmp.message_id') — он выводится.
        1. Василий Наумкин 21 june 2012, 15:25 # 0
          Я делаю просто — print_r($var); die;
          И смотрю в консоли браузера что выводится коннектором.

          Про лексикон ничего не могу сказать, тут нужно разбираться, а мне некогда.
          1. Виталий Киреев 21 june 2012, 16:12 # 0
            А куда это вставить то можно, если идет class MessageGetListProcessor extends modObjectGetListProcessor {...} а потом сразу return MessageGetListProcessor;

            Нашел в ошибках, что этот процессор запрашивает таблицу, начинающуюся на modx_ хотя у меня в схеме без него. В сниппетах все прекрасно работало. Подскажите, пожалуйста, где убирается этот префикс?
            1. Василий Наумкин 21 june 2012, 16:18 # 0
              Не подскажу.

              Это перевод, а не лично мой опыт работы. Я пока только getlist пробовал погонять по исходникам самого MODX, проблем с префиксом не было, все нормально работало.

              Могу посоветовать поглядеть продвинутые компоненты, которые используют эти «классные» процессоры.
              Articles например — https://github.com/splittingred/Articles/tree/develop/core/components/articles/processors
              1. Виталий Киреев 21 june 2012, 17:49 # 0
                Разобрался со всем, оказывается в сниппете addPackage был с пустым префиксом, а в процессорах не указан и выставляло по умолчанию modx_.
                А $objectType только на ошибки в процессоре влияет.
                1. Василий Наумкин 21 june 2012, 17:50 # 0
                  А как тогда сниппеты работали, с пустым префиксом?

                  Или в них модель отдельно подключали?
                  1. Виталий Киреев 22 june 2012, 08:48 # 0
                    Пустой имеется в виду подключал прямо в сниппете свою модель через addPackage(блаблабла, '');
        2. Иван Брежнев 21 june 2012, 15:55 # 0
          2. Запускается функция checkPermissions(), который вы можете @@импользовать@@ для проверки любых прав доступа.
          1. Иван Брежнев 21 june 2012, 15:55 # 0
            2. Запускается функция checkPermissions(), которую вы можете использовать для проверки любых прав доступа.
          2. Виталий Киреев 24 june 2012, 15:26 # 0
            Почти освоил эту штуку. Только не получается сделать автоапдейт из таблицы (это как в системных настройках — меняешь и сразу запоминается). В методе modObjectUpdateProcessor::initialize() оказывается пустым $this->object и все тут…
            1. Василий Наумкин 24 june 2012, 16:57 # 0
              В таких случаях я смотрю исходник и делаю как там.

              Вот исходник автоапдейта системных параметров — https://github.com/modxcms/revolution/blob/develop/core/model/modx/processors/system/settings/updatefromgrid.class.php

              Процессору шлется строка data, он его принимает, проверяет, преобразует из JSON в массив и выставляет переменные объекта.

              Очень красиво.
              1. Василий Наумкин 24 june 2012, 17:03 # 0
                Ссылка стала битой из-за переноса в комментарии.

                Вот нормальная:
                goo.gl/BgZfm
                1. Виталий Киреев 24 june 2012, 17:17 # 0
                  Меня как раз это осенило и хотел сообщить об этом)) даже файл тот же смотрел :)) Странно, что они не вынесли UpdateFromGridProcessor отдельно.
            2. Иван Брежнев 09 october 2012, 16:18 # 0
              Вопрос по «классным» процессорам.
              Пытаюсь получить только данные из двух столбцов таблицы, пишу вот в этот метод след. код
              public function prepareQueryBeforeCount(xPDOQuery $c)

              $c->select(array('id','pagetitle'));

              Все равно выбираются все столбцы из таблицы. Я уже и называние класса добавлял перед названием столбца, тот же результат. Может быть у вас есть решение?
              Т.к. только из-за этого пока не могу полностью перейти на классные процессоры
              1. Иван Брежнев 10 october 2012, 17:32 # 0
                Получилось, нужно было переопределить в расширении класса две функции getData() и prepareRow(), т.к. первая функция возвращает объект, а вторая его ожидает. Изменил так чтобы getData() возвращала массив.
                1. Иван Брежнев 10 october 2012, 17:33 # 0
                  Но по кол-ву строк получилось так же как и в процедурном стиле.
                  А время отклика через «классный» процессов выше (т.к. там куча проверок и событий) чем через процедурный.
                2. Василий Наумкин 10 october 2012, 17:38 # 0
                  $c->select('id,pagetitle') — одна строка, через запятую.

                  Покажутся все столбцы, так как это записано в модели, но реально выберутся данные только из указанных — остальные должны быть пусты.

                  Дальше да, prepareRow() и всех делов.
                  1. Иван Брежнев 11 october 2012, 00:20 # 0
                    В том то и дело что выбираются данные из всех столбцов. Я бы не постил, если бы не попробовал сам различные варианты.
                    Ниже иллюстрации.

                    i26.fastpic.ru/big/2012/1011/f5/4421f51f21031820061407016b0e33f5.png
                    i26.fastpic.ru/big/2012/1011/5b/f592963e8b248c963eb4d64c97861f5b.png
                    1. Василий Наумкин 11 october 2012, 06:21 # 0
                      Сделал в getData $c->prepare(); echo $c->toSql();die;

                      SELECT `id`,`pagetitle`,`parent` FROM `modx_site_content` AS `modResource` WHERE ((`modResource`.`published` = 1 AND `modResource`.`deleted` = 0) AND `modResource`.`id` IN (13,14,52)) ORDER BY `modResource`.`menuindex` ASC LIMIT 20

                      То есть, SQL запрос в getCollection попадает верный, а вот почему оно выбирает все столбцы, вместо указанных — мне не ведомо.

                      Проверил — и в сниппете так же, и в методе getIterator(). Похоже, с каких-то пор они просто игнорируют SELECT.

                      То ли баг, то ли так и надо.
                      1. Иван Брежнев 11 october 2012, 14:36 # 0
                        Интересно)
                        1. Иван Брежнев 11 october 2012, 14:40 # 0
                          Может быть xPDO сначала кэширует все данные, а потом уже из кэша их достает. Но все равно не понятно почему он игнорирует перечисленные столбцы в SELECT
                          1. Василий Наумкин 11 october 2012, 14:47 # 0
                            Мое мнение — глюк.

                            Раньше точно работало — проверял. Сейчас нет времени разбираться.
                            1. Иван Брежнев 11 october 2012, 14:49 # 0
                              Возможно и глюк.
                              версия MODX 2.2.5
                              1. Иван Брежнев 11 october 2012, 16:25 # 0
                                Проверял только что свою догадку, думал данные извлекаются из кэша, оказалось что нет. В кэше появляется только тот документ на котором я вызываю сниппет pastebin.com/mcR3Nr7H и почему-то всегда ресурс с id=1 кэшируется, остальных ресурсов в кэше нет.
                                1. Василий Наумкин 11 october 2012, 16:33 # 0
                                  Подозреваю, потому, что id=1 — это site_start.

                                  Проблема явно не в кэш, там надо исходники копать. Правда, не ясно зачем, если вопрос уже решен?
                                  1. Иван Брежнев 11 october 2012, 16:38 # 0
                                    Я как раз изучаю исходники, просто интереса ради.
                                    Да id=1 это site_start, дефолтная установка 2.2.5
                            2. Иван Брежнев 11 october 2012, 14:45 # 0
                              Кстати есть такой метод чтобы конвертировать SQL запрос в объект xPDO
                              $modx->getCriteria($className, $criteria, $cacheFlag);

                              Т.е. обратный процесс методу toSQL()
                          2. Иван Брежнев 11 october 2012, 00:24 # 0
                            А это процессор который получился
                            pastebin.com/j7JBtHLr
                            1. Иван Брежнев 11 october 2012, 00:58 # 0
                              Вот то что возвращает получившийся процессор, то что нужно
                              i25.fastpic.ru/big/2012/1011/54/b8fc5426ff408e5e197906a73643c954.png
                        2. Дмитрий Баданин 29 december 2012, 13:25 # 0
                          Немного неясно как делать соединения.
                          Сделал innerJoin как в последнем примере. Смотрю в консоли — поля связанной таблицы по-прежнему не возвращает. Может быть нужно в $c->select прописывать в классе getlist?
                          1. Василий Наумкин 29 december 2012, 13:33 # 0
                            Да, конечно нужно не только объединить таблицы, но и выбрать из них нужные данные.
                            1. Дмитрий Баданин 29 december 2012, 13:51 # 0
                              Вот такой код не срабатывает — results 0.
                              public function prepareQueryBeforeCount(xPDOQuery $c) {			
                              		$c->select(array('id','name','TdTypes','updated','enabled'));
                              		$c->innerJoin('TdTypes','TdTypes','TdItems.tid=TdTypes.id');
                              		return $c;
                                  }
                              Пробовал и такой вариант:
                              $c->select(array('columns'=>array('id','name','TdTypes','updated','enabled')));
                              и такой
                              $c->select('id','name','TdTypes','updated','enabled');
                              Может быть определяю неверно, может не в той функции определяю. Подскажи, пожалуйста.
                              1. Василий Наумкин 29 december 2012, 15:27 # 0
                                xPDOQuery::select() принимает только строку. В документации была опечатка, уже поправил.

                                То есть:
                                $c->select('id,name,TdTypes,updated,enabled');
                                1. Дмитрий Баданин 29 december 2012, 18:31 # 0
                                  Попробовал строкой. В консоль попало только это
                                  {"total":"1","results":[]}
                                  Если закомментировать строку с select, то выводится это
                                  {"total":"1","results":[{"id":14,"enabled":1,"created":"-1-11-30 00:00:00","updated":"-1-11-30 00:00:00","ended":"-1-11-30 00:00:00","cid":3,"tid":3,"fid":0,"name":"\u0422\u0435\u0441\u0442\u043e\u0432\u044b\u0439","description":""]}}
                                  Функция целиком выше.
                                  1. Василий Наумкин 29 december 2012, 18:37 # 0
                                    Смотри лог, по любому выбираешь несуществующий столбец.

                                    xPDO такие ошибки пишет в системный журнал.
                                    1. Василий Наумкин 29 december 2012, 18:40 # 0
                                      Попробуй еще
                                      $c->select($this->modx->getSelectColumns('TdTypes','TdTypes'));
                                      1. Дмитрий Баданин 29 december 2012, 19:01 # 0
                                        Вот так
                                        $c->select($this->modx->getSelectColumns('TdTypes','TdTypes'))
                                        как раз получилось.
                                        Только поле name таблицы TdTypes затерло поле name таблицы TdItems при выводе. Видимо в алиасах дело. Поразбираюсь, спасибо за помощь.
                                        1. Василий Наумкин 29 december 2012, 19:06 # 0
                                          Крайне полезно делать
                                          $c->prepare();
                                          echo $c->toSql();
                                          die;
                                          Будет дамп SQL запроса, который можно загонять для проверки в phpmyadmin.
                                          1. Дмитрий Баданин 30 december 2012, 18:23 # 0
                                            Не знаю пригодится ли кому-нибудь эта конструкция, но вот так я связал три таблицы. Полагаю есть более элегантные решения, но я пока до них не дошел.
                                            public function prepareQueryBeforeCount(xPDOQuery $c) {
                                            			$c->select($this->modx->getSelectColumns('TdTypes','TdTypes','TdTypes_',array('name')));
                                            			$c->select($this->modx->getSelectColumns('TdCompanies','TdCompanies','TdCompanies_',array('name')));
                                            			$c->select($this->modx->getSelectColumns('TdItems','TdItems'));
                                            			$c->innerJoin('TdTypes','TdTypes','TdItems.tid=TdTypes.id');
                                            			$c->innerJoin('TdCompanies','TdCompanies','TdItems.cid=TdCompanies.id');
                                            			//$c->prepare();
                                            			//echo $c->toSql();
                                            			//die;
                                            			return $c;
                                                }
                                            Эта процедура дает такой запрос
                                            SELECT `TdTypes`.`name` AS `TdTypes_name`, `TdCompanies`.`name` AS `TdCompanies_name`, `TdItems`.`id`, `TdItems`.`enabled`, `TdItems`.`created`, `TdItems`.`updated`, `TdItems`.`ended`, `TdItems`.`cid`, `TdItems`.`tid`, `TdItems`.`fid`, `TdItems`.`name`, `TdItems`.`description` FROM `modx_tenders_tdItems` AS `TdItems` JOIN `modx_tenders_tdTypes` `TdTypes` ON TdItems.tid=TdTypes.id JOIN `modx_tenders_tdCompanies` `TdCompanies` ON TdItems.cid=TdCompanies.id 
                                            1. Василий Наумкин 30 december 2012, 18:30 # 0
                                              Поздравляю!

                                              Лично я не знаю более элегантного решения объединить 3 таблицы в одной выборке, чем join.

                                              Кстати, можно немного упростить, выбирая поля псевдонимами:
                                              $c->select('TdTypes.name as td_name');
                                              $c->select('TdCompanies.name as comp_name');
                                              
                                              1. Дмитрий Баданин 30 december 2012, 18:45 # 0
                                                Да, такая конструкция гораздо лучше — вчера пробовал, почему то строку не принимало как надо. Сейчас все ок.
                                          2. Дмитрий Баданин 29 december 2012, 19:01 # 0
                                            Журнал пустой. Ошибок нет.
                                You need to login to create comments.