Hamster-fox.ru Техническая оптимизация и обновление MODX Revolution. Часть 2. Шаблоны и скрипты.

В продолжение предыдущего топика. В данном топике я просто выложу некоторые Smarty-шаблоны с сайта, а так же код процессора, который я использовал на замену Wayfinder-у. Под катом много кода и комментов. 1. Smarty-шаблоны. Как я и говорил не раз, phpTemplates+Smarty — это то, что нам позволяет значительно снизить нагрузку на MODX-сайт. Но помимо этого Smarty-шаблоны имеют одну офигенную штуку, которой в MODX-шаблонизации просто нет, а именно — наследование/расширение шаблонов. Давайте рассмотрим это на примере Smarty-шаблонов из Hamster-а. Основной шаблон (используется остальными расширяющими шаблонами.)

{config name=site_name assign=site_name}

{* HEAD *}

{field name=longtitle} | {$site_name}
<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]--> 
{* Eof HEAD *}
	<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>
Заметка: Элементы {*… *} — это комментарии, то есть не обрабатываются и никуда не выводятся. В некоторых комментариях имеются вызовы сниппетов, на замену которым использованы процессоры или типа того. И вот здесь мы сразу рассмотрим что же такое «наследование шаблонов» и почему у нас сразу такой большой шаблон, а не разбросанный на отдельные кусочки, чтобы эти кусочки можно было использовать в других шаблонах (как это традиционно используется в MODX-шаблонах). Для этого давайте опять посмотрим на исходный MODX-шаблон: [[$Head]]
[[$Header]]
[[*content]]
[[$Footer]]
В данном случае MODX-шаблон конечно выглядит компактней. Но если нам нужен еще один шаблон, похожий, но с мелкими изменениями, нам придется полностью копировать этот шаблон. А дальше хорошо, если изменения где-то в общем чанке. А если нам надо внести изменения не в общий кусочек кода? И в дальнейшем получится, что если у нас накопится штук 10 шаблонов, и надо внести изменения в шаблоны, то может оказаться, что нам придется вносить изменения во все шаблоны. Плюс постоянный поиск по всем этим чанкам тоже порой отнимает не мало времени (я уже не говорю про потерю в производительности, так как сейчас речь вообще не об этом). А что мы имеем в Smarty? Вот код еще одного шаблона, который не имеет отличий от базового шаблона: {extends file="layout.tpl"} Да, это все! То есть мы просто использовали другой шаблон и все. Никакого копирования никакого кода. А как будет выглядеть еще один шаблон, который имеет отличия от базового шаблона? Вот так, к примеру, выглядит расширяющий шаблон каталога на Hamster-е: {extends file="layout.tpl"}

{block name=content}

[[!Catalog]]
{/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}
[[!Catalog]]
{/block} То есть на выходе в расширяющем шаблоне мы имеем не {field name=content}, а это:
[[!Catalog]]
А в остальном это тот же самый шаблон. При этом шаблоны могут иметь сколько угодно уровней вложенности. То есть этот расширяющий шаблон можно расширить другим шаблоном, и изменить любой из их общих блоков. А если нам к каком-то новом шаблоне надо воткнуть код туда, где вообще не предполагалось изменений, то мы просто вставим в основном шаблоне пустой блок, и в новом шаблоне переопределим его. Вот поэтому я и использую вот такой один общий шаблон, так как в таком случае все перед глазами и работает быстро (без лишних инклюдов и т.п.), и при этом нет вообще потери в гибкости (благодаря Smarty). Еще примеры шаблонов. С заголовком по условию: {extends file="layout.tpl"} {block name=content}

{if $modx->resource->parent == 3}Бренд «{field name=pagetitle}»{else}{field name=pagetitle}{/if}

[[!Catalog.Category]]
{/block} С некешируемыми MODX-тегами. Quip: {extends file="layout.tpl"}

{block name=content}

{field name=pagetitle}

{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"
]} 2. Вызываем процессор: {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"} Вот код этого шаблончика:

    {foreach $items as $item} {* Wrapper *} {include file="inc/menu/catalog/row.tpl"} {/foreach}

Переменная-массив $items объявлена перед инклюдом шаблона, а значит она видна внутри этого шаблона. И что здесь происходит с ней? Здесь мы видим открывающие и закрывающие теги ul, а внутри них в цикле по каждому элементу массива $items инклюдится другой шаблончик. Каждый отдельный элемент массива имеет имя $item ( {foreach $items as $item} ). Вот код и этого шаблончика:
  • {$item.linktext} {assign var=items value=$item.childs} {if $items} {* Wrapper *} {include file="inc/menu/catalog/outer.tpl"} {/if}
  • А здесь у нас набиваются тег 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. Но это так, мысли вслух… Вот, наверно, и все, что я хотел здесь рассказать. Если что, задавайте вопросы. UPD: Актуальный скрипт менюшки: gist.github.com/Fi1osof/6987afe4545a37dc805d Добавил параметр hideSubMenus.