Twig для EVO 1.4.x - первый подход

Twig пришел в cms Evolution еще 3 года назад, в 2018 году. Этот компонент и Первый урок, обозначенный как вводная часть, находится здесь
modx.evo.im/blog/docs/5602.html
Автор разработки Pathologic .

А недавно вышел второй резиз под названием EvoTwig2, с подобным уроком, с которым так же рекомендую ознакомиться, т.к. есть отличия. В частности, отсутствует фильтр modxParser.
github.com/Pathologic/EvoTwig2
Дополнение EvoTwig как раз и позволяет в cms Evolution применять шаблонизатор Twig версии 3.x.

Сам я Twig ранее не использовал, да и активности по его поводу на этом сайте тоже особо не замечалось. Но в свете создания Evo 3.0 и интегрированным в нее Laravel с шаблонизатором Blade, думаю что и к Twig может появиться интерес.

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

Исходные данные
1. Вводная часть по ссылкам выше.
2. Документация на сайте Twig
twig.symfony.com/doc/3.x/
3. Помощь сообщества в телеграм-канале.
4. Версия php не ниже 7.2.5
5. Версия Evo 1.4.х от 1.4.14. Если у вас более ранняя версия — надо сначала ее обновить.

1. Установка.

Ставится как обычное дополнение, можно и без composer.
А если кто будет пользоваться composer, то делать это надо из папки assets. Но обратите внимание, что папка vendor внутри assets есть, а файла composer.json нет. Поэтому нужно сначала создать этот файл и вписать в него
require:{
  "pathologic/evo-twig-lib": "*"
}

либо выполнить команду composer init.

2. Настройка окружения.

2.1. После установки надо перейти в Элементы — Плагины и включить плагин EvoTwig (по умолчанию он отключен), а так же перейти на вкладку Конфигурация и при необходимости сконфигурировать под себя. Но можно и ничего не менять, все будет работать.

2.2. Шаблоны в Twig — это обычные файлы, расширение может быть любым, а по умолчанию tpl, например home.tpl. По умолчанию файлы шаблонов хранятся по адресу assets/templates/tpl. Папка tpl вроде как должна создаваться автоматически, но у меня не создалась и сделал вручную.
В паке tpl можно создавать подпапки, я создал подпапку chunks для чанков.

2.3. Подключение шаблонов.
Шаблоны Twig к шаблонам Evo подключаются просто — в области Код шаблона (HTML) вписывается
@FILE:home
обратите внимание, что название шаблона Twig пишется без расширения.

3. Теги шаблонов.

У Twig всего 3 тега (разделителя):
Тег для обработки
{% %}
Тег для вывода результатов обработки
{{ }}
Тег для комментирования
{# #}
А вот то что можно применять внутри этих тегов представляет собой весьма внушительный функционал, с ним можно ознакомиться в Twig Reference по ссылке
twig.symfony.com/doc/3.x/
а некоторые его возможности рассмотрим ниже, и комментарии буду давать по ходу написания, чтобы не забылось раньше времени.

Дальше в статье я покажу, как перевести уже существующий сайт Evo на использование шаблонизатора Twig. Как известно, все познается в сравнении.

4. Шаблоны.

Шаблон Twig — это обычный html шаблон, так же как в в классическом evo, и в нем нужно запрограммировать меню, хлебные крошки, вывод документов, и т.д. и т.п. что обычно и делается в Evo.

4.1. Наследование шаблонов.
Удобно иметь базовый шаблон и дочерние шаблоны.
При этом в базовом шаблоне будут как сквозные разделы (одинаковые для всех страниц) — head со стилями и метатегами, шапка сайта с логотипом, контактами, меню и хлебными крошками, футер и скрипты сайта, так и разделы отличные друг от друга для разных шаблонов. Такие разделы в базовом шаблоне прописываются (обертываются) блоками, например, блок контентной части
{% block content %}{% endblock %}

У меня этот блок в базовом шаблоне пустой, но это совсем не обязательно, там может что то и быть, например
{% block content %}
	<div class="base">Блок базового шаблона</div>
{% endblock %}


4.2. Дочерний шаблон — это шаблон страницы, и в нем как раз и прописываются те блоки, которые будут подменяться в базовом шаблоне. А самым первым тегом должен быть тег extends с указанием базового шаблона. Этот тег используется для расширения шаблона от другого.

{% extends 'base.tpl' %}

Пример шаблона page.tpl для обычной текстовой страницы
{% extends 'base.tpl' %}
{% block content %}
	<div class="section">
		{{ resource.content | raw}}
	</div>
{% endblock %}

Этот блок content полностью заместит имеемый одноименный блок в базовом шаблоне.

Если нужно, чтобы содержимое блока content в базовом шаблоне тоже сохранилось, тогда в дочернем шаблоне этот блок прописывается так
{% block content %}
{{ parent() }}
	<div class="section">
		{{ resource.content | raw}}
	</div>
{% endblock %}

parent() — функция возвращает содержимое одноименного блока родительского шаблона.

{{ resource.content|raw }} — это вывод контента, то что в Evo соответствует [*content*].

Здесь:
resource — имя переменной
content — аргумент переменной
raw — фильтр
Дело в том что в шаблонах Twig работает автоматическое экранирование. Фильтр raw отмечает значение как «безопасный», что означает, что в блоке экранирования эта переменная будет выведена как не экранируема.
В дальнейшем это фильтр будет часто встречаться. Вы можете этот фильтр временно убрать и посмотреть в чем будет разница.

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

4.3. Базовый шаблон.
Для понимания приведу код — аналог одного из шаблонов template Evo
<!DOCTYPE html>
<html lang="ru">
{{head}}
<body>
<div class="page-wrapper">
{{header}}
{{titlebar}}
	<div class="section">
		[*content*]
	</div>
{{footer}}
</div>
{{js}}
</body>
</html>

У классического Evo нет базового шаблона, и поэтому для каждого типа страниц создаются свои индивидуальные шаблоны. А общие разделы таскаются из шаблона в шаблон копипастом, или выносятся в чанки.

В случае с Twig с приведенного шаблона делается базовый шаблон и код из чанком тоже вставляются в этот базовый шаблон, т.к. есть шаследование шаблонов.
В целях экономии места я дальше буду приводить код каждого чанка и варианты его реализации в Twig, и в конце приведу код базового шаблона полностью в итоговом варианте.
Касательно контентной части, то мы ее уже реализовали в болке content в п4.1 и 42.

5. Заголовок сайта head.

Сейчас это чанк head, убрав лишнее, он выглядит так
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=[(modx_charset)]">
	<title>[*titl*]</title>
	<meta name="keywords" content="[*keyw*]">
	<meta name="description" content="[*desc*]">
	<base href="[(site_url)]">
	<link href="assets/templates/css/vendor.min.css" rel="stylesheet">
	<link href="assets/templates/css/style.min.css" rel="stylesheet">
</head>

Здесь все просто TV параметры в шаблонах Twig теперь прописываем так

{{ resource.titl }}

а вывод из таблицы системных настроек так

{{ config.site_url }}

Но есть нюанс, например для метатега title — он должен быть обязательно заполнен, даже с точки зрения валидации, поэтому обычно в TV параметре titl присутствует значение по умолчанию, которое Twig не понимает, а фильтр modxParser в EvoTwig2 отсутствует, и таким образом уже не вывести

{{ resource.titl | modxParser }}

Поэтому нужно строго придерживаться инструментария Twig. Я сделал так

{{ resource.titl is empty? resource.pagetitle: resource.titl }}

Т.е. если админ по каким-то причинам не прописал метатег title, то в него выведется Заголовок страницы.
При инсталяции Evo значение по умолчанию этого TB уже «из коробки» прописано так

[*pagetitle*] — [(site_name)]

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

{% if resource.titl is empty %}
{{ resource.pagetitle }} — {{ config.site_name }}
{% else %}
{{ resource.titl }}
{% endif %}

или какими-то другими способами, арсенал весьма обширный.
В итоге у меня этот раздел в базовом шаблоне получился так
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=[(modx_charset)]">
	<title>{{ resource.titl is empty? resource.pagetitle: resource.titl }}</title>
	<meta name="keywords" content="{{ resource.keyw }}">
	<meta name="description" content="{{ resource.desc is empty? resource.introtext: resource.desc}}">
	<base href="{{ config.site_url }}">
	<link href="assets/templates/css/vendor.min.css" rel="stylesheet">
	<link href="assets/templates/css/style.min.css" rel="stylesheet">
</head>


6. Шапка сайта header.

Сейчас это чанк header, убрав лишнее, он выглядит так
<header>
    <div class="header-top">
	<div class="logo">
	    <a href="/" class="main-logo"><img src="assets/templates/images/logo.png" alt="[(site_name)]"></a>
	</div>
        <div class="top-left">
            <ul class="contact-list clearfix">
                <li><a href="[~7~]">[(company_city)]</a></li>
                <li><a href="mailto:[(company_mail)]">[(company_mail)]</a></li>
		<li><a href="tel:+[[phone? &phone=`[(company_phone)]`]]">[(company_phone)]</a></li>
           </ul>
	</div>
    </div>
<nav>
[[DLMenu?
&parents=`0`
&maxDepth=`3`
&hereClass=`current`
&outerTpl=`@CODE:<ul class="navigation clearfix">[+wrap+]</ul>`
&rowTpl=`@CODE:<li><a href="[+url+]" title="[+title+]"><span>[+title+]</span></a></li>`
&rowHereTpl=`@CODE:<li class="current"><a href="[+url+]" title="[+title+]"><span>[+title+]</span></a></li>`
&parentRowTpl=`@CODE:<li class="dropdown"><a href="[+url+]"><span>[+title+]</span></a>[+wrap+]</li>`
&parentRowHereTpl=`@CODE:<li class="current dropdown"><a href="[+url+]"><span>[+title+]</span></a>[+wrap+]</li>`
&parentRowActiveTpl=`@CODE:<li class="dropdown"><a href="[+url+]"><span>[+title+]</span></a>[+wrap+]</li>`
&innerTpl=`@CODE:<ul>[+wrap+]</ul>`]]
</nav>
</header>

Ссылки теперь прописываются так {{ makeUrl(7) }}.
Как вывести системные настройки из конфигурации мы уже знаем, аналогично и пользовательские настройки (дополнение Client Settings и другие аналогичные), т.к. они находятся там же.

Сниппет phone у меня предназначен для вырезания всего лишнего, оставляет только цифры. Можно использовать и сниппет, в Twig это можно сделать через так

{{ runSnippet('phone',{
'phone':'config.company_phone'
})

Но лучше воспользоваться возможностями Twig и сделать так

{{ config.company_phone|replace({'+':'', ' ':'', ')':'', '(':'', '- ':''}) }}

Это фильтр replace.

6.1. Сниппет DLMenu. Вариант первый.

{{ runSnippet('DLMenu',{
'parents':'0',
'maxDepth':'3',
'hereClass':'current',
'outerTpl':'@T_CODE:<ul class="navigation clearfix">{{ data.wrap|raw }}</ul>',
'rowTpl':'@T_CODE:<li><a href="{{ data.url }}" title="{{ data.title }}"><span>{{ data.title }}</span></a></li>',
'rowHereTpl':'@T_CODE:<li class="current"><a href="{{ data.url }}" title="{{ data.title }}"><span>{{ data.title }}</span></a></li>',
'parentRowTpl':'@T_CODE:<li class="dropdown"><a href="{{ data.url }}"><span>{{ data.title }}</span></a>{{ data.wrap|raw }}</li>',
'parentRowHereTpl':'@T_CODE:<li class="current dropdown"><a href="{{ data.url }}"><span>{{ data.title }}</span></a>{{ data.wrap|raw }}</li>',
'parentRowActiveTpl':'@T_CODE:<li class="dropdown"><a href="{{ data.url }}"><span>{{ data.title }}</span></a>{{ data.wrap|raw }}</li>',
'innerTpl':'@T_CODE:<ul>{{ data.wrap|raw }}</ul>'
   })|raw
}}

{{ runSnippet('DLMenu',{
'parents':'0',
'maxDepth':'2',
'hereClass':'current',
'outerTpl':'@CODE:<ul class="navigation clearfix">[+wrap+]</ul>',
'rowTpl':'@CODE:<li><a href="[+url+]" title="[+title+]"><span>[+title+]</span></a></li>',
'rowHereTpl':'@CODE:<li class="current"><a href="[+url+]" title="[+title+]"><span>[+title+]</span></a></li>',
'parentRowTpl':'@CODE:<li class="dropdown"><a href="[+url+]"><span>[+title+]</span></a>[+wrap+]</li>',
'parentRowHereTpl':'@CODE:<li class="current dropdown"><a href="[+url+]"><span>[+title+]</span></a>[+wrap+]</li>',
'parentRowActiveTpl':'@CODE:<li class="dropdown"><a href="[+url+]"><span>[+title+]</span></a>[+wrap+]</li>',
'innerTpl':'@CODE:<ul>[+wrap+]</ul>',
   })|raw
}}

В DocLister и дополнениях на его основе чанки подключаются с префиксом T_, а плейсхолдеры — как переменная data с соответствующими аргументами, что и показано в примере.
Уточнение

Обратите так же внимание на использование фильтра raw.

Все работает, но не особо впечатляет.

6.2. Сниппет DLMenu. Вариант второй.

Передадим runSnippet DLMenu в переменную и откажемся от этой кучи чанков, воспользовавшись циклом for наблона Twig.

{% set DL = runSnippet('DLMenu', {
'parents':'0',
'maxDepth':'2',
'returnDLObject':'1'
}) %}

Здесь тег set — это присвоить значение переменной. Здесь оговорюсь, вообще-то по стандартам Twig название переменной должно быть в нижнем регистре, можно использовать подчеркивание. Есть и другие требования, желательно ознакомиться.
twig.symfony.com/doc/3.x/coding_standards.html

До того как перейти к циклу for, рекомендуется убедиться, что все вывелось правильно — из указанного parents, и посколку это меню, то должны быть title, поскольку в меню 2 уровня, то дожны быть children, и т.п. Делается так

{{ dump(DL.getMenu()) }}

Если все отработало нормально, dump можно удалить или закомментировать, может еще пригодится

{# {{ dump(DL.getMenu()) }} #}

Теперь переходим к циклцу for, в итоговом варианте он у меня получился так
{% for data in DL.getMenu()[0] %}
	<li class="{% if data.children != 0 %}dropdown{% endif %}"><a href="{{ data.url }}" title="{{ data.title }}"><span>{{ data.title }}</span></a>
{% if data.children %}
	<ul>
{% for data in data.children %}
		<li class="dropdown"><a href="{{ data.url }}"><span>{{ data.title }}</span></a></li>
{% endfor %}
	</ul>
{% endif %}
	</li>
{% endfor %}

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

Здесь в основном цикле код из шаблона parentRowTpl, который при отсутствии потомков автоматически превращается в rowTpl. Дальше идет проверка на наличие потомков, и если они есть — то они выводятся.

Вот собственно и все. Значительно проще и компактнее и первого варианта, и того что мы пишем в шаблонах и чанках классического Evo.
Расставить классы можно по примеру того, как я сделал для класса dropdown

{% if data.children != 0 %}dropdown{% endif %}

7. Раздел заголовка h1 и хлебных крошек titlebar.

Сейчас это чанк titlebar, убрав лишнее, он выглядит так
<section class="page-title">
            <h1>[*pagetitle*]</h1>
[[DLCrumbs? &showCurrent=`1` 
&ownerTPL=`@CODE:<ul class="page-breadcrumb" itemscope itemtype="http://schema.org/BreadcrumbList">
        		[+crumbs.wrap+]
    		</ul>`
&tplFirst=`@CODE:<li itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem">
    			<meta itemprop="position" content="[+iteration+]" />
    			<a href="[+url+]" title="[(site_name)]" itemprop="item">[(site_name)]</a>
		</li>`]]
    </section>

В случае с DLCrumbs просто поменять плэйсхолдеры и вызвать через runSnippet не получится. Сразу скажу — аналогичная ситуация и с пагинацией для DocLister, о чем еще будет ниже.
Уточнение

Поэтому нужно воспользоваться конфигурационными файлами, примеры которых находятся по адресу assets/snippets/config/core
Можно сразу скопировать папку и/или переименовать ее в custom и отредактировать в ней файл crumbs.json с классическими плейсхолдерами evo под свой дизайн.
В итоге вызов сниппета DLCrumbs в шаблоне Twig

{{ runSnippet('DLCrumbs',{
'showCurrent':'1',
'config':'crumbs'
})|raw
}}

Файл с названием crumbs (расширение не указывается) берется именно из папки assets/snippets/config/coreassets/snippets/config/core
Но для теста можно подключить и файл из папки core таким образом
'config':'crumbs:core'
В реальном варианте так лучше не делать, т.к. при последующих обновлениях может затереться.

8. Итоговый базовый шаблон

<!DOCTYPE html>
<html lang="ru">
  <head>
	<meta http-equiv="Content-Type" content="text/html; charset=[(modx_charset)]">
	<title>{{ resource.titl is empty? resource.pagetitle: resource.titl }}</title>
	<meta name="keywords" content="{{ resource.keyw }}">
	<meta name="description" content="{{ resource.desc is empty? resource.introtext: resource.desc}}">
	<base href="{{ config.site_url }}">
	<link href="assets/templates/css/vendor.min.css" rel="stylesheet">
	<link href="assets/templates/css/style.min.css" rel="stylesheet">
</head>
<header>
	<div class="header-top">
		<div class="logo">
			<a href="/" class="main-logo"><img src="assets/templates/images/logo.png" alt="{{ config.site_name }}"></a>
		</div>
        <div class="top-left">
            <ul class="contact-list clearfix">
                <li><a href="{{ makeUrl(7) }}">{{ config.company_city }}</a></li>
                <li><a href="mailto:{{ config.company_mail }}">{{ config.company_mail }}</a></li>
		<li><a href="tel:+{{ config.company_phone|replace({'+':'', ' ':'', ')':'', '(':'', '- ':''}) }}">{{ config.company_phone }}</a></li>
           </ul>
		</div>
	</div>
	<nav>
{% set DL = runSnippet('DLMenu', {
    'parents':'0',
    'maxDepth':'2',
    'returnDLObject':'1'
}) %}
{% for data in DL.getMenu()[0] %}
	<li class="{% if data.children != 0 %}dropdown{% endif %}"><a href="{{ data.url }}" title="{{ data.title }}"><span>{{ data.title }}</span></a>
{% if data.children %}
	<ul>
{% for data in data.children %}
		<li class="dropdown"><a href="{{ data.url }}"><span>{{ data.title }}</span></a></li>
{% endfor %}
	</ul>
{% endif %}
	</li>
{% endfor %}
	</nav>
</header>
<section class="page-title">
    <h1>{{ resource.pagetitle }}</h1>
{{ runSnippet('DLCrumbs',{
'showCurrent':'1',
'config':'crumbs'
   })|raw
}}
</section>
{% block content %}{% endblock %}
<footer>
	<div class="copyright-text">© {{ config.site_name }} 2020</div>
</footer>
{% block js %}
<script src="assets/templates/js/vendor.min.js"></script>
<script src="assets/templates/js/script.js"></script>
{% endblock %}
</body>
</html>

Скрипты я обернул в блок Twig, т.к. в некоторых дочерних шаблонах нужно добавить дополнительный скрипт.
  • avatar
  • 1
  • +2
  • 257

14 комментариев

avatar
9. Шаблон новости article.tpl
{% extends 'base.tpl' %}
{% block content %}
<div class="new-article">
<img src="
{{ runSnippet('phpthumb',{
     'input':resource.image,
	 'options':'w=1024,h=576,zc=1,bg=ffffff'
   })
}}
" alt="{{ resource.pagetitle }}">
<div class="date"><i class="far fa-calendar"></i>
	{{ resource.createdon|date("d.M.Y H:i") }}
</div>
<div class="content-area">
	{{ resource.content | raw}}
</div>
</div>
{% endblock %}

В Evo для вывода даты на странице новости обычно используется сниппет, в шаблоне Twig для этого сниппет не нужен — имеется функция date.

Вместо даты создания можно использовать дату публикации. Можно выводить и по условию, как в примере по метатегу title в п.5.
avatar
10. Шаблон общей страницы новостей blog.tpl

Это и как пример для любой другой страницы, где используется Doclister.
Как уже ранее отмечал, пагинация Doclister в шаблоне Twig не работает, поэтому надо отредактировать и подключить конфигурацию paginate.json в папке custom, которую уже сделали в п.7.

Если у вас на сайте много страниц с пагинациями, сделанных на базе Doclister или sgLister, то и в классическом Evo пагинацию лучше, удобнее и быстрее делать через конфигурацию.

С учетом вышеизложенного, сразу даю пример вызова Doclister с параметром returnDLObject =1 для создания объекта и создания страницы блога с помощью циклов for шаблона Twig
{% extends 'base.tpl' %}
{% block content %}
<div class="section">
    <div class="row">
{% set DL = runSnippet('DocLister', {
    'tvPrefix':'',
    'parents':'5',
    'display':'3',
    'tvList':'image',
    'paginate':'pages',
    'pageLimit':'1',
    'pageAdjacents':'1',
    'config':'paginate',
    'returnDLObject':'1'
}) %}
{% for data in DL.getDocs %}
	<div class="col-lg-3">
	    <div class="image-box">
<img src="
{{ runSnippet('phpthumb',{
    'input':data.image,
    'options':'w=500,h=350,zc=1,bg=ffffff'
   })
}}
" alt="{{ data.pagetitle }}"></a>
	    </div>
	    <div class="content-area">
		<p class="date">{{ data.createdon|date("d.M.Y H:i") }}</p>
                <a href="{{ makeUrl(data.id) }}">{{ data.pagetitle }}</a>
	    </div>
            <div class="text">
{% set content = resource.content|striptags|replace({' ':' '}) %}
{{ data.introtext is empty? content|length > 75 ? content|slice(0, 75) ~ '...': content:  data.introtext}}
	    </div>
	    <a href="{{ makeUrl(data.id) }}">Подробнее</a>
{% else %}
	    <div class="alert">В этом разделе еще нет публикаций!</div>
{% endfor %}
	    </div>
	</div>
	<div class="default-pagination">
	    {{ plh['pages']|raw }}
	</div>
</div>
{% endblock %}

Пояснения.
1. Часть параметров, указанных в вызове DocLister и относящихся к пагинации, тоже можно перенести в конфигурационный файл.
2. Препаре и summary в приведенном варианте не работают. В обычном варианте runSnippet они работают.
3. Поскольку summary не работает, то реализовать функцию обрезания текста можно средствами Twig, как в примере, или с помощью других фильтров, таковые у Twig есть еще.
4. Если вы используете TV параметрами с префиксами, например tv.image, то такой синтаксис
data.tv.image
применять нельзя, будет ошибка. В этом случае (и в других подобных, и не только в DocLister) используйте
data['tv.image']

В примере
{% set content = resource.content|striptags|replace({' ':' '}) %}
{{ data.introtext is empty? content|length > 75? content|slice(0, 75) ~ '...': content: data.introtext}}

так же как и в summary выводится introtext, а если это поле не заполнено, то применяется фильтр и выводится част текста из content, 75 — количество символов, можно изменить на нужное.
avatar
avatar
11. Шаблон страницы с фотогалереей gallery.tpl

С фотогалереей SimpleGallery и ее сниппетом sgLister все примерно так же, как и с DocLister, хотя данные и хранятся в базе данных в другой таблице.
{% extends 'base.tpl' %}
{% block content %}
<div class="gallery-container">
{% set DL = runSnippet('sgLister', {
    'parents':resource.id,
    'returnDLObject':'1'
}) %}
{% for data in DL.getDocs() %}
    <figure class="image">
	<a href="{{ data.sg_image }}" class="lightbox-image" data-fancybox="object" data-caption="{{ data.sg_title }}">
	    <img src="{{ runSnippet('phpthumb',{'input':data.sg_image, 'options':'w=770,h=434,zc=1,bg=ffffff'}) }}" alt="{{ data.sg_title }}">
	</a>
    </figure>
{% endfor %}
</div>
{# {{ dump(DL.getDocs()) }} #}
{% endblock %}
{% block js %}
{{ parent() }}
<script defer src="assets/templates/js/custom.js"></script>
{% endblock %}

Здесь второй блок под названием js — это пример подключения на страницу дополнительных скриптов.
Если надо, аналогично делается и для стилей в head, но только тогда и в базовом шаблоне надо, чтобы стили или весь head был в одноименном блоке, как блок js.

Пояснения по sgLister.
Для приведенного выше варианта пояснений нет.
Но есть нюансы, если использовать в таком варианте
{{ runSnippet('sgLister',{
'thumbSnippet':'phpthumb', 
'thumbOptions':'w=770,h=434,zc=1,bg=ffffff',
'parents':'40',
'sgOrderBy':'sg_index DESC',
'display':'15',
'tpl':'@T_CODE:<figure class="image"><a href="{{ data.sg_image }}" class="lightbox-image" data-fancybox="object" data-caption="{{ data.sg_title }}"><img src="{{ data['thumb.sg_image'] }}" alt="{{ data.sg_title }}"></a></figure>',
   }) | raw
}}

Twig не понимает переменную, и я не понял почему, если аргумент прописан не через точку, а через индекс, т.е. заключен в квадратные скобки
{{ data['thumb.sg_image'] }}
А другого варианта вроде как и нет, так тоже нельзя
{{ data.thumb.sg_image'] }}
Причем это только для sgLister, у других такого нет и квадратные скобки у других идут на ура.
В итоге, тестируя этот вариант, я отключил параметры встроенного авторесайза и подключил свой препаре, и так сработало
{{ runSnippet('sgLister',{
'prepare':'imgformat',
'tvImg':'sg_image',
'phpthumb':'w=770,h=434,zc=1,bg=ffffff',
'parents':'40',
'sgOrderBy':'sg_index DESC',
'display':'15',
'tpl':'@T_CODE:<figure class="image"><a href="{{ data.sg_image }}" class="lightbox-image" data-fancybox="object" data-caption="{{ data.sg_title }}"><img src="{{ data.thumb }}" alt="{{ data.sg_title }}"></a></figure>',
   }) | raw
}}

Параметр tvImg — это из состава препаре imgformat, для универсальности, в доках его искать не надо.
avatar
Комментарий отредактирован 2021-03-20 19:41:58 пользователем paic
avatar
12. Чанки и символы типа @CODE в EvoTwig

Это исправления и дополнения к предыдущим пунктам статьи касательно Doclister и его производных, поскольку не все так, а где-то и совсем не так, как было написано выше.

Получается так, что если используем в чанке Twig, то надо применять
@T_CODE:
и внутри чанка data c аргументами от твига.

А если используем чанк Evo, по надо применять
@CODE:
и внутри чанка плейсхолдеры от Evo.

12.1. Практически это означает, что в чанке tpl Doclister надо применять @T_CODE:, а в чанках пагинации и другие @CODE:
{{ runSnippet('DocLister',{
'tvPrefix':'',
'display':'2',
'prepare':'imgformat',
'summary':'notags,len:40',
'tvImg':'image',
'phpthumb':'w=500,h=350,zc=1,bg=ffffff',
'depth':'0',
'parents':'5',
'dateFormat':'%d.%m.%Y',
'dateSource':'createdon',
'tvList':'image',
'paginate':'pages',
'id':'cat',
'pageLimit':'1',
'pageAdjacents':'1',
'tpl':'@T_CODE:<div class="col-lg-3">
	<div class="image-box">
             <img src="{{ data.thumb }}" alt="{{ data.pagetitle }}">
	</div>
	<div class="content-area">
	     <p class="date">{{ data.date }}</p>
             <a href="{{ data.url }}">{{ data.pagetitle }}</a>
             <div class="text">{{ data.summary | raw}}...</div>
		<a href="{{ data.url }}"></a>
             </div>
         </div>',
'noneTPL':'@CODE:<div class="alert">В этом разделе еще нет публикаций!</div>',
'TplPrevP':'@CODE: <li><a href="[+link+]" rel="prev"><i class="fa fa-angle-left"></i></a></li>',
'TplPage':'@CODE: <li><a href="[+link+]" class="page">[+num+]</a></li>',
'TplCurrentPage':'@CODE: <li class="active"><a>[+num+]</a></li>',
'TplNextP':'@CODE: <li><a href="[+link+]" rel="next"><i class="fa fa-angle-right"></i></a></li>',
'TplDotsPage':'@CODE:<li><a href="[+link+]" class="page"> ... </a></li>',
'TplWrapPaginate':'@CODE: <ul class="pagination">[+wrap+]</ul>'
   }) | raw
}}


12.2. В чанках sgLister - @CODE
{{ runSnippet('sgLister',{
'thumbSnippet':'phpthumb', 
'thumbOptions':'w=770,h=434,zc=1,bg=ffffff',
'parents':'40',
'sgOrderBy':'sg_index DESC',
'display':'15',
'tpl':'@CODE:<figure class="image"><a href="[+sg_image+]" class="lightbox-image" data-fancybox="object" data-caption="[+e.sg_title+]"><img src="[+thumb.sg_image+]" alt="[+e.sg_title+]"></a></figure>',
   }) | raw
}}


12.3. В чанках DLMenu — @CODE
{{ runSnippet('DLMenu',{
'parents':'0',
'maxDepth':'3',
'hereClass':'current',
'outerTpl':'@CODE:<ul class="navigation clearfix">[+wrap+]</ul>',
'rowTpl':'@CODE:<li><a href="[+url+]" title="[+title+]"><span>[+title+]</span></a></li>',
'rowHereTpl':'@CODE:<li class="current"><a href="[+url+]" title="[+title+]"><span>[+title+]</span></a></li>',
'parentRowTpl':'@CODE:<li class="dropdown"><a href="[+url+]"><span>[+title+]</span></a>[+wrap+]</li>',
'parentRowHereTpl':'@CODE:<li class="current dropdown"><a href="[+url+]"><span>[+title+]</span></a>[+wrap+]</li>',
'parentRowActiveTpl':'@CODE:<li class="dropdown"><a href="[+url+]"><span>[+title+]</span></a>[+wrap+]</li>',
'innerTpl':'@CODE:<ul>[+wrap+]</ul>',
   })|raw
}}


12.4. В чанках DLCrumbs — @CODE
{{ runSnippet('DLCrumbs',{
'showCurrent':'1', 
'ownerTPL':'@CODE:<ul class="page-breadcrumb" itemscope itemtype="http://schema.org/BreadcrumbList">
        [+crumbs.wrap+]
    </ul>',
'tplFirst':'@CODE:<li itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem">
    <meta itemprop="position" content="[+iteration+]" />
    	<a href="[+url+]" title="[(site_name)]" itemprop="item">[(site_name)]</a>
    </li>'
   })|raw
}}


12.5. Даже если применять T_CODE с переменными в DLMenu, sgLister и при этом сниппет срабатывает, все равно этого делать не надо — это лишняя обработка для сниппета и возможны баги, как например, у меня в sgLister не срабатывал встроенный авторесайз.
avatar
13. Контроллеры в EvoTwig

В EvoTwig базовый контроллер BaseController уже есть и находится по адресу assets/plugins/evotwig/vendor/pathologic/evo-twig-lib/src/BaseController.php
Здесь сразу оговорюсь.
Так получилось, что я изначально EvoTwig устанавливал из Extras, а чуть позже — обновлял через composer. В результате на сайте получилось два дубля — один в папке plugins, а другой — в папке vendor. Конфликтов никаких не возникало и ошибок тоже, я даже забыл за этот эпизод. Но Twig помнил. И когда я дошел до контроллеров, это мне вылезло боком — я пытался работать с тем базовым контроллером, что в плагине (ссылка выше), а фактически он не работал, работал тот, что в папке vendor, и при этом не расширялся для дочерних контроллеров.
Может так и надо, может где-то не то сделал, в общем — первый подход.
Поэтому откатился назад и сделал новую установку EvoTwig из Extras.
И все заработало.

13.1. Базовый контроллер

Но производить какие-то действия с базовым контроллером по штатному месту (ссылка выше) нельзя — при обновлении или затрется, или как случилось в моем случае. Поэтому базовый контроллер я перенес по адресу
assets/plugins/evotwig/addons/functions/BaseController.php
Чтобы была возможность с ним работать я добавил в класс BaseController 2 функции
public function setGlobalData()
    {
	// для использования в базовом шаблоне и всех других
    }
    protected function setPageData()
    {
	// для переопределения в контроллерах для дочерних шаблонов
    }

Соответственно в функцию __construct добавил
$this->setGlobalData();
$this->setPageData();
и на будущее (пригодится)
$this->docid = $this->modx->documentIdentifier;
Получилось так:
public function __construct(\DocumentParser $modx, array $params = [])
    {
        $this->modx = $modx;
        $this->params = $params;
	$this->docid = $this->modx->documentIdentifier;
	$this->setGlobalData();
	$this->setPageData();
    }

Пример вывода меню был в п.6.2, с контроллером теперь будет так:
1. В базовом контроллере BaseController
public function setGlobalData()
    {
	$this->data['mymenu'] = json_decode($this->modx->runSnippet('DLMenu', ['parents' => 0, 'maxDepth' => 2, 'api' => 1]), true)[0];
    }

Полностью видоизмененный базовый контроллер в папке functions выглядит так
<?php

namespace Pathologic\EvoTwig;

class BaseController implements ControllerInterface
{
    protected $modx;
    protected $data = [];
    protected $params = [];
    protected $template = '';

    /**
     * BaseController constructor.
     * @param  \DocumentParser  $modx
     * @param  array  $params
     */
    public function __construct(\DocumentParser $modx, array $params = [])
    {
        $this->modx = $modx;
        $this->params = $params;
	$this->docid = $this->modx->documentIdentifier;
	$this->setGlobalData();
	$this->setPageData();
    }

    /**
     * @param  array  $data
     */
    public function setTemplateData(array $data = [], $replace = false)
    {
        $this->data = $replace ? $data : array_merge($this->data, $data);
    }

    /**
     * @return array
     */
    public function getTemplateData(): array
    {
        return $this->data;
    }

    /**
     * @param string $template
     */
    public function setTemplate($template)
    {
        $this->template = $template;
    }

    /**
     * @return string
     */
    public function getTemplate(): string
    {
        $template = $this->template;
        if (empty($template)) {
            $dir = MODX_BASE_PATH . $this->params['templatesPath'];
            $tplExt = $this->params['templatesExtension'];
            $documentObject = $this->modx->documentObject;
            switch (true) {
                case file_exists($dir . 'tpl-' . $documentObject['template'] . '_doc-' . $documentObject['id'] . '.' . $tplExt):
                {
                    $template = 'tpl-' . $documentObject['template'] . '_doc-' . $documentObject['id'];
                    break;
                }
                case file_exists($dir . 'doc-' . $documentObject['id'] . '.' . $tplExt):
                {
                    $template = 'doc-' . $documentObject['id'] . '.' . $tplExt;
                    break;
                }
                case file_exists($dir . 'tpl-' . $documentObject['template'] . '.' . $tplExt):
                {
                    $template = 'tpl-' . $documentObject['template'];
                    break;
                }
            }
        }

        return $template;
    }

    /**
     * @return string
     */
    public function render(): string
    {
        $template = $this->getTemplate();
        $out = '';
        if (!empty($template)) {
            $tpl = $this->modx->twig->load($this->getTemplate() . '.' . $this->params['templatesExtension']);
            $out = $this->modx->twig->render($tpl, $this->getTemplateData());
        }

        return $out;
    }

	 public function setGlobalData()
    {
	$this->data['mymenu'] = json_decode($this->modx->runSnippet('DLMenu', ['parents' => 0, 'maxDepth' => 2, 'api' => 1]), true)[0];
	// сюда же добавляются и другие 
    }

    protected function setPageData()
    {
	// оставлять пустым
    }
}


2. В базовом шаблоне base.tpl теперь можно оставить только цикл for
{% for data in mymenu %}
        <li class="{% if data.children != 0 %}dropdown{% endif %}"><a href="{{ data.url }}" title="{{ data.title }}"><span>{{ data.title }}</span></a>
{% if data.children %}
        <ul>
{% for data in data.children %}
                <li class="dropdown"><a href="{{ data.url }}"><span>{{ data.title }}</span></a></li>
{% endfor %}
        </ul>
{% endif %}
        </li>
{% endfor %}


13.2. Контроллер для дочерних страниц

У меня на сайте есть два подобных шаблона для страниц каталог услуг и категория услуг. И в одном шаблоне, и в другом — DocLister, отличие только в чанках и дизайне.
Я для них сделал один контроллер под названием ServiceController, загружен он в папку assets/plugins/evotwig/addons/functions/ рядом с базовым контроллером и выглядит так
<?php
namespace Pathologic\EvoTwig;

class ServiceController extends BaseController
{

    protected function setPageData()
    {
	$this->data['service'] = json_decode($this->modx->runSnippet('DocLister', ['tvPrefix' => '', 'parents' => $this->docid, 'depth' => 0, 'tvList' => 'image', 'api' => 1]), true);
    }
}


Подключается контроллер к страницам следующим образом
В шаблоне каталога услуг
@FILE:service@ServiceController

В шаблоне категория услуг
@FILE:subservice@ServiceController


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

В шаблоне Категория услуг subservice.tpl эти самые услуги выводятся так

{% extends 'base.tpl' %}
{% block content %}
    <div class="service-section">
        <div class="row">
{% for data in service %}
	    <div class="col-lg-6">
		<div class="inner-box">
		    <div class="image-box">
			<a href="{{ makeUrl(data.id) }}"><img src="
{{ runSnippet('phpthumb',{
     'input':data.image,
	 'options':'w=500,h=500,zc=1,bg=ffffff'
   })
}}
			" alt="[+pagetitle+]"></a>
		    </div>
		    <div class="info-box">
			<a href="{{ makeUrl(data.id) }}">{{ data.pagetitle }}</a>
			<a href="{{ makeUrl(data.id) }}">Подробнее</a>
		    </div>
		</div>
	    </div>
{% endfor %}
	    </div>
	    <div class="content">
		{{ resource.content | raw}}
	    </div>
	</div>
{% endblock %}


В шаблоне Каталог услуг service.tpl — все аналогично, немного другой html и размеры картинок, приводить не буду.

Собственно, все это очень похоже на Laravel с Blade, только без Laravel и на версии EVO 1.4.х.
avatar
Но производить какие-то действия с базовым контроллером по штатному месту (ссылка выше) нельзя — при обновлении или затрется, или как случилось в моем случае. Поэтому базовый контроллер я перенес по адресу
Поэтому базовый контроллер нужно расширять, а не переносить и дописывать, ломая автозагрузку.
avatar
Для setPageData, т.е. для контроллеров дочерних шаблонов понятно, можно расширить напрямую «штатный» базовый контроллер
<?php
namespace Pathologic\EvoTwig;

class ServiceController extends BaseController
{
    public function __construct(\DocumentParser $modx, array $params = [])
    {
	$this->modx = $modx;
        $this->params = $params;
	$this->docid = $this->modx->documentIdentifier;
	$this->setPageData();
    }
    protected function setPageData()
    {
	$this->data['service'] = json_decode($this->modx->runSnippet('DocLister', ['tvPrefix' => '', 'parents' => $this->docid, 'depth' => 0, 'tvList' => 'image', 'api' => 1]), true);
    }
}


А как расширить, чтобы работал setGlobalData?
avatar

class GlobalController extends BaseController {
    public function __construct(\DocumentParser $modx, array $params = [])
    {
        parent::__construct($modx, $params);
        $this->setTemplateData([
            'service' => 11111
        ]);
    } 
}

А дочерние расширяют GlobalController таким же образом.
avatar
Спасибо.
Подскажите еще, из какой папки правильно обновлять через Composer.
Может не из папки assets, потому что дубль создается. А из папки evotwig? Там уже и composer.json есть.
avatar
Создается и создается, что такого.
avatar
14. Контроллеры в EvoTwig, исправления и дополнения к п.13.

Заключительный алгоритм при работе с контроллерами, начиная с установки:

1. Создаем в папке assets файл composer.json, внутри файла помещаем
{
	"require":{
		"pathologic/evo-twig-lib": "*"
	}
}

2. Запускаем Composer и переходим в папку assets (вариант для Open Server)
cd domains/site.ru/assets
где site.ru — это ваш домен.

3. Запускаем команду и ждем ее выполнения
composer require pathologic/evo-twig-lib

4. Устанавливаем плагин EvoTwig2 из Extras (или вручную), включаем его в админке (по умолчанию он выключен.

5. Создаем нужные шаблоны в папке tpl и подключаем их в админке к нужным шаблонам Evo. Проверяем работоспособность.

6. Для работы с контроллерами создаем свои контроллеры (глобальный и дочерние для шаблонов) и помещаем их в отдельную папку вне папки Vendor. Я их разместил в папке по адресу assets/plugins/evotwig/addons/controllers/

Глобальный контроллер — расширяет базовый контроллер, а дочерние контроллеры для шаблонов должны расширять глобальный контроллер. Т.е. получается такая цепочка: базовый контроллер — глобальный контроллер — дочерний контроллер.
Дочерний контроллер подключается к шаблону, например @FILE:service@ServiceController, где service — название шаблона, а ServiceController — дочерний контроллер.
Котроллер GlobalController никуда подключать не надо.

В конфигурации плагина EvoTwig в поле Base controller class вместо прописанного там по умолчанию Pathologic\EvoTwig\BaseController указываем класс своего контроллера. У меня это Controllers\GlobalController

Пример Глобального контроллера, который расширяет штатный Базовый контроллер EvoTwig
<?php

namespace Controllers;

use Pathologic\EvoTwig\BaseController;

class GlobalController extends BaseController 
{
    public function __construct(\DocumentParser $modx, array $params = [])
    {
        parent::__construct($modx, $params);
	$this->docid = $this->modx->documentIdentifier;
        $this->setTemplateData([
            'mymenu' => json_decode($this->modx->runSnippet('DLMenu', ['parents' => 0, 'maxDepth' => 2, 'api' => 1]), true)[0]
	    // здесь можно добавлять и другие переменные, через запятую
        ]);
    } 
}

Пример дочерних контроллеров, которые расширяют Глобальный контроллер
<?php

namespace Controllers;

class ServiceController extends GlobalController
{
    public function __construct(\DocumentParser $modx, array $params = [])
    {
        parent::__construct($modx, $params);
        $this->setTemplateData([
	    'service' => json_decode($this->modx->runSnippet('DocLister', ['tvPrefix' => '', 'parents' => $this->docid, 'depth' => 0, 'tvList' => 'image', 'api' => 1]), true)
	    // здесь можно добавлять и другие переменные, через запятую
        ]);
    }
}


Как вариант, можно и так:
Глобальный контроллер
<?php

namespace Controllers;

use Pathologic\EvoTwig\BaseController;

class GlobalController extends BaseController 
{
    public function __construct(\DocumentParser $modx, array $params = [])
    {
        parent::__construct($modx, $params);
	$this->docid = $this->modx->documentIdentifier;
	$this->setGlobalData();
        $this->setPageData();
    } 
    public function setGlobalData()
    {
        $this->data['mymenu'] = json_decode($this->modx->runSnippet('DLMenu', ['parents' => 0, 'maxDepth' => 2, 'api' => 1]), true)[0];
        // сюда же добавляются и другие глобальные переменные
    }

    protected function setPageData()
    {
        // оставлять пустым, используется для дочерних контроллеров
    }
}

Дочерний контроллер
<?php

namespace Controllers;

class ServiceController extends GlobalController
{
    protected function setPageData()
    {
        $this->data['service'] = json_decode($this->modx->runSnippet('DocLister', ['tvPrefix' => '', 'parents' => $this->docid, 'depth' => 0, 'tvList' => 'image', 'api' => 1]), true);
	// сюда же добавляются и другие дочерние переменные
    }
}

7. В файл composer.json помещаем код автозагрузки для созданных контроллеров
{
	"require":{
		"pathologic/evo-twig-lib": "*"
	},
	"autoload":{
		"psr-4": {
            "Controllers\\": "plugins/evotwig/addons/controllers/"
        }
    }
}


8. В Composer запускаем команду
composer update

9. Подключаем дочерние контроллеры к нужным шаблонам и проверяем работоспособность сайта.

10. Обновление
Все что выше в п.1-9 — это для текущей версии EvoTwig 2.2.0, которая вышла 31.03.21.
В предыдущей версии все то же самое, за исключением п.6 в части конфигурации плагина EvoTwig.
Ранее в конфигурации не было поля Base controller class и нужно было в поле Controllers namespace прописывать namespace своего глобального контроллера, у меня это Controllers.

Теперь порядок обновления:
— Обновляем плагин EvoTwig2.
— В конфигурации плагина удаляем из поля Controllers namespace неймспейс своего контроллера. А в поле Base controller class вместо прописанного там по умолчанию Pathologic\EvoTwig\BaseController прописываем класс своего контроллера, например, у меня это Controllers\GlobalController.
— выполняем пункты 7 и 8, при этом обновятся и evo-twig-lib — иначе обновления плагина не вступят в силу, и автогазрузка своего контроллера.

Это все, что планировал написать в этом топике. Любые дополнения приветствуются.

Автору спасибо и за помощь, и за EvoTwig2.
  • paic
  • 0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.