Чаще всего полученные данные выводятся не в чистом виде. Например, требуется выполнение при выводе каких-либо условий или обработок (вывод превью для картинки, пересчет цены, форматирование значений).
Традиционным способом в MODX является вызов сниппетов в шаблонах вывода или использование модификаторов (phx). Такой подход часто используется новичками в силу простоты, но при этом увеличивается нагрузка на парсер и базу данных.
В DocLister предусмотрена возможность предварительной обработки данных средствами PHP с помощью параметра prepare.
Значением параметра prepare может быть:
Можно задавать несколько значений для последовательных обработок.
Сниппет обрабатывает значения массива $data после обработки возвращает массив $data или false. В последнем случае данные выводиться не будут, т.е. можно использовать prepare таким образом для фильтрации данных.
Также в сниппете можно использовать объекты $_DocLister и $_extDocLister.
Объект $_DocLister дает возможность обращаться к методам и свойствам контроллера. Например, можно подменить текущий шаблон вывода:
$_DocLister->renderTPL = "@CODE:[+pagetitle+]";
Для того, чтоб получить значение параметров из вызова сниппета используйте конструкцию:
$parameter_value = $_DocLister->getCFGDef('parameter_name');
Данная конструкция позволяет получить значения параметра 'parameter_name' из вызова сниппета.
[[DocLister? &tpl=`tplChunk` &parents=`1` ¶meter_name=`1`]]
В объекте $_extDocLister доступны методы getStore и setStore. setStore сохраняет, а getStore, соответственно, извлекает данные из памяти. Как только DocLister завершил свою работу, сохраненные блоки удаляются из памяти.
class DLprepare {
public static function prepare(array $data = array(), DocumentParser $modx, $_DL, prepare_DL_Extender $_extDocLister) {
return $data
}
}
Значение параметра в этом случае: DLprepare::prepare
Аналогичным образом можно использовать анонимные функции.
Итак, представим, что у нас есть некий вызов DocLister'a:
[[DocLister? &tpl=`tplChunk` &parents=`1`]]
Где tplChunk это ни что иное, как:
<a href="[+url+]">[[if? &is=`[+longtitle+]:notempty` &then=`[+longtitle+]` &else=`[[snippet? &text=`[+pagetitle+]`]]`]]</a>
Внутри чанка tplChunk имеется вложенный вызов if (допустим тот, что был указан в начале топика). Что мы делаем?
Добавляем к вызову DocLister параметр &prepare=test
(можно и не тест, а любое другое значение).
Создаем сниппет с именем test (ну или с тем именем, которое было указано в параметре prepare)
В сниппете пишем такой код:
<?php
if(!empty($data['longtitle'])){
$data['longtitle'] = $modx->runSnippet("snippet", array('text'=>$data['pagetitle']));
}
return $data;
Вы конечно можете вместо runSnippet написать и:
$data['longtitle'] = "[[snippet? &text=`".$data['pagetitle']."`]]";
Но еще раз вспомним почему это плохо: pagetitle может содержать символы об которые запнется парсер modx. Так можно написать только в том случае, если pagetitle может содержать вызов сниппета (но это оооочень редкий случай + есть другие способы для обработки вложенных modx тегов)! Но вернемся к нашим баранам.
Избавившись от вложенного сниппета теперь можно спокойно избавиться от чанка tplChunk и вынести шаблон прямо в вызов DocLister'a:
[[DocLister? &tpl=`@CODE:<a href="[+url+]">[+longtitle+]</a>` &parents=`1`]]
Теперь можно подвести итог: Проверка условий средствами php (при помощи сниппета prepare) позволяет избежать ненужных вызовов вложенных сниппетов (вложенных вызов происходит только тогда, когда условие true). А еще, на мой скромный взгляд, inline шаблоны это очень удобно, т.к. наглядно виден и сам вызов и шаблон - все в 1 месте.
Этот пример будет сложнее для понимания. Но он еще более наглядно демонстрирует преимущество параметра prepare.
На сайте имеются несколько групп товаров (телефоны, телевизоры, магнитофоны). В каждой группе соответственно свои позиции, которые пользователь добавляет в корзину. На странице оформления заказа все выбранные позиции должны еще иметь подпись (к какой группе товаров они принадлежат). Помимо каждый товар в корзине имеет такие характеристики, как кол-во заказываемого товара, стоимость товара и общая стоимость с учетом количества. Так же нужно посчитать общее ИТОГО в виде полной суммы заказа.
Упростим немного эту задачку переместив в TV-параметр count заказываемое количество позиций. А саму корзину выведем точным перечислением ID документов. Таким образом вызов принимает следующий вид:
[!DocLister?
&documents=`5133, 5132, 5135, 5131, 5136`
&idType=`documents`
&tpl=`ListShop`
&tvList=`price,count`
&ownerTPL=`@CODE:<table class="table table-hover params"><tr><td>Категория</td><td>Позиция</td><td>Остаток</td><td>Стоимость</td><td>Общая стоимость</td></tr>[+dl.wrap+]</table>`
!]
А чанк ListShop содержит такой код:
<tr>
<td>[[DocInfo? &docid=`[+parent+]` &field=`pagetitle`]]</td>
<td>[+pagetitle+]</td>
<td>[+tv.count+] шт.</td>
<td>[+tv.price+] руб.</td>
<td>[[FullPrice? &price=`[+tv.price+]` &count=`[+tv.count+]`]] руб.</td>
</tr>
Если сниппет DocInfo всем знаком, то сниппет FullPrice это ни что иное, как банальное перемножение двух чисел:
<?php
$count = isset($count) ? (int)$count : 0;
$price = isset($price) ? (int)$price : 0;
return $count*$price;
?>
Чтобы стало понятнее, вот так выглядит дерево ресурсов:
Дерево ресурсов
Добьем последний штрих - посчитаем на какую сумму у нас всего товаров в корзине. Для этого потребуется накидать небольшой сниппет, который пробежится опять по всем документам и посчитает общую сумму FullPrice. Назовем этот сниппет totalFullPrice (где 27 и 26 это ID тв-параметров с ценой и количеством):
<?php
//Получаем те же самые данные только для упрощения работы используем json формат. в целом не важно как это происходит, главное получить тут список нужны id документов
$out = $modx->runSnippet("DocLister", array('documents'=>'5133, 5132, 5135, 5131, 5136', 'idType'=>'documents', 'idType'=>'parents', 'depth'=>2, 'api'=>'id', 'JSONformat'=>'id'));
$out = json_decode($out, true);
$id = array();
foreach($out as $item){
$id[] = $item['id'];
}
$q = $modx->db->query("SELECT value,contentid,tmplvarid FROM ".$modx->getFullTableName("site_tmplvar_contentvalues")." WHERE contentid IN (".implode(",", $id).") AND tmplvarid IN (26, 27)");
$q = $modx->db->makeArray($q);
$out = array();
foreach($q as $item){
$out[$item['contentid']][$item['tmplvarid']] = $item['value'];
}
$price = 0;
foreach($out as $id=>$data){
$price += $data['27'] * $data['26'];
}
return $price;
?>
Т.к. это корзина, то все вызовы сниппетов делаем не кешированными и смотрим на результат (меня интересует общее число запросов): 12 SQL запросов для решения такой плевой задачи.
Пробуем теперь сделать тоже самое при помощи prepare сниппета.
[!DocLister?
&documents=`5133, 5132, 5135, 5131, 5136`
&idType=`documents`
&prepare=`total`
&tpl=`@CODE: <tr><td>[+category+]</td><td>[+pagetitle+]</td><td>[+tv.count+] шт.</td><td>[+tv.price+] руб.</td><td>[+fullPrice+] руб.</td></tr>`
&tvList=`price,count`
&ownerTPL=`@CODE:<table class="table table-hover params"><tr><td>Категория</td><td>Позиция</td><td>Остаток</td><td>Стоимость</td><td>Общая стоимость</td></tr>[+dl.wrap+]</table>`!]
Как видите, в этом случае мы опять отказались от чанка в пользу inline шаблона в котором используются 2 непонятных плейсхолдера - category и fullPrice. Давайте посмотрим на код сниппета prepare, чтобы стало понятнее:
<?php
$data['fullPrice'] = $data['tv.count'] * $data['tv.price'];
//[[DocInfo? &docid=`[+parent+]` &field=`pagetitle`]]
$data['category'] = $modx->runSnippet('DocInfo',array('docid'=>$data['parent'], 'field'=>'pagetitle'));
$key = 'totalFullPrice';
$price = $modx->getPlaceholder($key);
if(empty($price)){
$price = 0;
}
$price += $data['fullPrice'];
$modx->setPlaceholder($key,$price);
return $data;
?>
fullPrice это ни что иное, как банальное перемножение 2 числе (цены и количества). Согласитесь, нет необходимости для такой банальной операции создавать/вызывать еще 1 сниппет, когда можно произвести расчет прямо как говорится "не отходя от кассы".
category запускает тот же самый DocInfo при помощи runSnippet (т.е. ничего не поменялось).
А теперь самое главное! Создается глобальный плейсхолдер totalFullPrice, который можно уже использовать вне чанков DocLister'a! Если его нет - то устанавливается значение 0. Если есть, то к нему прибавляется fullPrice. Таким образом, мы избавляемся от необходимости делать повторную выборку тех же самых данных только ради того, чтобы посчитать общую стоимость. Т.е. считаем все и сразу. Соответственно вызов [!totalFullPrice!] на странице заменяется на [+totalFullPrice+] (я еще ни разу не встречал, чтобы подобные данных получали при помощи сниппета вложенного в чанк ListShop. Как правило их получают именно при помощи сниппета похожего на totalFullPrice.)
Проверяем результат - 9 SQL запросов.
Давайте опять подведем итог: Благодаря prepare сниппету получилось избавиться от необходимости повторной обработки тех же самых данных, которые нужны для получения и вывода какой-то другой информации в произвольном месте этой же страницы.
Этот пример еще сложнее, но одновременно с этим еще более полезней и интересней.
Итак, если посмотреть внимательно на скриншот дерева ресурсов и список ID документов которые мы выводим, то можно заметить, что из разделов телевизоры и магнитофоны выводится по 2 документа. Таким образом, parents у документов 5135, 5131 и 5136, 5132 будут одинаковыми соответственно. Т.е. у первой пары parents будет 5638, а у второй 5639. Что нам это дает? А это дает вот что - мы целых 2 раза вызываем DocInfo просто так. Представьте, вы получили pagetitle документа 5638 и запомнили его. Если он понадобился вам опять - то вы не повторно его запрашиваете, а сразу используете.
Именно для этой цели в DocLister, а если быть точнее в экстендере prepare есть 2 метода: getStore и setStore. Один сохраняет, а второй соответственно извлекает данные из памяти. Как только DocLister завершил свою работу, сохраненные блоки удаляются из памяти (массив автоматически очищается) и все работает дальше без каких-либо глюков, как это могло бы быть, если бы мы сохраняли временные данные в глобальный массив плейсхолдеров. Более того, при таком подходе и больших объемах потребовалось бы очень много памяти...
Ладно, хватит лирики. Давайте посмотрим как можно переписать сниппет prepare с учетом getStore и setStore:
if(($data['category']=$_extDocLister->getStore('currentParents'.$data['parent'])) === null){
//[[DocInfo? &docid=`[+parent+]` &field=`pagetitle`]]
$data['category'] = $modx->runSnippet('DocInfo',array('docid'=>$data['parent'], 'field'=>'pagetitle'));
$_extDocLister->setStore('currentParents'.$data['parent'], $data['category']);
}
Я не стал копипастить весь исходник, а только процитировал измененную часть. Но думаю тут и так понятно, что мы "обернули" вызов сниппета DocInfo в эти 2 функции. А в качестве уникального ключа указали currentParents[+parent+]. Таким образом, для формирований этой же страницы MODX-у потребовалось всего 7 SQL запросов.
Таким образом: сниппет prepare позволяет не только обрабатывать данные, но и исключить бесполезное повторное выполнение кода.
После того, как каждый документ обработан, DocLister пытается применить обертку ownerTPL. Порой бывает необходимо эту обертку дополнительно обработать и возможно подменить в зависимости от различных условий (кол-во выводимых документов, даты и т.п.).
return $modx->runSnippet('DocLister', array(
'ownerTPL' => '@CODE: <div class="mini-slider clearfix">
<div id="carousel-left">[+slider.left+]</div>
<div id="carousel-center">[+slider.center+]</div>
<div id="carousel-right">[+slider.right+]</div>
<a id="prev" href="/04_extras/doclister/02_data-processing-before-output.html#"></a>
<a id="next" href="/04_extras/doclister/02_data-processing-before-output.html#"></a>
</div>',
'tpl' => '@CODE: <div class="sert">
<img src="[+tv.image+]" alt="[+e.title+]" title="[+e.title+]">
</div><!-- slide !-->',
'tvList' => 'image',
'prepareWrap' => function($data, $modx, $_DL, $_eDL){
$plh = isset($data['placeholders']) && is_array($data['placeholders']) ? $data['placeholders'] : array();
$plh['slider.left'] = $plh['slider.center'] = $plh['slider.right'] = '';
$wrap = explode('<!-- slide !-->',
(isset($plh['dl.wrap']) && is_scalar($plh['dl.wrap']) ? $plh['dl.wrap'] : '')
);
$count = count(
isset($data['docs']) && is_array($data['docs']) ? $data['docs'] : array()
);
for($i = 0; $i <= $count; $i){
$left = APIHelpers::getkey($wrap, $i++, '');
$center = APIHelpers::getkey($wrap, $i++, '');
$right = APIHelpers::getkey($wrap, $i++, '');
if(empty($left) || empty($center) || empty($right)){
break;
}else{
$plh['slider.left'] .= $left;
$plh['slider.center'] .= $center;
$plh['slider.right'] .= $right;
}
}
return $plh;
}
));
return $modx->runSnippet('DLBuildMenu', array(
'idType' => 'parents',
'parents' => 0,
'debug' => 0,
'maxDepth' => 2,
'tvList' => 'dropMenu',
'TplOneItem' => '@CODE: <li><a href="[+url+]" title="[+e.title+]">[+title+]</a></li>',
'TplNoChildrenDepth1' => '@CODE: <li><a href="[+url+]" title="[+e.title+]">[+title+]</a></li>',
'TplDepth1' => '@CODE: <li class="drop"><a href="[+url+]" title="[+e.title+]"> [+title+]</a>[+dl.submenu+]</li>',
'TplMainOwner' => '@CODE: <nav><ul>[+dl.wrap+]</ul></nav>',
'TplSubOwner' => '@CODE: <div class="drop-block">
<!--<i class="closed"></i>-->
<div class="drop-content clearfix" style="[+background+]">
<div class="cell">
<ul>
[+dl.wrap+]
</ul>
</div>
<div class="left">
[+addText+]
</div>
</div>
</div>',
'AfterPrepare' => 'mainMenu::afterPrepare',
'prepareWrap' => function($data, $modx, $_DL){
$plh = $data['placeholders'];
if($_DL->getCfgDef('currentDepth', 1)==2){
$parent = current($_DL->getOneField('parent', true));
$plh['addText'] = $modx->runSnippet('DDocInfo', array('id' => $parent, 'field' => 'dropMenuText'));
$bg = $modx->runSnippet('DDocInfo', array('id' => $parent, 'field' => 'dropMenuImage'));
if($bg && file_exists(MODX_BASE_PATH . $bg) && is_file(MODX_BASE_PATH . $bg)){
$plh['background'] = 'background:url('.$bg.') right bottom no-repeat';
}
}
return $plh;
}
));