- PVSM.RU - https://www.pvsm.ru -

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

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

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

Для начала нужно было определиться на какое из событий требуется повесить плагин. Для тех, кто еще не знаком с системой MODX, замечу, что плагином здесь называется именно дополнение, которое вызывается на некое предопределенное событие. Из большого списка системных событий [3], предоставляемых по умолчанию оказалось, что подходит только событие 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]]. В принципе задача была решена, но невозможно было применить к полям фильтры ввода-вывода [4].
Поэтому пришлось глубже разбираться в том, что именно делает парсер, когда обрабатывает теги. А делает он следующее.
Собирает все теги в контенте с помощью функции

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 [5].

Автор: Argnist88

Источник [6]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/modx/22245

Ссылки в тексте:

[1] RTFM: http://rtfm.modx.com/display/ADDON/Home

[2] fastField: http://modx.com/extras/package/fastfield

[3] системных событий: http://rtfm.modx.com/display/revolution20/System+Events

[4] фильтры ввода-вывода: http://wiki.modx.im/revolution:%D1%81%D0%BE%D0%B7%D0%B4%D0%B0%D0%BD%D0%B8%D0%B5_%D1%81%D0%B0%D0%B9%D1%82%D0%BE%D0%B2_%D1%81_modx:%D0%BD%D0%B0%D1%81%D1%82%D1%80%D0%BE%D0%B9%D0%BA%D0%B0_%D1%81%D0%BE%D0%B4%D0%B5%D1%80%D0%B6%D0%B8%D0%BC%D0%BE%D0%B3%D0%BE:%D1%84%D0%B8%D0%BB%D1%8C%D1%82%D1%80%D1%8B_%D0%B2%D0%B2%D0%BE%D0%B4%D0%B0-%D0%B2%D1%8B%D0%B2%D0%BE%D0%B4%D0%B0_%D0%BC%D0%BE%D0%B4%D0%B8%D1%84%D0%B8%D0%BA%D0%B0%D1%82%D0%BE%D1%80%D1%8B_%D0%B2%D1%8B%D0%B2%D0%BE%D0%B4%D0%B0

[5] GitHub: https://github.com/argnist/fastField

[6] Источник: http://habrahabr.ru/post/161843/