Подгрузка "классных" процессоров средствами API MODX

Сразу уточню, что материал рассчитан в основном на тех, что уже использует процессоры, знает что это такое и как их готовить. И вообще этот материал многим может показаться сухим и не интересным. Но кто программирует (Илья, это тебя особенно касается;-)), обязательно надо изучить материал. Поверьте на слово, что это очень мощный инструментарий.

Я не раз уже поднимал тему подгрузки файлов-процессоров средствами API MODX. Раньше, когда у нас процессоры были просто php-файлами, нас это вообще не интересовало, мы просто выполняли $modx->runProcessor($action) и все. MODX делал require этого файла и все, возвращал полученный результат. Сейчас же у нас процессоры стали гораздо умнее — их можно расширять другими процесс-классами и т.п. Процессоры сегодня могут содержать очень мощный функционал, и это часто полностью законченные, самостоятельные классы. Так зачем нам писать кучу лишнего кода, когда мы можем использовать уже готовые классы, переопределить их и заставить работать под свои нужды?

Рассмотрим стандартный механизм работы процессора (здесь и далее будем рассматривать только «классные» процессоры). Мы выполняем $response = $modx->runProcessor($action, $params, $options); MODX в методе modX::runProcessor определяет путь до файла процессора, и если это класс, то он его инициализирует и возвращает результат выполнения (по сути это всегда результат метода modProcessor::process()). Но что, если мы не хотим получить результат метода processor::process(), а хотим просто получить результат какого-то внутреннего метода класса? То есть нам нужен просто класс?

Для этого мы просто подгружаем файл процессора require_once $processor_absolute_path. И тогда нам доступен сам класс процессора. Но в чем здесь проблема? Во-первых, элементарно не удобно. То есть каждый раз нам надо писать актуальный путь до процессора. А если мы его переместили, и вообще в другой пакет? Идти высчитывать кол-во методов dirname(), писать папки и т.п.

Во-вторых, отсутствие стандартного метода и единых проверок может даже привести к фатальным ошибкам.

В общем, я и с Джейсоном пытался этот вопрос обсуждать, но как-то мы не пришли к какому-то единому знаменателю (я Джейсона пытался убедить в необходимости ввести метод типа modX::loadProcessor()). А вот Марк Mark-H мне сегодня подкинул очевидную идею — использовать $modx->loadClass(); Не могу даже понять почему я раньше до этого не догадался, но его подсказки хватило, чтобы сразу увидеть удобство этого подхода и решение моих проблем.

Суть метода xPDO::loadClass заключается в том, что он ищет указанный класс во всех указанных папках подключенных пакетов (если не передан параметр, запрещающий делать это). То есть вы один раз можете выполнить $modx->addPackage($packName, $path); и далее уже выполнять подгрузку файла процессора так: $modx->loadClass($classname, '', false, true); (третий параметр false указывает на то, что не надо игнорировать подключенные пакеты, и надо искать по их папкам, а четвертый параметр true указывает на то, что надо игнорировать класс работы с базой данных (ведь у нас это просто процессор)). Все. Теперь не надо высчитывать пути.

Как этот процесс максимально оптимизировать? Каждый раз писать $modx->addPackage() мягко выражаясь — не очень удобно. Потому лучше само собой добавить это в extensionPackages. Но надо учитывать две задачи:

  1. Уникальность имен пакетов (то есть нельзя два пакета назвать processors, при этом именно название пакета плюсуется к конечному пути пакета). А ведь нам очень желательно, чтобы процессоры лежали в папке processors, чтобы не рушить стандарты (тот же processor-плагин пакета modxSmarty ищет процессоры именно в папке processors).
  2. Возможность указывать несколько пакетов с процессорами.

В общем, обычно экстеншены ведут к папке component/model/packageName/. Нам этот путь не подходит, так как противоречит вышеизложенным правилам. Значит делаем два пакета. Один ведет в component/model/packageName/ (он нам нужен для хранения основных классов, в том числе и тех, которые работают с базой данных), а второй в component/processors/packageName/.

Через консоль выполняем два запроса:

$modx->addExtensionPackage('myPackage', '[[++core_path]]components/myPackage/model/',array(
    "serviceName" => "myservice",
    "serviceClass" => "myClass",
));

В этом запросе мы не просто добавили свой пакет. Указав serviceName и serviceClass, у нас каждый раз при инициализации MODX будет выполнять еще и $modx->getService($name, $class); То есть у нас всегда в MODX будет свой объект $modx->myservice, который будет инстансом класса myClass.

$modx->addExtensionPackage('myProcessors', '[[++core_path]]components/myPackage/processors/');

А вот это уже мы добавили папку для наших процессоров. То есть надо будет создать папку myPackage в [[++core_path]]components/myPackage/processors/, и там создавать наши процессоры.

Код нашего сервис-класса будет выглядеть так:

class myClass{
    public $modx = null;
    
    function __construct(modX & $modx) {
        $this->modx = & $modx;
    }
    
    public function loadProcessor($fqn){
        return $this->modx->loadClass($fqn, '', false, true);
    }
}

То есть теперь, чтобы не писать каждый раз эти лишние 3 параметра, можно просто выполнять $modx->myservice->loadProcessor($classname); Кстати, не забывайте, что в $classname знак точки — это разделитель директорий, то есть.

И вот теперь один небольшой пример:

$modx->myservice->loadProcessor('path.myclass');
if($instance = myclass::getInstance($modx, 'myclass' )){
        $response = $instance->run();
        print_r($response->getResponse());
}

В данном случае мы могли не только стандартный метод processs выполнить, но и любой другой дозволенный.

Так же если вы свои процессоры пишите, и какие-то зависят от других, то теперь не обязательно писать require_once, а можно использовать этот метод, выполняя в начале кода $this->myservice->loadProcessor();

Я сейчас включу этот функционал в обновленную сборку и выложу новый снимок.

UPD: В этом методе есть один неприятный момент. xPDO::loadClass не подгрузит два одинаковых по названию файла в разных папках, и не возвращает реальные имена классов процессоров. Переписал этот метод в классе modxsite: https://gist.github.com/Fi1osof/a87522b0b0ea9e60a2ed/revisions

Но это временное решение. Разговаривал с Джейсоном. Нас скоро ждут кардинальные перемены в ядре MODX-а, в том числе и автоподгрузка классов. И вообще он смотрит в сторону удаления метода loadClass. Это все классно, но мне некоторых плюшек не хватает уже сегодня, потому добавляю это в новую сборку (чуть попозже выложу). Основной метод: $modx->modxsite->loadProcessor(), возвращающий реальное название процесор-класса. Обкатывать это будет уже в процессе.

UPD2: Обновленная сборка с новым методом $modx->modxsite->loadProcessor(); Так же в сборку были добавлены два пакета:

  • PackMan (для быстрой сборки пакетов их имеющегося на сайте)
  • CMPgenerator (для генерации моделей пакетов из таблиц в базе данных)

Если кому-то эти пакеты не известны, изучите в обязательном порядке.

UPD3: Данный метод изначально не предполагался как идеальный, а только временное решение, но вот нашелся в нем большой минус, который накладывает серьезные ограничения — область видимость. Традиционно файлы классов подгружаются самим MODX-ом, и в самом файле (за пределами описания класса) $this — это текущая инстанция MODX-а. При вызове же через $modx->modxsite->loadProcessor $this — это $modx->modxsite, который ни в коем случае не является инстанцией MODX. Ввиду этого не получается использовать $this в этих классах. Понятно, что мало кто этот хук вообще использует, но тем не менее. Можно было бы использовать $this->loadProcessor() для подгрузки других процессоров, но это сопровождается теми же проблемами, плюс к этому еще и мешает использовать этот процессор штатными средствами. По этой причине придется все так же использовать require, а $modx->loadProcessor() использовать исключительно для подгрузки только тех процессоров, которые не используют переменную $this.

Можно использовать в файлах классов так:

if($this instanceof modX){
    $modxsite = & $this->modxsite;
}
else{
    $modxsite = & $this;
}
$modxsite->loadProcessor('class');

Но учтите, что хотя если вы про это и забудете, оно будет работать, но по хорошему, лучше этим не злоупотреблять. Только если у вас стабильная выработанная методология. В принципе по мне, так это меньшее зло, чем require_once abs_path;