Обробка даних перед виводом

    Найчастіше отримані дані виводяться не в чистому вигляді. Наприклад, слід дотримуватися при виведенні будь-яких умов або обробок (висновок превью для картинки, перерахунок ціни, форматування значень).

    Традиційним способом в 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` &parameter_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

    Аналогічним чином можна використовувати анонімні функції.

    Приклади з блогу Agel Nash

    Приклад 1

    Отже, уявімо, що у нас є певний виклик 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 місці.

    Приклад №2

    Цей приклад буде складніший для розуміння. Але він ще більш наочно демонструє перевагу параметра 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 сніпетів вдалося врятуватися від необхідності повторної обробки тих же самих даних, які потрібні для отримання і виведення якоїсь іншої інформації в довільному місці цієї ж сторінки.

    Приклад №3

    Цей приклад ще складніше, але одночасно з цим ще більше корисніше і цікавіше.

    Отже, якщо подивитися уважно на скріншот дерева ресурсів і список 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. Часом буває необхідно цю обгортку додатково обробити і можливо підмінити в залежності від різних умов (кількість виведених документів, дати і т.д.).

    Висновок 3 блоків з рівномірним наповненням за 1 виклик

    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="/ua/04_extras/doclister/02_data-processing-before-output.html#"></a>
            <a id="next" href="/ua/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+]">&nbsp;[+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;
        }
    ));