Про изоляцию условий модификаторов от парсера MODX Revolution

Как многие знают, парсер MODX Revolution при работе с модификаторами, имеет одну неприятную особенность, которая иногда сводит на нет их применение. Проблема в том, что при использовании условных модификаторов типа [[*id:is=`1`:then=`выполнить_что_либо`]] код находящийся в условии будет обработана парсером, вне зависимости от того, истинно ли было утверждение или ложно. Данный момент не является критичным, можно вообще не пользоваться модификаторами. Но вот появилось немного времени, стало интересно подумать и разобраться в вопросе, и возможно предложить какое-то решение. Небольшой дисклеймер, будет довольно много букв.


Сначала давайте убедимся, что проблема действительно присутствует. Самым наглядным примером для меня является использование чанка с заранее неверным кодом, которое вызовет предупреждение. Итак, создаем чанк с названием, например, testChunk, содержащий следующий код:

<a href="/[[~[[*parent]]]]" data-doc="1">test</a>

Для чистоты эксперимента возьмем чистую сборку MODX 2.6.1 и создадим два новых документа, один родительский в корне, другой его дочерний (это условие обязательно). Создаем пустой шаблон и пропишем туда следующий модификатор:

[[*isfolder:isnot=`1`:then=`[[$testChunk]]`]]

, очистим кеш и для начала в браузере обратимся к родительской странице. В логи MODX Revolution сразу же упадет сообщение об ошибке.

[2018-03-04 14:50:18] (ERROR @ modx.class.php : 990) `0` is not a valid integer and may not be passed to makeUrl()
[2018-03-04 14:50:18] (ERROR in resource 15 @ modparser.class.php : 1372) Bad link tag `[[~0]]` encountered

Действительно, содержимое чанка обработалось, хотя контента, который он должен был отдать на странице, мы не увидели. Как мы можем видеть, обработался у нас чанк на родительском документе, так как именно у него родитель равен 0, а метод makeUrl() не смог этот ноль обработать. И это несмотря на то, что модификатору было указано сработать только в случае, если запрашиваемый документ папкой не будет. Повторим обращение, и если документ у нас помечен как кешируемый, то больше никаких ошибок в логах мы не увидим. Это нам говорит о том, что модификаторы кешируются и условия их выполнения запоминаются. Если обратиться к дочернему документу, то все отработает как мы и ожидаем, и мы увидим ссылку на родительский документ.

Казалось бы, можно сделать все документы кешируемыми или отказаться от использования модификаторов. По поводу идеи сделать все документы кешируемые, во-первых сделать их кешируемыми не всегда получается, иногда бывают ограничения, во-вторых оно вам надо, после каждого обновления кеша при первом обращении к контенту снова генерировалось что-то, например сложные конструкции, типа соседних документов или тэгов с разветвленной вложенной логикой. Что касается отказа от модификаторов, да, от них можно отказаться, но мне с ними удобнее и их использование позволяет не плодить огромное количество шаблонов. Поэтому предлагаю попробовать взглянуть на эту проблему глубже.

Покопавшись в интернетах можно найти рекомендации использовать хитрые конструкции, при которых код чанка, сниппета или что мы там хотим вызвать, будет формироваться на лету, только при условии из модификатора: https://modx.com/blog/2012/09/14/tags-as-the-result-or-how-conditionals-are-like-mosquitoes/. Давайте проверим этот способ. Нам предлагается поступить следующим образом:

[[[[*field:is=`0`:then=`!SomeScript`:else=`$SomeChunk`]]]]

Сразу обратим внимание, что в данном примере задействованы оба условия. Давайте адаптируем к нашему примеру:

[[[[*isfolder:isnot=`1`:then=`$testChunk`]]]]

Выполнив данный код мы получим в лог сообщение:

[2018-03-04 15:11:34] (ERROR @ modparser.class.php : 540) Could not find snippet with name .

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

[[$test[[*isfolder:isnot=`1`:then=`Chunk`]]]]
[[$[[*isfolder:isnot=`1`:then=`testChunk`]]]]

Они оба работают, но есть одно но. Если мы будем смотреть на отладочную информацию выводимую MODX Revolution с количеством обращений к БД, то сразу заметим, что данные конструкции дают дополнительные запросы к БД как в не кешируемымом, так и в кешируемом варианте. Это связано с тем, что парсер пытается найти несуществующий чанк и не находит его. А для его поисков он дергает БД, и если с некешированной версией еще можно смириться, то 2 дополнительных запроса к БД на некешируемой версии страницы на ровном месте лично меня не устраивают.

Давайте попробуем как-то решить эту проблему другим путем. Например, не сразу очевидным, но возможным решением, был бы вывод тегов комментариев [[- в случае, когда условие не выполняется. Сразу скажу, что эта замечательная идея при подходе в лоб работать не будет, так как конструкция [[*isfolder:isnot=`1`:then=`[[-`]] не обработается правильно. Парсер MODX устроен таки образом, что не позволяет указывать не закрывающиеся пары квадратных скобок. То есть обернуть наш код в конструкцию:

[[*isfolder:isnot=`1`:then=`[[-`]]
Код
[[*isfolder:isnot=`1`:then=`]]`]]

у меня не получилось. В общем, подумав немного, приходит в голову следующая идея, опять таки в лоб, это добавление тэгов комментариев в код модификаторов, то есть, например:

[[+comstart]]
Код
[[+comstop]]

Однако и такой вариант, хоть и легко реализуем, работать не будет. Дело в том, что парсер MODX, обрабатывает модификаторы построчно и данный финт у нас не получится, так как комментарии будут вырезаны не возымев нужного нам действия. И вот тут остается только вклиниваться в обработку и вставлять комментарии до того, как текст попадет на обработку парсеру. Для этого разберемся как работает и обрабатывается страница до того момента, как начинают парситься тэги.

Пробежимся по основным этапам. При обращении к URL MODX вызывает $modx ->handleRequest(), который фактически вызывает $modx->request->handleRequest(), который в свою очередь вызывает prepareResponse метод класса modRequest, который будет готовить наш контент к отправке (в нем же к слову выбрасывается событие ‘OnLoadWebDocument’). Пока нам это ничего не дает, идем дальше. Метод prepareResponse вызывает метод outputContent класса modResponse, и вот уже можно оглядеться. Метод определяет тип документы и если он не бинарный, то отправляет его на парсинг, используя метод processElementTags объекта modParser. Посмотрев работу последнего методы мы видим, что перед тем, как начать парсить документ, выбрасывается событие OnParseDocument. То есть это единственное событие, в котором будет достаточно данных для обработки контента и на которое мы можем повесить какое-то свое действие, которое обработает контент раньше основного парсера. Замечательно, создаем плагин, назвав его PreConditions, который будет обрабатывать отслеживаемые тэги и будет подменять их на какие-то данные до обработки основным парсером.

Давайте продумаем логику работы плагина. Нам необходимо найти в контенте специфические тэги, которые должны быть обработаны раньше основного парсера. Для этого мы будем использовать OnParseDocument событие, чтобы сделать это прямо перед обработкой. При этом мы должны придумать какой-то механизм, чтобы эта обработка не исполнялась каждый раз, чтобы не воздействовать на производительность. Мы планируем использовать данный плагин для подстановки MODX комментариев для исключения блоков контента из обработки по определенному условию, хотя можно придумать и другие способы применения данного компонента. Единственный недостаток такого подхода с добавлением комментариев до этапа обработки основным парсером, что для каждого случая придется использовать два модификатора вместо одного. Чуть позже мы замерим, как это скажется на производительности.

Для правильной обработки нам необходимо выбрать токен для тэгов, то есть символ, который будет обрабатываться только нашим плагином и еще не используется MODX (то етсь не +,%,~,$,* и не #, если подключены fastField). Я выбрал символ ‘^’ так как в регулярных выражениях он означает начало строки, что по смыслу нам подходит. Все остальное будет в точности походить на работу с условными модификаторами (Conditional output modifiers). В принципе должны работать и строчные модификаторы, однако строки можно обрабатывать и обычными модификаторами, поэтому смысла их использования на этом этапе нет. Конечный внешний вид использования будет следующий:

[[^isfolder:is=`1`:then=`[[-`]]
Какой-то код
[[^isfolder:is=`1`:then=`]]`]]

Отмечу ряд особенностей работы компонента, во-первых для обработки тэгов плагин используем два класса, это modPreConditions и modPreConditionTag, который наследуется от modTag, во-вторых для обработки тэгов он использует собственный метод collectElementTags, который по своей логике напоминает родной collectElementTags, однако несколько проще и работает на регулярных выражениях. В-третьих, плагин обрабатывает контент не привычной для нас переменной $modx->resource->_output, ее в этот момент еще нет, а переменной $modx->documentOutput, в которой находится нужный нам контент. В-четвертых, плагин устанавливает собственную «глобальную» переменную $modx->preparsed, чтобы не обрабатывать контент каждый раз в рамках одного запроса к сайту. Все, далее я не буду подробно расписывать, как работает плагин и классы, так как они фактически эмулируют работу основного парсера, только по более простой схеме и с меньшим функционалом. Для интересующихся весь код можно посмотреть код на github.

Я предлагаю сразу перейти к тестам на производительность и насколько наш компонент будет сильно на нее влиять. Соберем чистую установку MODX 2.6.1 в которую добавим вышеупомянутые два документа родитель и потомок. Далее добавим в нее новый шаблон:

[[*content]]

[[$testChunk]] <-- то, что будем менять.

[[!timeStop]]

<div style="text-align:center;">sql: [^qt^] ([^q^]), php: [^p^], mem: [^m^], time: [^t^] from [^s^].</div>

, и создадим пустой чанк testChunk, и сниппет timeStop:

<?php
return "\n<br />" . (microtime(true) - $modx->startTime)."s<br />\n" . round(memory_get_peak_usage(true)/1048576,2)."Mb\n";

, как мы знаем, перед вызовом метода handleRequest() в index.php MODX Revolution сохраняет время перед его вызовом в специальной переменной, поэтому мы можем получить точное время выполнения от начала обработки запроса до выполнения нашего сниппета, который замерит время. Тут еще один дисклеймер, что я понимаю и знаю, что замерять время вообще не очень корректно, так как на него влияют множество факторов и оно сильно зависит, как от состояния железа, так от состояния веб-сервера и процесса PHP. Но здесь мы будем его замерять, просто ориентируясь работает быстрее, медленнее, и большая ли разница, т.е. нас интересует примерная картина, а не математическая точность.

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

Для генерации кусков HTML и не только кода будем использовать Emmet, который позволяет генерировать нужное количество кода с разными подстановками. Например:

{<a href="/[[~[[*parent]]]]" data-doc="$">test $</a>${newline}}*1000

, сгенерирует 1000 ссылок с порядковыми номерами. Если еще не интересовались Emmet, рекомендую ознакомиться.

Итак, первый блок будет состоять из трех проверок и будет связан с обработкой данных внутри чанка.

Чанк [[$testChunk]] без каких-либо внутренних проверок


Внутри чанка [[$testChunk]] создаем 1000 ссылок с кодом:

<a href="/[[~[[*parent]]]]" data-doc="1">test</a>

, и делаем замер по заранее определенной схеме и снимаем показатели:

Родительский документ не кешированный
Количество обращений к БД7
Время отработки PHP0,93954515457153s
Родительский документ закешированный
Количество обращений к БД0
Время отработки PHP0,08843207359314s
Дочерний документ не кешированный
Количество обращений к БД7
Время отработки PHP0,24113202095032s
Дочерний документ закешированный
Количество обращений к БД0
Время отработки PHP0,075303077697754s
В логах MODX предупреждение makeUrl() на родительском документе

Чанк [[$testChunk]] с одной внутренней проверкой


Внутри чанка [[$testChunk]] оставляем 1000 ссылок и оборачиваем их в проверку:

[[*isfolder:isnot=`1`:then=`
<a href="/[[~[[*parent]]]]" data-doc="1">test</a>
…
<a href="/[[~[[*parent]]]]" data-doc="1000">test</a>
`]]

, и делаем замер по заранее определенной схеме и снимаем показатели:

Родительский документ не кешированный
Количество обращений к БД7
Время отработки PHP0,88471412658691s
Родительский документ закешированный
Количество обращений к БД0
Время отработки PHP0,093286037445068s
Дочерний документ не кешированный
Количество обращений к БД7
Время отработки PHP0,2331531047821s
Дочерний документ закешированный
Количество обращений к БД0
Время отработки PHP0,088649988174438s
В логах MODX предупреждение makeUrl() на родительском документе

Чанк [[$testChunk]] с множеством внутренних проверок


Внутри чанка [[$testChunk]] все 1000 ссылок оборачиваем в проверку:

[[*isfolder:isnot=`1`:then=`<a href="/[[~[[*parent]]]]" data-doc="1">test</a>`]]

, и делаем замер по заранее определенной схеме и снимаем показатели:

Родительский документ не кешированный
Количество обращений к БД7
Время отработки PHP1,2959039211273s
Родительский документ закешированный
Количество обращений к БД0
Время отработки PHP0,092460155487061s
Дочерний документ не кешированный
Количество обращений к БД7
Время отработки PHP0,52802681922913s
Дочерний документ закешированный
Количество обращений к БД0
Время отработки PHP0,075139999389648s
В логах MODX предупреждение makeUrl() на родительском документе

Второй блок будет состоять из трех проверок и будет связан с обработкой данных внутри шаблона с использованием чанков и без них.

Единичная проверка в шаблоне с чанком [[$testChunk]]


В чанке осталяем наши сформированные 1000 ссылок без каких либо проверок. Внутрь шаблона помещаем конструкцию [[*isfolder:isnot=`1`:then=`[[$testChunk]]`]], и делаем замер по заранее определенной схеме и снимаем показатели:

Родительский документ не кешированный
Количество обращений к БД7
Время отработки PHP0,92086791992188s
Родительский документ закешированный
Количество обращений к БД0
Время отработки PHP0,10577201843262s
Дочерний документ не кешированный
Количество обращений к БД7
Время отработки PHP0,24568891525269s
Дочерний документ закешированный
Количество обращений к БД0
Время отработки PHP0,08309006690979s
В логах MODX предупреждение makeUrl() на родительском документе

Единичная проверка в шаблоне с чанком [[$testChunk]] с выполнением условия


В чанке осталяем наши сформированные 1000 ссылок без каких либо проверок. Внутрь шаблона помещаем конструкцию [[$[[*isfolder:isnot=`1`:then=`testChunk`]]]], как нам рекомендовали, для того, чтобы условия не обрабатывались и делаем замер по заранее определенной схеме и снимаем показатели:

Родительский документ не кешированный
Количество обращений к БД11
Время отработки PHP0,13047409057617s
Родительский документ закешированный
Количество обращений к БД3
Время отработки PHP0,078697204589844s
Дочерний документ не кешированный
Количество обращений к БД7
Время отработки PHP0,1879620552063s
Дочерний документ закешированный
Количество обращений к БД0
Время отработки PHP0,097842931747437s
В логах MODX какие-либо предупреждения отсутствуют

Множественная проверка кода прямо в шаблоне


Внутрь шаблона помещаем 1000 вызовов конструкции:

[[*isfolder:isnot=`1`:then=`<a href="/[[~[[*parent]]]]">test</a>`]]

и делаем замер по заранее определенной схеме и снимаем показатели:

Родительский документ не кешированный
Количество обращений к БД6
Время отработки PHP1,2022387981415s
Родительский документ закешированный
Количество обращений к БД0
Время отработки PHP0,073862075805664s
Дочерний документ не кешированный
Количество обращений к БД6
Время отработки PHP0,62936997413635s
Дочерний документ закешированный
Количество обращений к БД0
Время отработки PHP0,088212966918945s
В логах MODX предупреждение makeUrl() на родительском документе


Собрав все эти данные мы можем перейти к замерам работы нашего компонента, который так же будет состоять из трех блоков. Устанавливаем компонент из ветки master, в данной реализации используется плагин, который срабатывает на событие onParseDocument. Еще раз напомню, что компонент может быть применен исключительно к контенту размещенному в шаблонах.

Единичная проверка в шаблоне с выполнением условия


Перетаскиеваем сформированные 1000 ссылок в шаблон и оборачиваем в проверку:

[[^isfolder:is=`1`:then=`[[-`]]
<a href="/[[~[[*parent]]]]" data-doc="1">test</a>
…
<a href="/[[~[[*parent]]]]" data-doc="1000">test</a>
[[^isfolder:is=`1`:then=`]]`]]

, и делаем замер по заранее определенной схеме и снимаем показатели:

Родительский документ не кешированный
Количество обращений к БД6
Время отработки PHP0,12065505981445s
Родительский документ закешированный
Количество обращений к БД0
Время отработки PHP0,074366092681885s
Дочерний документ не кешированный
Количество обращений к БД6
Время отработки PHP3,3769309520721s
Дочерний документ закешированный
Количество обращений к БД0
Время отработки PHP0,1069130897522s
В логах MODX какие-либо предупреждения отсутствуют

Множественные проверки в шаблоне с выполнением условия


Вместо 1000 сформированных ссылок в шаблон помещаем 1000 конструкций, в которой ссылка проверяется по условию:

[[^isfolder:is=`1`:then=`[[-`]]
<a href="/[[~[[*parent]]]]" data-doc="1">test</a>
[[^isfolder:is=`1`:then=`]]`]]

, и делаем замер по заранее определенной схеме и снимаем показатели:

Родительский документ не кешированный
Количество обращений к БД6
Время отработки PHP0,68638801574707s
Родительский документ закешированный
Количество обращений к БД0
Время отработки PHP0,080582857131958s
Дочерний документ не кешированный
Количество обращений к БД6
Время отработки PHP3,8289330005646s
Дочерний документ закешированный
Количество обращений к БД0
Время отработки PHP0,084268093109131s
В логах MODX какие-либо предупреждения отсутствуют

Единичная проверка в шаблоне с чанком [[$testChunk]] с выполнением условия


Вместо 1000 конструкций возвращаемся к использованию нашего чанка, в котором у нас присутсвует 1000 сформированных ссылок. Используем в шаблоне один раз такую конструкцию:

[[^isfolder:is=`1`:then=`[[-`]]
[[$testChunk]]
[[^isfolder:is=`1`:then=`]]`]]

, и делаем замер по заранее определенной схеме и снимаем показатели:

Родительский документ не кешированный
Количество обращений к БД6
Время отработки PHP0,13211798667908s
Родительский документ закешированный
Количество обращений к БД0
Время отработки PHP0,078866004943848s
Дочерний документ не кешированный
Количество обращений к БД7
Время отработки PHP3,3375608921051s
Дочерний документ закешированный
Количество обращений к БД0
Время отработки PHP0,08203387260437s
В логах MODX какие-либо предупреждения отсутствуют

Желающие могут сравнить все данные в целом, я лишь обращу внимание, на несколько интересных моментов. Обратите внимание, что в первой таблице при использовании чанков запросов к БД было одинаковое количество, как у родителя, так и у потомков. В нашей последней таблице с замером работы чанка вместе с компонентом это не так, так как на родительском документе часть кода, которая не подходит по условию, реально не обрабатывается, а следовательно не дергается чанк и запросов у родителя на один меньше. Что хочется еще отметить, что при единичном использовании разницы во времени обработки практически нет. При множественной проверке компонент создает ощутимое влияние на систему, но если копнуть глубже, то создает данное влияние не сам компонент, а постоянный вызов события onParseDocument, который дергается при обработке любого типа контента. При этом, даже если сделать наш плагин пустым, то событие в любом случае будет дергаться и будет существенная задержка при генерации страницы. Можем ли мы с этим что-то сделать?

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

Для того, чтобы повесить компонент на событие или устанавливаем пакет из ветки onParseTemplate, который сам пропишет собственное событие. Или создаем нужное нам событие OnParseTemplate вручную.

$event = $modx->newObject('modEvent');
$event->set('name', 'OnParseTemplate');
$event->set('service', 5);
$event->set('groupname', 'System');
$event->save();

В любом случае после установки компонента или создания события, как ранее упоминалось, придется внести изменения в файл core/model/modx/modtemplate.class.php, добавив код вызова события в районе 114 строки:

if (is_string($this->_output) && !empty($this->_output)) {
	/* turn the processed properties into placeholders */
	$this->xpdo->toPlaceholders($this->_properties, '', '.', true);

меняем на:

if (is_string($this->_output) && !empty($this->_output)) {
	/* invoke OnParseTemplate event */
	$this->xpdo->invokeEvent('OnParseTemplate', array('content' => $this->_output));

	/* turn the processed properties into placeholders */
	$this->xpdo->toPlaceholders($this->_properties, '', '.', true);

Проверяем, что плагин у нас повешен на нужном событии OnParseTemplate, после чего делаем замеры из трех блоков, которые мы только что делали для этого же компонента на событии onParseDocument.

Единичная проверка в шаблоне с выполнением условия


Перетаскиеваем сформированные 1000 ссылок в шаблон и оборачиваем в проверку:

[[^isfolder:is=`1`:then=`[[-`]]
<a href="/[[~[[*parent]]]]" data-doc="1">test</a>
…
<a href="/[[~[[*parent]]]]" data-doc="1000">test</a>
[[^isfolder:is=`1`:then=`]]`]]

, и делаем замер по заранее определенной схеме и снимаем показатели:

Родительский документ не кешированный
Количество обращений к БД6
Время отработки PHP0,12673187255859s
Родительский документ закешированный
Количество обращений к БД0
Время отработки PHP0,088196992874146s
Дочерний документ не кешированный
Количество обращений к БД6
Время отработки PHP0,20241808891296s
Дочерний документ закешированный
Количество обращений к БД0
Время отработки PHP0,089241981506348s
В логах MODX какие-либо предупреждения отсутствуют

Множественные проверки в шаблоне с выполнением условия


Вместо 1000 сформированных ссылок в шаблон помещаем 1000 конструкций, в которой ссылка проверяется по условию:

[[^isfolder:is=`1`:then=`[[-`]]
<a href="/[[~[[*parent]]]]" data-doc="1">test</a>
[[^isfolder:is=`1`:then=`]]`]]

, и делаем замер по заранее определенной схеме и снимаем показатели:

Родительский документ не кешированный
Количество обращений к БД6
Время отработки PHP0,67841005325317s
Родительский документ закешированный
Количество обращений к БД0
Время отработки PHP0,077855110168457s
Дочерний документ не кешированный
Количество обращений к БД6
Время отработки PHP0,76762890815735s
Дочерний документ закешированный
Количество обращений к БД0
Время отработки PHP0,084574937820435s
В логах MODX какие-либо предупреждения отсутствуют

Единичная проверка в шаблоне с чанком [[$testChunk]] с выполнением условия


Вместо 1000 конструкций возвращаемся к использованию нашего чанка, в котором у нас присутсвует 1000 сформированных ссылок. Используем в шаблоне один раз такую конструкцию:

[[^isfolder:is=`1`:then=`[[-`]]
[[$testChunk]]
[[^isfolder:is=`1`:then=`]]`]]

, и делаем замер по заранее определенной схеме и снимаем показатели:

Родительский документ не кешированный
Количество обращений к БД6
Время отработки PHP0,12418699264526s
Родительский документ закешированный
Количество обращений к БД0
Время отработки PHP0,086171865463257s
Дочерний документ не кешированный
Количество обращений к БД7
Время отработки PHP0,21409296989441s
Дочерний документ закешированный
Количество обращений к БД0
Время отработки PHP0,08700704574585s
В логах MODX какие-либо предупреждения отсутствуют

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

Целью данного материала не было желание создания компонента, который бы устранил имеющуюся проблему с разбором условий модификторов в парсере MODX Revolution. В данном материале, я изначально хотел только разобраться, существует ли данная проблема и работают ли те пути ее решения, которые нам предлагаются. Как оказалось, проблема есть, а предлагаемые решения работают не совсем так, как мы бы хотели.

Благодаря этому я углубился в работу MODX парсера и сделал компонент, который, с моей точки зрения, позволяет обойти эту проблему. Однако я никого не призываю использовать этот компонент, хотя я его уже использую на продакшене, а поделился данным материалом скорее как небольшим и интересным исследованием, состоящим из хитросплетений последовательности обработки документов, вызовов событий, а так же замеров разных показателей исполнения. Надеюсь, кому-то будет так же интересно, как и мне, когда я в этом разбирался :)

Ну и как обычно, если будут предложения или пожелания по работе компонента, предлагайте :)

UPDATE: Решение с fenom парсером гораздо проще и эффективнее, спасибо всем, кто указал на это.

Думаю история была бы не полной, если бы я не добавил аналогичные замеры для Fenom парсера. Они будут представлены в двух вариантах без кеширования скомпилированных шаблонов и с кешированием скомпилированных шаблонов. Проверку мы будем делать по последнему тесту, то есть единичная проверка в шаблоне с чанком, т.е. сравнивать результаты можно с последней таблицей перед апдейтом. Для работы с fenom парсером устанавливаем последний pdoTools, после чего в системных настройках включаем pdotools_fenom_parser, чтобы парсер обрабатывал не только чанки, но и шаблоны. Больше пока ничего не трогаем.

Единичная проверка в шаблоне с чанком [[$testChunk]] с Fenom без кеширования


Вместо 1000 конструкций возвращаемся к использованию нашего чанка, в котором у нас присутсвует 1000 сформированных ссылок. Используем в шаблоне один раз такую конструкцию:

{if !$_modx->resource['isfolder']}
{include 'testChunk'}
{/if}

, и делаем замер по заранее определенной схеме и снимаем показатели:

Родительский документ не кешированный
Количество обращений к БД8
Время отработки PHP0,4661979675293s
Родительский документ закешированный
Количество обращений к БД2
Время отработки PHP0,13042497634888s
Дочерний документ не кешированный
Количество обращений к БД8
Время отработки PHP0,3633668422699s
Дочерний документ закешированный
Количество обращений к БД2
Время отработки PHP0,08700704574585s
В логах MODX какие-либо предупреждения отсутствуют

Единичная проверка в шаблоне с чанком [[$testChunk]] с Fenom с кешированием


Оставляем контент таким же как и на предыдущем шаге. Заходим в системные настройки и выставляем опцию pdotools_fenom_cache для кеширования скомпилированных шаблонов, и делаем замер по заранее определенной схеме и снимаем показатели:

Родительский документ не кешированный
Количество обращений к БД7
Время отработки PHP0,1616849899292s
Родительский документ закешированный
Количество обращений к БД1
Время отработки PHP0,13863492012024s
Дочерний документ не кешированный
Количество обращений к БД8
Время отработки PHP0,35265302658081s
Дочерний документ закешированный
Количество обращений к БД3
Время отработки PHP0,29361200332642s
В логах MODX какие-либо предупреждения отсутствуют

Без каких либо выводов, желающие могут сами сравнить результаты. Еще раз подчеркну, что я не являюсь специалистом по fenom и если сделал что-то не так, поправьте пожалуйста в комментариях.
Кудашев Сергей
12 марта 2018, 05:12
2
1 043
+10

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

Павел Гвоздь
12 марта 2018, 09:03
+3
Честно скажу, читать такую простыню текста довольно неприятно. Учитывая, что можно сделать значительно короче. Дочитал примерно до середины. Однако, поставил плюс, т.к. довольно важные вопросы поднимаются и похвально, что есть желание разобраться.

P.S. Опять же, замену нативному парсеру можно найти в шаблонизаторе Fenom.
    Кудашев Сергей
    12 марта 2018, 10:48
    0
    Павел, в начале специально предупреждаю, что будет довольно много текста. Прекрасно понимаю, что читать такую простыню очень сложно, но выкидывать части повествования не хотелось бы, так как это последовательность рассуждений, где одно вытекает из другого и если не описать все подробно, то не будет понятно, что за что цепляется и как мы приходим к какому-то препарсеру и почему в идеале его надо вешать на собственное событие.

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

    Сидит ворона с сыром, подходит лиса бьет бейсбольной битой, забирает сыр и уходит. Ворона сидит и думает: «Нормально так басню сократили!».

    В любом случае спасибо, постараюсь воспользоваться советами о том, как можно сделать значительно короче.

    P.S. не являюсь большим специалистом по Fenom, однако конструкция в шаблоне с включенным pdotools_fenom_parser:

    {var $isfolder = '[[*isfolder]]'}
    {if $isfolder > 0}
        Do nothing
    {else}
        [[$testChunk]]
    {/if}

    , выдаст на родительском документе ровно тоже самое предупреждение, с которого начинается повествование. То есть чанк обработается, вне зависимости от логики условия.
      Дмитрий
      12 марта 2018, 10:53
      +1
      А так?
      {if $_modx->resource.isfolder > 0}
       {include 'somechunk'}
      {/if}
      Павел Гвоздь
      12 марта 2018, 10:56
      +2
      Мда… :)

      не являюсь большим специалистом по Fenom, однако конструкция в шаблоне с включенным pdotools_fenom_parser:
      А вот так:
      {if $_modx->resource['isfolder']}
          Do nothing
      {else}
          {include 'testChunk'}
      {/if}
      ?
      Павел Гвоздь
      12 марта 2018, 10:59
      +2
      Вообще, нативный «шаблонизатор» очень кастрированный и использовать его на боевых сайтах я, уже как 2 года, считаю нецелесообразным. ИМХО, проще изучить Феном, чем копаться в этих дебрях и писать конструкции типа:
      [[#[[#[[#23.parent]].parent]].pagetitle]]
      или по тексту поста:
      [[[[*isfolder:isnot=`1`:then=`$testChunk`]]]]
        Кудашев Сергей
        12 марта 2018, 14:42
        +1
        по тексту поста [[[[*isfolder:isnot=`1`:then=`$testChunk`]]]] как раз писать не надо :)
      Pavel Zarubin
      12 марта 2018, 12:48
      0
      Так происходит потому что сначала запускается родной парсер а уже после его работы запускается парсер фенома, а родной парсер понятное дело не понимает ваших условий :)
Евгений Шеронов
12 марта 2018, 19:18
+4
Я, конечно, тоже за Fenom.

Но вот это решает же проблему, нужно только дописать дефисы:
[[[[*isfolder:isnot=`1`:then=`$testChunk`]]]] // из статьи
[[[[*isfolder:isnot=`1`:then=`$testChunk`:else=`--`]]]] // как Илья Уткин научил :)
Тогда никаких ошибок в лог и страшных конструкций вроде ...then=`[[-`…
    Кудашев Сергей
    12 марта 2018, 21:17
    +1
    У Ильи не видел, видимо проглядел. Спасибо, взял на заметку :)
Олег
15 марта 2018, 22:47
0
А если вместо {include… писать {insert… — это увеличит производительность? Кто нибудь замерял?
    Кудашев Сергей
    16 марта 2018, 00:17
    +1
    Олег, не совсем понятно, зачем Вам это надо, но это можно легко сделать самому. В системных настройках pdotools проверяете, чтобы опции pdotools_fenom_modx и pdotools_fenom_php были включены. Создаете пустой чанк testChunk, именно пустой, потому что если контент из него будет рендериться, придется долго ждать. После чего замеряете или от момента начала обработки входящего запроса MODX или само время обработки цикла. Корректнее конечно второй вариант, но первый тоже можно поглядеть, ради интереса:

    1. Посчитать время с момента начала обработки входящего запроса MODX:
    {var $start = $modx->startTime}
    
    {for $counter=1 to=10000}
       {include 'testChunk'}
    {/for}
    
    {$.php.microtime(true) - $start}

    2. Посчитать время выполнения только в блоке обработки цикла:
    {var $start = $.php.microtime(true)}
    
    {for $counter=1 to=10000}
       {include 'testChunk'}
    {/for}
    
    {$.php.microtime(true) - $start}

    Если не заметно, кто именно быстрее, увеличиваете значение to, пока не станет понятно :)

    P.S. Насколько вижу insert немного пошустрее :) Только это хоть как-то заметно от 10000 вызовов поэтому на реальном проекте Вы вряд ли заметите разницу.