В продолжение предыдущего топика.
В данном топике я просто выложу некоторые Smarty-шаблоны с сайта, а так же код процессора, который я использовал на замену Wayfinder-у.
Под катом много кода и комментов.
1. Smarty-шаблоны.
Как я и говорил не раз, phpTemplates+Smarty — это то, что нам позволяет значительно снизить нагрузку на MODX-сайт. Но помимо этого Smarty-шаблоны имеют одну офигенную штуку, которой в MODX-шаблонизации просто нет, а именно — наследование/расширение шаблонов. Давайте рассмотрим это на примере Smarty-шаблонов из Hamster-а.
Основной шаблон (используется остальными расширяющими шаблонами.)
<!DOCTYPE html>
<html lang="ru">
{config name=site_name assign=site_name}
{* HEAD *}
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>{field name=longtitle} | {$site_name}</title>
<meta name="keywords" content="{field name=keywords}" />
<link rel="shortcut icon" href="/assets/images/favicon.ico" type="image/ico" />
<link rel="stylesheet" media="all" href="/assets/hamster/css/style.css" />
<link rel="stylesheet" media="all" href="/assets/hamster/css/prettyPhoto.css" />
<link type="text/css" rel="stylesheet" href="/assets/components/minishop/css/web/jquery.stickr.css">
<!--[if IE]>
<![endif]-->
<base href="{config name=site_url}" />
{* Eof HEAD *}
</head>
<body>
<section id="wrapper">
<section id="main">
<header>
{* Header *}
<a id="logo" title="{$site_name}" href="/"></a>
<nav id="menu">
{*snippet name=Wayfinder params="startId=`0`&level=`1`"*}
{assign var=params value=[
"startId" => 0
,"level" => 1
,"cacheable" => true
,"id" => "mainMenu"
]}
{processor action="web/menu/getcatalogmenu" ns="hamster" params=$params assign=result}
{assign var=items value=$result.object}
{include file="inc/menu/catalog/outer.tpl"}
</nav>
<div id="phone_order">
Заказ по телефону:<br />+7 (495) 221-90-21<br />+7 (495) 221-90-23<br />+7 (925) 092-28-33
</div>
<div id="user_panel">
<a id="cartLink" href="{link id=4}" title="Корзина">Корзина</a>
<span class="uLogin">[[!uLogin? &providers="vkontakte,facebook,odnoklassniki,twitter,mailru,google" &hidden="" &userGroups="Authorized" ]]</span>
</div>
{* Eof Header *}
</header>
<section id="columns">
<aside id="catalog">
{* Catalog.nav *}
<h3><a href="{link id=2}" title="Каталог товаров">Каталог товаров</a>:</h3>
<div id="product_lists">
{*snippet name=Wayfinder params="startId=`1` &level=`1` &rowTpl=`listRowTpl`"*}
{assign var=params value=[
"startId" => 1
,"level" => 1
,"cacheable" => true
,"id" => "secondMenu"
]}
{processor action="web/menu/getcatalogmenu" ns="hamster" params=$params assign=result}
{assign var=items value=$result.object}
{include file="inc/menu/catalog/outer.tpl"}
</div>
<div id="search">
{* Search *}
<form id="search_form" action="{link id=6}">
<input type="text" placeholder="Поиск по артикулу или названию" name="search[text]" /> <input type="submit" value="Искать" />
</form>
{* Eof Search *}
</div>
<div id="catalog_tree">
{*snippet name="Wayfinder@MainCatalogMenu"*}
{assign var=params value=[
"startId" => 2
,"level" => 4
,"sortBy" => "pagetitle"
,"levelClass" => "level"
,"where" => [
"template" => 2
]
,"cacheable" =>true
,"id" => "catalog"
]}
{processor action="web/menu/getcatalogmenu" ns="hamster" params=$params assign=result}
{assign var=items value=$result.object}
{include file="inc/menu/catalog/outer.tpl"}
</div>
{* Eof Catalog.nav *}
</aside>
<article id="content">
{block name=Breadcrumbs}<div id="breadcrumbs">{snippet name=Breadcrumbs params="showHomeCrumb=`0` ¤tAsLink=`0` &showCurrentCrumb=`0`"}</div>{/block}
{block name=content}
{field name=content}
{/block}
</article>
<div class="clear"></div>
</section>
</section>
</section>
<footer>
{* Footer *}
<section id="footers">
<section id="brands">
<div class="smartlist">
{* БрендыХамстерФокс *}
{snippet name=brands_slider}
{* Eof БрендыХамстерФокс *}
</div>
</section>
<aside class="left">
© Хамстер-Фокс.2012<br />
Все права защищены.
</aside>
<aside class="right">
</aside>
<div class="clear"></div>
</section>
{literal}
<!-- Yandex.Metrika counter -->
<noscript><div><img src="//mc.yandex.ru/watch/19623109" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
{/literal}
{* Eof Footer *}
</footer>
</body>
Заметка: Элементы {*… *} — это комментарии, то есть не обрабатываются и никуда не выводятся. В некоторых комментариях имеются вызовы сниппетов, на замену которым использованы процессоры или типа того.
И вот здесь мы сразу рассмотрим что же такое «наследование шаблонов» и почему у нас сразу такой большой шаблон, а не разбросанный на отдельные кусочки, чтобы эти кусочки можно было использовать в других шаблонах (как это традиционно используется в MODX-шаблонах). Для этого давайте опять посмотрим на исходный MODX-шаблон:
<!DOCTYPE html>
<html lang="ru">
<head>
[[$Head]]
</head>
<body>
<section id="wrapper">
<section id="main">
<header>
[[$Header]]
</header>
<section id="columns">
<aside id="catalog">
[[$Catalog.nav]]
</aside>
<article id="content">
[[*content]]
</article>
<div class="clear"></div>
</section>
</section>
</section>
<footer>
[[$Footer]]
</footer>
</body>
В данном случае MODX-шаблон конечно выглядит компактней. Но если нам нужен еще один шаблон, похожий, но с мелкими изменениями, нам придется полностью копировать этот шаблон. А дальше хорошо, если изменения где-то в общем чанке. А если нам надо внести изменения не в общий кусочек кода? И в дальнейшем получится, что если у нас накопится штук 10 шаблонов, и надо внести изменения в шаблоны, то может оказаться, что нам придется вносить изменения во все шаблоны. Плюс постоянный поиск по всем этим чанкам тоже порой отнимает не мало времени (я уже не говорю про потерю в производительности, так как сейчас речь вообще не об этом).
А что мы имеем в Smarty? Вот код еще одного шаблона, который не имеет отличий от базового шаблона:
{extends file="layout.tpl"}
Да, это все! То есть мы просто использовали другой шаблон и все. Никакого копирования никакого кода.
А как будет выглядеть еще один шаблон, который имеет отличия от базового шаблона? Вот так, к примеру, выглядит расширяющий шаблон каталога на Hamster-е:
{extends file="layout.tpl"}
{block name=content}
<div class="products smartlist list">[[!Catalog]]</div>
{/block}
И да, это тоже все! То есть мне надо было всего лишь заменить блок вывода, чтобы не content текущей страницы выводился, а каталог, плюс он имел бы div-обрамление.
Давайте разберем как это работает.
1. Подключаем основной шаблон (обязательно в начале шаблона):
{extends file="layout.tpl"}
2. При расширении шаблона весь последующий код расширяющего шаблона просто так не воспринимается. В таких случаях должны использоваться специальные конструкции-блоки. Вот, найдите в основном шаблоне вот такой блок:
{block name=content}
{field name=content}
{/block}
{field name=content} — это то же самое, что и [[*content]]
Блок обязательно имеет свое имя (в данном случае name=content). Все остальное, что имеется внутри этого блока, выводится как есть. Но если мы расширяем шаблон и используем новый блок с таким же названием, то этот блок замещается содержимым нового блока. В нашем случае это:
{block name=content}
<div class="products smartlist list">[[!Catalog]]</div>
{/block}
То есть на выходе в расширяющем шаблоне мы имеем не {field name=content}, а это:
<div class="products smartlist list">[[!Catalog]]</div>
А в остальном это тот же самый шаблон. При этом шаблоны могут иметь сколько угодно уровней вложенности. То есть этот расширяющий шаблон можно расширить другим шаблоном, и изменить любой из их общих блоков.
А если нам к каком-то новом шаблоне надо воткнуть код туда, где вообще не предполагалось изменений, то мы просто вставим в основном шаблоне пустой блок, и в новом шаблоне переопределим его.
Вот поэтому я и использую вот такой один общий шаблон, так как в таком случае все перед глазами и работает быстро (без лишних инклюдов и т.п.), и при этом нет вообще потери в гибкости (благодаря Smarty).
Еще примеры шаблонов.
{extends file="layout.tpl"}
{block name=content}
<h1>{if $modx->resource->parent == 3}Бренд «{field name=pagetitle}»{else}{field name=pagetitle}{/if}</h1>
<div class="products smartlist list">[[!Catalog.Category]]</div>
{/block}
С некешируемыми MODX-тегами. Quip:
{extends file="layout.tpl"}
{block name=content}
<h1 id="pagetitle">{field name=pagetitle}</h1>
{field name=content}
<div class="post-comments" id="comments">[[!Quip?
&thread=`blog-post-[[*id]]`
&threaded=`1`
&dateFormat=`%d/%m/%Y %H:%I`
&tplComment=`commentTpl`
&closeAfter=`30`
]]
<br /><br />
[[!QuipReply?
&thread=`blog-post-[[*id]]`
&requireAuth=`1`
&moderate=`0`
&tplAddComment=`commentWithUlogin`
&tplLoginToComment=`authToComment`&closeAfter=`30`
]]
</div>
{/block}
В общем, со Smarty-шаблонами творить можно что угодно.
2. Замена Wayfinder.
Внимание! Если вы используете Smarty-блоки, внимательно читайте этот комментарий, чтобы избежать бесконечной рекурсии.
Вот это, пожалуй, лучшая наработка и того, что было сделано на Hamster-е. Планирую ее в дальнейшем оформить в пакет и постепенно дорабатывать. Для начала разберем, как это дело работает (кстати, при этом мы увидим еще один интересный фокус со Smarty-шаблонами).
Сам процессор.
«Сердце» модуля — этот процессор. Его главное предназначение — сделать выборку документов, участвующих в формировании менюшки.
Вот его код:
<?php
class modWebMenuGetCatalogMenuProcessor extends modProcessor{
protected $activeIDs = array(); // ID of active parents
public function initialize(){
$this->setDefaultProperties(array(
'id' => 'menu', // Menu id
'cacheable' => false,
'startId' => $this->modx->resource->id,
'level' => 1,
'sortBy' => 'menuindex',
'sortOrder' => 'ASC',
'levelClass' => '',
'activeClass' => 'active',
'ignoreHidden' => false,
'showUnpublished' => false,
));
return parent::initialize();
}
public function process() {
$output = '';
// get active parents
if(!empty($this->modx->resource) AND $this->modx->resource instanceOf modResource){
$resource = $this->modx->resource;
$this->activeIDs[] = $resource->id;
while($resource = $resource->getOne('Parent')){
$this->activeIDs[] = $resource->id;
}
}
// get menu items
if(!$items = $this->getMenuItems()){
return;
}
// prepare menu items
$items = $this->prepareMenu($items);
return array(
'success' => true,
'message' => '',
'object' => $items,
);
}
public function getMenuItems(){
$items = array();
$startId = $this->getProperty('startId');
$level = $this->getProperty('level');
$cacheable = $this->getProperty('cacheable');
$id = $this->getProperty('id', 'menu');
$cacheKey = $this->modx->context->key."/{$id}/{$startId}";
if($cacheable){
if($fromCache = $this->modx->cacheManager->get($cacheKey)){
return $fromCache;
}
}
//else
if($items = $this->getItems($startId, $level)){
if($cacheable){
$this->modx->cacheManager->set($cacheKey, $items);
}
}
return $items;
}
protected function getItems($parent, $level){
$level--;
$items = array();
$q = $this->modx->newQuery('modResource');
$where = $this->getDefaultConditions();
$where['parent'] = $parent;
$q->where($where);
$q->select(array(
'id', 'parent', 'pagetitle', 'longtitle', 'description', 'menutitle', 'link_attributes', 'uri', 'alias',
));
$q->sortby($this->getProperty('sortBy'), $this->getProperty('sortOrder'));
if($q->prepare() && $q->stmt->execute()){
while($row = $q->stmt->fetch(PDO::FETCH_ASSOC)){
if($level>0){
$row['childs'] = $this->getItems($row['id'], $level);
}
else{
$row['childs'] = array();
}
$items[$row['id']] = $row;
}
}
return $items;
}
protected function prepareMenu(array & $items, $currentlevel=1){
$levelClass = $this->getProperty('levelClass');
$activeClass = $this->getProperty('activeClass');
foreach($items as &$item){
$cls = array();
if($levelClass){
$cls[] = "{$levelClass}{$currentlevel}";
}
$item['linktext'] = ($item['menutitle'] ? $item['menutitle'] : $item['pagetitle']);
if(in_array($item['id'], $this->activeIDs)){
if($activeClass){
$cls[] = $activeClass;
}
}
$item['cls'] = implode(" ", $cls);
if($item['childs']){
$item['childs'] = $this->prepareMenu($item['childs'], $currentlevel+1);
}
}
return $items;
}
protected function getDefaultConditions(){
$where = array(
'deleted' => 0,
);
if(!$this->getProperty('showUnpublished')){
$where['published'] = true;
}
if(!$this->getProperty('ignoreHidden')){
$where['hidemenu'] = false;
}
if($_where = $this->getProperty('where')){
$where = array_merge($where, $_where);
}
return $where;
}
}
return 'modWebMenuGetCatalogMenuProcessor';
?>
Помимо выборки документов, этот процессор умеет кешировать результат и в дальнейшем использовать его для формирования меню. Возвращает процессор массив документов (элементов меню). Конечно процессор не все поля документов получает, а только самые необходимые. В дальнейшем я планирую его серьезно доработать/переработать, чтобы он был еще более гибкий (есть ряд мыслей, включая ввод методов типа setSelection и объединение методов getItems и prepareMenu), но это чуть позже.
Далее остается только набить эти данные в шаблоны, чтобы сформировать конечный HTML-код шаблона. И вот здесь как раз я и покажу еще одну фишку Smarty-шаблонов, которая мне очень понравилась :-)
Итак, рассмотрим пример вызова этого процессора и формирование менюшки. Вот код:
{assign var=params value=[
"startId" => 2
,"level" => 4
,"sort" => "sortBy"
,"levelClass" => "level"
,"where" => [
"template" => 2
]
,"cacheable" =>true
,"id" => "catalog"
]}
{processor action="web/menu/getcatalogmenu" ns="hamster" params=$params assign=result}
{assign var=items value=$result.object}
{include file="inc/menu/catalog/outer.tpl"}
1. Набиваем массив параметров, которые мы передадим в процессор:
{assign var=params value=[
"startId" => 2 // Стартовый раздел
,"level" => 4 // Количество уровней вложенности
,"sortBy" => "pagetitle" // Сортировка по заголовку
,"levelClass" => "level" // класс уровня (будет level1, level2 и т.п.)
,"where" => [ // Условия поиска
"template" => 2 // документы с шаблоном 2
]
,"cacheable" =>true // Кешировать результат
// ID меню (использется в формировании ключа кеша,
// чтобы случайно не пересекся с кешем других менюшек)
,"id" => "catalog"
]}
{processor action="web/menu/getcatalogmenu" ns="hamster" params=$params assign=result}
ns — это namespace, то есть пространство имен модуля в MODX.
assign=result — это присваиваем полученный результат переменной $result.
3. Присваиваем массив полученных элементов переменной $items:
{assign var=items value=$result.object}
4. Подгружаем Smarty-шаблон, в котором будет выполняться оформление этого массива в конечный код менюшки:
{include file="inc/menu/catalog/outer.tpl"}
Вот код этого шаблончика:
<ul>
{foreach $items as $item}
{* Wrapper *}
{include file="inc/menu/catalog/row.tpl"}
{/foreach}
</ul>
Переменная-массив $items объявлена перед инклюдом шаблона, а значит она видна внутри этого шаблона. И что здесь происходит с ней? Здесь мы видим открывающие и закрывающие теги ul, а внутри них в цикле по каждому элементу массива $items инклюдится другой шаблончик. Каждый отдельный элемент массива имеет имя $item ( {foreach $items as $item} ). Вот код и этого шаблончика:
<li class="{$item.cls}">
<a href="{$item.uri}" title="{$item.pagetitle}" {$item.link_attributes}/>{$item.linktext}</a>
{assign var=items value=$item.childs}
{if $items}
{* Wrapper *}
{include file="inc/menu/catalog/outer.tpl"}
{/if}
</li>
А здесь у нас набиваются тег li и a. Но обратите внимание на этот участок:
{assign var=items value=$item.childs}
{if $items}
{* Wrapper *}
{include file="inc/menu/catalog/outer.tpl"}
{/if}
Здесь мы пытаемся новой переменной $items присвоить значение дочерних элементов массива $item.childs ( {assign var=items value=$item.childs} ), и если эти элементы имеются, то мы ОПЯТЬ вызываем outer-шаблон менюшки:
{if $items}
{* Wrapper *}
{include file="inc/menu/catalog/outer.tpl"}
{/if}
Таким образом у нас на этих двух шаблончиках получается рекурсия, которая набьет код менюшки произвольной вложенности. Прикольно — рекурсия на шаблонах :-) Вы такое в чанках видели? К слову, я тут думал по поводу того, а можно ли на MODX-элементах выполнить такую рекурсию? И пришел к выводу, что только на чанках это не сделать. Мы не можем внутри чанка передать в другой чанк только один из элементов массива. Для этого нам придется вызывать сниппет, который будет вызывать чанк, в котором будет вызываться другой сниппет, который в свою очередь будет повторно вызывать первый чанк. В итоге, получается, что нам надо 2 сниппета и 2 чанка (или 1 сниппет, если в него передавать имя вызываемого чанка, и два чанка). Но вообще вот этот процессор в первоначальном виде имел в себе метод fetchMenu, на уровне которого элементы меню набивались в конечный код через вызов Smarty-шаблонов. Соответственно там можно было просто заменить вызов Смарти-шаблонов на чанки, и получилось бы тоже самое (просто медленней работало бы). То есть, можно просто использовать расширяющий процессор, а в нем расширить метод process, и прогнать элементы через чанки, и вернуть уже сразу конечный HTML. Но это так, мысли вслух…
Вот, наверно, и все, что я хотел здесь рассказать. Если что, задавайте вопросы.