Fi1osof 25 июля 2013 6 3
В продолжение предыдущего топика.

В данном топике я просто выложу некоторые 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"    
]}


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"}


Вот код этого шаблончика:
<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. Но это так, мысли вслух…

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

UPD: Актуальный скрипт менюшки: gist.github.com/Fi1osof/6987afe4545a37dc805d
Добавил параметр hideSubMenus.
3 комментария
maxmg1
maxmg 25 июля 2013г в 19:42 #
Вот теперь мне стало понятно!
Спасибо за статью!
Fi1osof1
Fi1osof 25 июля 2013г в 23:25 #
Пожалуйста :-)
Tramp13571
Tramp1357 07 августа 2013г в 21:09 #
Спасибо за статью!
Использовал процессор на сайте, все заработало сразу.
Только сегодня почему-то он начал выдавать
Notice: Undefined variable: modx in /home/v/v98516/v98516.bget.ru/public_html/core/site/processors/getmenu.class.php on line 58


ругается на конструкцию
$cacheKey = $modx->context->key.'/{$id}/{$startId}';

Подскажи пожалуйста, что может быть?

Если просто в этом месте вставляю {$modx->context->key.'/{$id}/{$startId}'} то выводит 'web'
Если из процессора вывожу print_r($modx->context->key.'/{$id}/{$startId}'), то выводит '/page1/4'
Fi1osof1
Fi1osof 07 августа 2013г в 23:11 #
Пожалуйста!

По поводу нотиса: там действительно косяк. В процессорах внутри функций нет переменной $modx. Есть только $this->modx. То есть правильный кот вот такой (сейчас поправлю в топике):
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;
}


Как видишь, переменные $id и $startId тоже внутри этой функции объявляются. Так что смотри, чтобы все переменные были объявлены внутри функции (в php же в функциях область видимости — локальная, и если нужна глобальная видимость переменной, то необходимо использовать global для этой переменной, к примеру global $modx).

Спасибо за багрепорт!
Tramp13571
Tramp1357 07 августа 2013г в 23:21 #
Спасибо, понял.
в php же в функциях область видимости — локальная, и если нужна глобальная видимость переменной, то необходимо использовать global для этой переменной, к примеру global $modx
это я знаю, все-таки php не очень далеко в этом от с++ ушел :)

Кстати, раз списались… мы говорили по поводу передачи наборов параметров в процессоры. я дописал код в твои функции, в песочнице топик
Fi1osof1
Fi1osof 07 августа 2013г в 23:44 #
в песочнице топик
ОК, сейчас гляну. Спасибо!
Tramp13571
Tramp1357 07 августа 2013г в 23:25 #
и все-таки странно… пол дня работало, и не ругался.
Fi1osof1
Fi1osof 07 августа 2013г в 23:45 #
Зависит от того включен вывод нотисов или нет. Может нотисы были отключены, а потом ты их влючил через ini_set('display_errors', 1) или типа того.
Tramp13571
Tramp1357 08 августа 2013г в 16:38 #
Добрый день. Николай, не подскажешь, как сделать так, чтобы я мог назначить переменную в файле шаблона страницы (page.tpl), изменить ее во вложенном шаблоне row.tpl (page.tpl — outer.tpl — row.tpl) и затем ее конечное значение использовать в page.tpl?

Пробовал объявить ее как global в плагине — не помогает
Fi1osof1
Fi1osof 09 августа 2013г в 02:19 #
Добрый день.
Smarty-шаблоны ведут себя так же, как и обычные php-файлы (в общих чертах). Поэтому оперируешь простыми переменными.
{assign var=foo value=$value}

Переменная {$foo} будет видна далее по коду. Таким же образом ты ее можешь перегрузить.
Tramp13571
Tramp1357 09 августа 2013г в 14:31 #
Это я понимаю. Мне нужно в файле шаблона row задать значение переменной, а затем использовать его в родительском файле. можно сделать так?
Fi1osof1
Fi1osof 09 августа 2013г в 19:11 #
Если в родительском шаблоне переменная нужна после вызова дочернего шаблона (где эта переменная будет создана), то да. А если раньше, то как? Если переменной тупо еще нет.
Fi1osof1
Fi1osof 09 августа 2013г в 19:40 #
Все-таки я был не прав. Видимо переменные, объявленные в шаблонах, видны только внутри текущего шаблона и дочерних. А выше не видны. В инете нашел несколько тем по этому поводу, но решения ни у кого не отмечено.

Первое альтернативное решение, которое мне пришло в голову — это использовать modResource::setOption()/::getOption(). Дело в том, что эти методы имеют все производные от xPDOObject. Метод ::getOption() хорош еще и тем, что можно задать массив первостепенного источника и значение по умолчанию.

К примеру, задаем переменную в дочернем шаблоне (только эта переменная — свойство текущего ресурса).
{$modx->resource->setOption('foo', 'value')}


В родительском шаблоне после вызова дочернего шаблона эта переменная будет видна в ресурсе.
{$modx->resource->getOption('foo', $defaultSourceArray, $defaultValue)}


Но надо сразу учитывать, чтобы дочерний шаблон не был кешируемым, так как это переменная не Smarty, а самого документа. То есть если дочерний шаблон не был отработан, то и в ресурсе этой переменной не будет.
Tramp13571
Tramp1357 09 августа 2013г в 22:28 #
Спасибо за ответ! Идея с setOption/getOption — по-моему то, что надо. Сейчас попробую.
Я уже хотел написать функцию расширения со статической переменной, но я не знаю, как долго сохраняется такая функция в памяти, т.к. если она удаляется сразу после отработки, то статическая переменная теряет смысл.

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

Я сделал эти меню дочерними к элементам верхнего меню, а левое генерируется примерно как

{processor action=«getdata» params=«parent=`{field name=id}`»}

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

И я решил просто создать в шаблоне переменную, в которую при формировании верхнего меню (во вложенном файле row.tpl) будет прописываться id документа верхнего меню с классом active.

Сейчас я просто перенес весь код в основной шаблон (все вертится в одном файле), но не нравится мне это решение, модульное построение как-то больше доверия внушает.
Fi1osof1
Fi1osof 09 августа 2013г в 23:10 #
А у тебя случаем не включено глобальное кеширование Smarty? через настройки modxSmarty.
Tramp13571
Tramp1357 09 августа 2013г в 23:14 #
нет
Fi1osof1
Fi1osof 09 августа 2013г в 23:23 #
Тогда очень странное поведение.
Хотя еще вариант: проверь-ка запрос в процессоре. Параметр parent=$id вызывает сомнения. Надежней [
«where» => [
«parent» => $id
]
]

Для четкой отладки пиши в процессоре так:
public function prepareQueryBeforeCount(xPDOQuery $c) {
    $c = parent::prepareQueryBeforeCount($c);
    
    $c->prepare();
    print $c->toSQL();
    exit;
    
    return $c;
}


Так ты увидишь реальный SQL-запрос при выполнении этого процессора. Можешь убрать exit, чтобы процесс не обламывался, и вы всегда видел выполняется процесс или нет.
Tramp13571
Tramp1357 10 августа 2013г в 00:00 #
Хотя еще вариант: проверь-ка запрос в процессоре. Параметр parent=$id вызывает сомнения. Надежней [
конструкция
{processor action=«getdata» params=«parent=`{field name=«id»}`»}
работает надежно, просто мне нужен id не текущего документа, а id документа, соответствующего активному пункту в верхнем меню, т.к. из его дочерних документов и формируется левое меню
Tramp13571
Tramp1357 09 августа 2013г в 23:52 #
Я, видимо, снова непонятно написал.
При выводе верхнего меню я формирую переменную mysite:

<code>{assign var=mysite value=1}
{processor action="getmenu" ns="site" propset="top_menu" assign=result}
<ul class="nav sf-menu clearfix">
    {foreach $result.object as $item}
    <li class="{$item.cls}">
    {if ($item.cls=='active')}
        {assign "mysite" value=$item.id} {*startId для левого меню*}
    {/if}
    <a href="{$item.uri}" title="{$item.pagetitle}" {$item.link_attributes}/>{$item.linktext}</a>
    </li>
{/foreach}
</ul>
</code>

а затем использую ее при формировании левого меню:

<code>{processor action="getmenu" ns="site" propset="left_menu" params="startId=`{$mysite}`&id=`page_{$mysite}`" assign=result}
{assign var=items value=$result.object}
{include file="left-menu/outer.tpl"}
</code>

Так все работает. Просто я хотел раскидать код вывода верхнего меню по файлам outer.tpl и row.tpl, а в row.tpl и задать значение mysite, но тут-то и столкнулся с тем, что значение, которое я задаю в row.tpl, теряется при возврате в файл шаблона.
Fi1osof1
Fi1osof 10 августа 2013г в 01:15 #
но тут-то и столкнулся с тем, что значение, которое я задаю в row.tpl, теряется при возврате в файл шаблона
Вот судя по результатам опытов и топикам в сети, это и будет не решаемой проблемой. То есть только через методы типа ::setOption() и ::getOption();
Tramp13571
Tramp1357 10 августа 2013г в 10:55 #
Ясно. Спасибо за помощь!
Fi1osof1
Fi1osof 10 августа 2013г в 15:05 #
Пожалуйста!
Tramp13571
Tramp1357 12 августа 2013г в 23:27 #
Николай, добрый вечер. не подскажешь, в чем может быть проблема?:
Fi1osof1
Fi1osof 12 августа 2013г в 23:50 #
Добрый вечер. Для надежности используй этот скрипт: gist.github.com/Fi1osof/328469331b5258ff009a
<?php
print '<pre>';
$modx->setLogLevel(3);
$namespace = 'shop';
if(!$response = $modx->runProcessor('web/catalog/goods/getdata', 
array(
    
), array(
    'processors_path'   => $modx->getObject('modNamespace', $namespace)->getCorePath().'processors/',        
))){
    print "Не удалось выполнить процессор";    
    return;
}
 
print_r($response->getResponse());


У тебя проблема в том, что ты указываешь относительный путь до папки процессоров, а надо абсолютный. То есть вместо 'processors_path' => 'core/site/processors/' пиши хотя бы 'processors_path' => MODX_CORE_PATH.'site/processors/'
Tramp13571
Tramp1357 12 августа 2013г в 23:53 #
Пробовал, то же самое. а относительный путь я посмотрел, выведя на экран

print $modx->getObject('modNamespace', $namespace)->getCorePath().'processors/';

Что странно, только вчера делал другой сайт — все работает. Не может быть проблемы в том, что modx изначально я сразу поставил на php 5.4.17? там править пришлось timezone при установке… хотя, сама modx работает.
Fi1osof1
Fi1osof 13 августа 2013г в 00:05 #
timezone вообще не может быть здесь при чем-то. И работать обязано, если скрипт правильно прописан.
У тебя этот скрипт что возвращает? Полный актуальный путь?
$namespace = 'core';
print $modx->getObject('modNamespace', $namespace)->getCorePath().'processors/';
Tramp13571
Tramp1357 13 августа 2013г в 00:12 #
Все, разобрался. namespace неверно прописал. Спасибо за помощь.
Fi1osof1
Fi1osof 13 августа 2013г в 00:23 #
Пожалуйста.
Tramp13571
Tramp1357 14 августа 2013г в 21:57 #
Николай, добрый вечер. Сегодня впервые пытаюсь сделать многоуровневое меню с вложенными шаблонами (как в топике).
Выдает такую ошибку:
Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 130968 bytes) in /.../smarty_internal_templatecompilerbase.php on line 2261 (номер строки меняется).
Если делаю вызов outer.tpl -> row.tpl -> outer2.tpl -> row2.tpl, то все работает. Но стоит вызвать outer.tpl из row.tpl или row.tpl из outer2.tpl, то появляется эта ошибка.
Что может быть? Неужели так много памяти требуется?!
Fi1osof1
Fi1osof 14 августа 2013г в 22:45 #
На самом деле очень странное поведение. С этим тоже столкнулся на новом сайте. При чем что интересно — оба сайта на modxcloud.com, оба сделаны одинаково, но косяк лезет только на одном сайте.
Потребление памяти связано с бесконечной рекурсией. Почему-то даже тогда, когда условие if не выполняется в шаблоне row.tpl, все равно (видимо на уровне прекомпилляции) вызывается шаблон outer.tpl Короче какая-то нелогичная фигня получается. Сейчас копаю. Как раскопаю, отпишусь.
Fi1osof1
Fi1osof 14 августа 2013г в 23:15 #
Все, разобрался. У тебя этот код где-то в блоке находится? ( {block name=someblock}… {/block} ).
У меня было это в блоке, и из-за этого рекурсия бесконечная и была. Здесь появляется логика в плане бесконечной подгрузки шаблона. Блок — это отдельная очень хитрая сущность, которая может расширяться, и в которой могут быть и другие блоки. Вот для того, чтобы быть «в курсе» по всем внутренним блокам, Smarty выполняет обход всех вложений {include file=} без учета всяких условий и т.п. (просто чтобы полностью скомпиллировать блок) (без условий — это в общих чертах). И так как там шаблон вызывает предыдущий шаблон, то и происходит бесконечная рекурсия. То есть память выжирается не на уровне выполнения процессора, а на уровне прекомпилляции шаблона.

Если у тебя менюшка находится в блоке и обязана там быть, то лично я эту проблему обошел за счет сниппета. То есть Смарти-код с процессором менюшки и т.п. вынес в отдельный шаблон, который вызывается сниппетом, а в блоке прописал этот сниппет {snippet name=menu}. В результате при прекомпилляции блока Smarty не выполняет этот сниппет и не зацикливается без дела. Все нормально работает.
Tramp13571
Tramp1357 14 августа 2013г в 23:17 #
Понял. Да, действительно в блоке. Спасибо!
Fi1osof1
Fi1osof 14 августа 2013г в 23:18 #
Пожалуйста.
Tramp13571
Tramp1357 14 августа 2013г в 23:29 #
У меня просто на одной странице выводится список городов (страна->регион->область->город), я решил так:

{if {field name=id}==36}
{processor action=«getmenu» ns=«site» propset=«cities» assign=«result»}
{assign var=«items» value=$result.object}
{include file=«cities/outer.tpl»}
{else}
{block name=«content»}
{field name=«content»}
{/block}
{/if}
Fi1osof1
Fi1osof 14 августа 2013г в 23:33 #
Это тоже помогло с рекурсией или как?
Tramp13571
Tramp1357 14 августа 2013г в 23:41 #
Да, я просто вынес формирование меню из блока. Работает.
Fi1osof1
Fi1osof 14 августа 2013г в 23:54 #
А, ну да, не в блоке этого не происходит.
Fi1osof1
Fi1osof 14 августа 2013г в 23:26 #
Кстати, как еще один вариант (чтобы обойтись без сниппетов), думаю, можно использовать Smarty-плагин для набивки менюшки. То есть написать свой плагинчик и после вызова процессора результат отправлять туда. А там уже вызывать Смарти-шаблончики для набивки. Вряд ли прекомпиллятор будет вызывать эту функцию.
Tramp13571
Tramp1357 14 августа 2013г в 23:30 #
Да, хорошая идея. Надо попробовать
Tramp13571
Tramp1357 14 августа 2013г в 23:52 #
Блин, действительно все просто:
function smarty_function_load($params, & $smarty)
{
    if(!isset($params['file']) OR !$file = $params['file']){return;}

    if(!empty($params['assign'])){
        $assign = (string)$params['assign'];
    }
    
    $output = $smarty->fetch($file);
    return !empty($assign) ? $smarty->assign($assign, $output) : $output;
}


и вызываю
{load file="cities/outer.tpl"}


Работает.
Fi1osof1
Fi1osof 14 августа 2013г в 23:55 #
Во, классно :-)
Я чуть-чуть другое имел ввиду, но так даже лучше. Получилось универсально, и без бесконечной рекурсии.
Tramp13571
Tramp1357 15 августа 2013г в 00:38 #
А как тебе такая идея:
добавляем в процессор параметр tpl, в который передаем имя файла шаблона

{processor action="getmenu" ns="site" propset="cities" tpl="cities/outer.tpl"}

и добавляем код в плагин function.processor.php:

function smarty_function_processor($params, & $smarty)
{
  ...
    if ($response = $modx->runProcessor($action, $scriptProperties, $options)) {
        $output = $response->getResponse();
        if ($response->isError()) {
            if ($response->hasFieldErrors()) {
                $errors = (array) $response->getFieldErrors();
                foreach ($errors as $error) {
                    $output['field_errors'][$error->getField()] = $error->getMessage();
                }
            }
        } else {
+           if (isset($params['tpl']) and $tpl = $params['tpl']) {
+                $items=$ouput['object'];
+                $out=$smarty->fetch($tpl);
+                $ouput=$out;
+            } else {
            $output['success'] = true;
+            }
        }
    }

    return !empty($assign) ? $smarty->assign($assign, $output) : $output;
}

Тогда, если есть это параметр, процессор вернет не объект, а обработанный smarty текст.
Я уже у себя проверил, работает. и шаблоны поаккуратнее смотрятся, вывод меню или данных — в одну строку :)
Fi1osof1
Fi1osof 15 августа 2013г в 00:44 #
На заметку: обновил скрипт: gist.github.com/Fi1osof/6987afe4545a37dc805d
Добавил параметр hideSubMenus. Теперь неактивные подменю не будут подгружаться из бд. Функция пока экспериментальная, так как с кешированием еще разбирался.
Tramp13571
Tramp1357 15 августа 2013г в 00:51 #
Ясно, спасибо.
Fi1osof1
Fi1osof 15 августа 2013г в 00:52 #
Не за что.
Fi1osof1
Fi1osof 15 августа 2013г в 00:52 #
Идея интересная. Надо будет над ней подучать. Только здесь момент сразу:
$out=$smarty->fetch($tpl);
$ouput=$out;

Лучше сразу $ouput=$smarty->fetch($tpl);

Но с выводом надо будет поиграться. Элементарно скорее всего в $smarty->fetch($tpl) просто не будет виден результат процессора, так как assign ты еще не сделал.
Tramp13571
Tramp1357 15 августа 2013г в 01:29 #
Действительно, вместо
$items=$ouput['object'];

надо написать
$smarty->assign('items',$output['object']);
Fi1osof1
Fi1osof 15 августа 2013г в 01:32 #
Шустренько работает.
Tramp13571
Tramp1357 15 августа 2013г в 01:33 #
С {snippet name=Wayfinder} рендер был около 0,6с. теперь около 0,06с!
Fi1osof1
Fi1osof 15 августа 2013г в 01:43 #
Да, вот это результат :-)
Fi1osof1
Fi1osof 15 августа 2013г в 07:38 #
Кстати, обрати внимание, что в том же Smarty-плагине {snippet} имеется такой параметр как parse. Если его передать со значением true {snippet name=mysnippet parse=parse}, то он не просто отработает указанный сниппет, но еще и полностью пропарсит результат (то есть если в результате будут MODX-теги, то он все отработает). Чтобы убедиться, элементарно в шаблоне пропиши {snippet name=MetaX} (само собой MetaX надо установить, если вдруг не используешь его). А после захода на страницу загляни в кеш документа (core/cache/resource/web/resources/).
Вот такой кеш примерно будет:
'_content' => '<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
    <!-- base xhtml4 -->
	<base href=".........." />
<!-- meta -->
	<meta name="keywords" content="[[$AllKeywords:notempty=`[[$AllKeywords:strip]], `]]" />
	........
	
[[+metax.css]]
[[+metax.rss]]
<!-- end MetaX output -->


А вот если указать parse=true, то и эти плейсхолдеры все будут отработаны еще на уровне Smarty-шаблона, и в кеше уже будет только конечный HTML.
Tramp13571
Tramp1357 25 августа 2013г в 02:23 #
Предложение по процессору: добавить параметр (например cascade), который будет задавать, ставить ли activeClass всей цепочке от родителя к текущему или только текущему документу (у меня возникла такая проблема, при выводе древовидного списка категорий, чтобы оно не закрывалось при перерисовке):

<code>
    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;
            
+            if($this->getProperty('cascade')){
                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,
        );
    }
</code>

ну и соответственно добавить параметр cascade
Fi1osof1
Fi1osof 25 августа 2013г в 13:05 #
В процессоре действительно еще не все параметры созданы, но постепенно будут появляться.
В данном случае не cascade нужен, а currentClass (к примеру current), чтобы всегда можно было определить текущий элемент. То есть active — это цепочка активных, но у текущего элемента еще и класс current будет.
Tramp13571
Tramp1357 25 августа 2013г в 20:37 #
Да, так действительно лучше будет.
a
artemspb 09 июня 2015г в 13:02 #
Как можно поменять условие выборки не по шаблону, а по тв параметру?
a
artemspb 09 июня 2015г в 13:13 #
так как столкнулся с проблемой вывода меню с помощью wayfinder'а, решил попробовать этот способ.
а проблема следующая: меню формируется из ресурсов, часть которых находится в корне, часть в контейнерах (которые не опубликованы и скрыты, нужно для общей каталогизации дерева ресурсов, чтобы менеджерам было удобно), и получается то, что ресурсы находящиеся в корне — выводятся, остальные — нет (родители которых скрыты и не опубликованы).
Если можно решить эту проблему с помощью данного процессора, то как тогда сделать выборку по тв?
Либо где и что в wayfinder'е нужно подкрутить?
Tramp13571
Tramp1357 09 июня 2015г в 13:53 #
ресурсы находящиеся в корне — выводятся, остальные — нет (родители которых скрыты и не опубликованы)


{$params=[
    'startId'=>0,
    'level'=>2,              //уровень вложенности
    'ignoreHidden'=>true,    //добавить к выборке скрытые от показа в меню
    'showUnpublished'=>true  //добавить к выборке неопубликованные
]}
{processor action='site/web/getmenu' ns=modxsite params=$params assign=result}


Все в исходниках :)
a
artemspb 09 июня 2015г в 14:41 #
Не совсем понятно объяснил наверно. Мне нужно не все документы вывести, а только определенные. В wayfinder'е есть параметр — includeDocs, где указываются id ресурсов для вывода. Так вот, ресурсы в корне — выводятся, в контейнерах — нет
Tramp13571
Tramp1357 09 июня 2015г в 15:47 #
тогда site/web/resources/getdata и передать параметр
'where'=>['id:in'=>[1,2,5,22...]]
или
'where'=>['parent:in'=>[1,2,5,22...]]
или что-то еще из синтаксиса SQL по правилам xPDO
Авторизуйтесь или зарегистрируйтесь (можно через соцсети ), чтобы оставлять комментарии.