"Классные" процессоры в 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 июня 2012, 08:24
modx.pro
12
10 730
0

Комментарии: 48

Василий Наумкин
21 июня 2012, 12:25
1
0
Перевел, как смог. Пишите ошибки в комментах — буду править.
    21 июня 2012, 12:30
    0
    во втором абзаце «чущественно»
    21 июня 2012, 12:41
    0
    после 14 пункта в абзаце:
    «каждый „классный“ процессор (create, update, getlist, remove и get) имеет собственные и методы» — думаю «и» не нужно >> «имеет свои собственные методы»
      Василий Наумкин
      21 июня 2012, 12:43
      0
      Спасибо!
      Дочитайте до конца и давайте все правки сразу, списком =)
        21 июня 2012, 12:59
        0
        ок! :)
        все дочитал, перевод удачный, сравнивал некоторые отрывки с оригиналом, в общем понравилось, спасибо! :)
        хотя, признаюсь, материал понятен пока на 70-80%, нужно будет еще почитать и вернуться, когда будет писать свои компоненты :)
        пока увидел только еще одно исправление — «Когда вы сделали замечательно окно на modExt/ExtJS» — наверно, «замечаетльное»
    Виталий Киреев
    21 июня 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') — он выводится.
      Василий Наумкин
      21 июня 2012, 15:25
      0
      Я делаю просто — print_r($var); die;
      И смотрю в консоли браузера что выводится коннектором.

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

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

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

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

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

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

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

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

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

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

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

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

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

                i26.fastpic.ru/big/2012/1011/f5/4421f51f21031820061407016b0e33f5.png
                i26.fastpic.ru/big/2012/1011/5b/f592963e8b248c963eb4d64c97861f5b.png
                  Василий Наумкин
                  11 октября 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.

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

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

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

                          Т.е. обратный процесс методу toSQL()
                        Иван Брежнев
                        11 октября 2012, 00:24
                        0
                        А это процессор который получился
                        pastebin.com/j7JBtHLr
                    Дмитрий Баданин
                    29 декабря 2012, 13:25
                    0
                    Немного неясно как делать соединения.
                    Сделал innerJoin как в последнем примере. Смотрю в консоли — поля связанной таблицы по-прежнему не возвращает. Может быть нужно в $c->select прописывать в классе getlist?
                      Василий Наумкин
                      29 декабря 2012, 13:33
                      0
                      Да, конечно нужно не только объединить таблицы, но и выбрать из них нужные данные.
                        Дмитрий Баданин
                        29 декабря 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');
                        Может быть определяю неверно, может не в той функции определяю. Подскажи, пожалуйста.
                          Василий Наумкин
                          29 декабря 2012, 15:27
                          0
                          xPDOQuery::select() принимает только строку. В документации была опечатка, уже поправил.

                          То есть:
                          $c->select('id,name,TdTypes,updated,enabled');
                            Дмитрий Баданин
                            29 декабря 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":""]}}
                            Функция целиком выше.
                              Василий Наумкин
                              29 декабря 2012, 18:37
                              0
                              Смотри лог, по любому выбираешь несуществующий столбец.

                              xPDO такие ошибки пишет в системный журнал.
                                Василий Наумкин
                                29 декабря 2012, 18:40
                                1
                                0
                                Попробуй еще
                                $c->select($this->modx->getSelectColumns('TdTypes','TdTypes'));
                                  Дмитрий Баданин
                                  29 декабря 2012, 19:01
                                  0
                                  Вот так
                                  $c->select($this->modx->getSelectColumns('TdTypes','TdTypes'))
                                  как раз получилось.
                                  Только поле name таблицы TdTypes затерло поле name таблицы TdItems при выводе. Видимо в алиасах дело. Поразбираюсь, спасибо за помощь.
                                    Василий Наумкин
                                    29 декабря 2012, 19:06
                                    1
                                    0
                                    Крайне полезно делать
                                    $c->prepare();
                                    echo $c->toSql();
                                    die;
                                    Будет дамп SQL запроса, который можно загонять для проверки в phpmyadmin.
                                      Дмитрий Баданин
                                      30 декабря 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
                                        Василий Наумкин
                                        30 декабря 2012, 18:30
                                        0
                                        Поздравляю!

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

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