Добавляем новый тег в MODX Revolution

в 6:11, , рубрики: modx, modx revolution, плагин, метки: ,

В данном топике описан мой опыт по созданию плагина для MODX Revolution, который добавляет новый тег к данной CMS. Напомню, что разработчик может использовать теги в контенте ресурсов своего сайта или в шаблонах и чанках. Например, тег [[*pagetitle]] будет обработан парсером MODX и вернет заголовок страницы, на которой находится пользователь.

Среди обширного списка тегов мне не хватало еще одного — вывода полей любого выбранного ресурса. Для этого приходилось скачивать и устанавливать из репозитория MODX сниппет getResourceField. Кроме неудобства, что данное решение не входит в базовую поставку CMS, оно еще и обладает, на мой взгляд, слишком длинным именем, не говоря уже о том, что приходится держать открытым RTFM, чтобы не напутать с названиями параметров. Поэтому я написал плагин fastField, о котором пойдет речь дальше.

Для начала нужно было определиться на какое из событий требуется повесить плагин. Для тех, кто еще не знаком с системой MODX, замечу, что плагином здесь называется именно дополнение, которое вызывается на некое предопределенное событие. Из большого списка системных событий, предоставляемых по умолчанию оказалось, что подходит только событие OnParseDocument, поскольку вызывается в парсере MODX в методе processElementTags() класса modParser (файл core/model/modx/modparser.class.php). После ни одно событие уже не сможет получить контент с нашим тегом, поскольку все они будут вырезаны, как несуществующие.
Первоначальная версия плагина была довольно проста:

$content = $modx->documentOutput;
$pattern = '@[[#(d+).(.+?)]]@si';
if (preg_match($pattern, $content, $matches) > 0) {
    $tag = $matches[0];
    $resource_id = $matches[1];
    $resource_field = explode('.', $matches[2]);
    $resource = $modx->getObject('modResource', $resource_id);

    if (count($resource_field) == 1) {
       $value = $resource->get($resource_field[0]);
    }
    else {
        if ($resource_field[0] == 'tv' && isset($resource_field[1])) {
            $value = $resource->getTVValue($resource_field[1]);
        }
        elseif (in_array($resource_field[0], array('properties', 'property', 'prop'))) {
            $value = $resource->getProperty($resource_field[2], $resource_field[1]);
        }
        else {
            $value = '';
         }
    }
    $modx->documentOutput = str_replace($tag, $value, $content);
}

Задачей стояла обработка тегов вида [[#10.pagetitle]], [[#10.tv.MyTV]]. В принципе задача была решена, но невозможно было применить к полям фильтры ввода-вывода.
Поэтому пришлось глубже разбираться в том, что именно делает парсер, когда обрабатывает теги. А делает он следующее.
Собирает все теги в контенте с помощью функции

public function collectElementTags($origContent, array &$matches, $prefix= '[[', $suffix= ']]') 

причем теги возвращаются в виде массива из 2 элементов — внешний тег и внутренний. Поскольку наш новый тег обладает всеми признаками тега, то данная функция вернет и его. Далее строится массив $tagMap, который содержит список замен для функции str_replace вида тег => обработанный тег. При обработке каждого тега вызывается функция парсера

public function processTag($tag, $processUncacheable = true)

в которой содержимое тега разбивается на части: токен (символ, обозначающий тот или иной вид тега, например, * для полей ресурса или ~ для ссылок, в нашем случае — решетка #), имя (или тело тега, например, pagetitle в теге [[*pagetitle]]), фильтры (:ucase и т.д.) и параметры (&parameter в тегах сниппетов или других). По токену определяется какой именно класс тега будет вызываться для обработки тега. Все они — потомки абстрактного класса modTag. Поэтому для создания нового тега создадим новый класс modResourceFieldTag. Во всех классах тегов переопределены методы process() и getContent(). При этом у нас новый тег очень сходен с тегом поля ресурса, и я сделал его производным от класса modFieldTag, чтобы оставить его метод process(). Вот, что получилось:

class modResourceFieldTag extends modFieldTag {

    /**
     * Overrides modTag::__construct to set the Field Tag token
     * {@inheritdoc}
     */
    function __construct(modX & $modx) {
        parent :: __construct($modx);
        $this->setToken('#');
    }

    /**
     * Get the raw source content of the field.
     *
     * {@inheritdoc}
     */
    public function getContent(array $options = array()) {
        if (!$this->isCacheable() || !is_string($this->_content) || $this->_content === '') {
            if (isset($options['content']) && !empty($options['content'])) {
                $this->_content = $options['content'];
            } else {
                $tag = explode('.', $this->get('name'));
                $tagLength = count($tag);
                // for processing tags in resource_id place ([[#[[+id]].pagetitle]])
                $tags = array();
                if ($collected= $this->modx->parser->collectElementTags($tag[0], $tags)) {
                    $tag[0] = $this->modx->parser->processTag($tags[0], $this->modx->parser->isProcessingUncacheable());
                }
                if (is_numeric($tag[0])) {
                    $resource = $this->modx->getObject('modResource', $tag[0]);
                    if ($resource)
                    {
                        if ($tagLength == 2) {
                            if ($tag[1] == 'content') {
                                $this->_content = $resource->getContent($options);
                            }
                            else {
                                $this->_content = $resource->get($tag[1]);
                            }
                        }
                        else {
                            if (($tag[1] == 'tv') && ($tagLength == 3)) {
                                $this->_content = $resource->getTVValue($tag[2]);
                            }
                            elseif (in_array($tag[1], array('properties', 'property', 'prop')) && ($tagLength == 4)) {
                                $this->_content = $resource->getProperty($tag[3], $tag[2]);
                            }
                            else {
                                $this->_content = '';
                            }
                        }
                    }
                    else {
                        $this->_content = '';
                    }

                }
            }
        }
        return $this->_content;
    }
}

Для обработки случая, когда тег вызывается с плейсхолдером для идентификатора ресурса (к примеру, [[#[[+id]].pagetitle]]) дополнительно обрабатываем эту часть тега:

$tags = array();
if ($collected= $this->modx->parser->collectElementTags($tag[0], $tags)) {
    $tag[0] = $this->modx->parser->processTag($tags[0], $this->modx->parser->isProcessingUncacheable());
}

Теги, которые могут вызываться в фильтрах, будут обработаны парсером после выполнения события.

Теперь осталось вызвать обработку данных тегов, собственно в плагине fastField:

switch ($modx->event->name) {
    case 'OnParseDocument':
        $content = $modx->documentOutput;
        $tags= array ();
        if ($collected= $modx->parser->collectElementTags($content, $tags, '[[', ']]', array('#')))
        {
            $tagMap= array ();
            foreach ($tags as $tag) {
                $token = substr($tag[1], 0, 1);
                if ($token == '#') {
                    include_once $modx->getOption('core_path') . 'components/fastfield/model/fastfield/fastfield.php';

                    $tagParts= xPDO :: escSplit('?', $tag[1], '`', 2);
                    $tagName= substr(trim($tagParts[0]), 1);
                    $tagPropString= null;
                    if (isset ($tagParts[1])) {
                        $tagPropString= trim($tagParts[1]);
                    }

                    $element= new modResourceFieldTag($modx);
                    $element->set('name', $tagName);
                    $element->setTag('');
                    $element->setCacheable(false);
                    $tagMap[$tag[0]] = $element->process($tagPropString);
                }
            }
            $modx->parser->mergeTagOutput($tagMap, $content);
            $modx->documentOutput = $content;
        }
        break;
}

Надеюсь, данная статья послужит читателям руководством для создания собственных тегов. Код может быть не идеальный, но он может служить заготовкой для дальнейшего экспериментирования с этой замечательной CMS/CMF.

Исходный код плагина доступен на GitHub.

Автор: Argnist88

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js