Большой рассказ про pdoTools, часть первая

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

Изначально он не задумывался, как набор универсальных сниппетов, нет. Он должен был стать набором классов, на основе которых программисты могли бы разрабатывать собственные сниппеты. Однако, идея не прижилась, и сниппеты на нём разрабатывал один я.

Понятное дело, что через какое-то время я пришел к универсальным сниппетам "на все случаи жизни", которые и вошли в комплект pdoTools. Про них вы можете почитать на страницах документации, а ниже я расскажу, что же там под капотом.

Вы узнаете, как pdoTools работает с чанками, что такое быстрые плейсхолдеры, как делать выборки из сторонних таблиц, присоединять их в запросы и т.д. В общем, масса полезной информации.

Ядро компонента разделено на 3 класса: общий pdoTools, работа с БД - pdoFetch и работа с оформлением, то есть pdoParser.

При установке в систему они регистрируются таким образом, чтобы вы могли быстро их запускать:


$pdoTools = $modx->getService('pdoTools');
$pdoFetch = $modx->getService('pdoFetch');

pdoFetch наследует pdoTools, так что не нужно вызывать эти два класса вместе. Если вы хотите работать с БД, вызывайте один Fetch, а если нет - Tools. Парсер вызывать вообще не нужно, они или включен в настройках MODX, или нет. pdoTools использует его для оформления чанков в любом случае.

Итак, что же умеет базовый класс pdoTools?

Ведение лога

Очень важная особенность pdoTools - он умеете вести лог того, что делает. Для этого вам доступны методы

  • addTime(string $message) - добавляет новую запись в лог,

  • getTime(bool $string [true]) - добавляет итоговое время и возвращает либо отформатированную строку (по умолчанию), либо массив время => сообщение.

Например, вот этот код:


$pdo = $modx->getService('pdoTools');
$pdo->addTime('pdoTools инициализирован');
print_r($pdo->getTime());

Выведет:


0.0000150: pdoTools инициализирован
0.0000272: Total time
1 572 864: Memory usage

То есть, вы можете подключать pdoTools в своих сниппетах, просто для логирования событий. Понятно дело, что его сниппеты сами всё пишут в лог, и как правило, менеджер может его почитать параметром &showLog=1.

Кэширование

pdoTools умеет кэшировать произвольные данные для своей работы. Вы тоже можете этим пользоваться.

  • setStore(string $name, mixed $object, string $type ["data"])

  • getStore(string $name, string $type ["data"])

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


foreach ($users as $id) {
    $user = $pdo->getStore($id, 'user')
    if ($user === null) {
        if (!$user = $modx->getObject('modUser', $id)) {
            $user = false;
        }
        $pdo->setStore($id, $user, 'user');
    }
    elseif ($user === false) {
        echo 'Не могу найти юзера с id = ' . $id;
    }
    else {
        echo $user->get('username');
    }
}

В этом коде мы сохраняет юзеров в отдельный namespace user, чтобы не мешать другим сниппетам, и проверяем наличие юзера в кэше. Обратите внимание, что по условиям примера, кэш может вернуть или null (юзер еще не получался), или false (юзер не найден).

В любом случае, запрос в БД будет только один на каждого юзера.

Сам pdoTools кэширует таким образом вызовы чанков. Это, так сказать, простое кэширование. Данные сохраняются только на время работы скрипта, то есть, они не пишутся на жесткий диск.

Есть и более продвинутое кэширование, методами MODX:

  • setCache(mixed $data, array $options) - сохраняет данные $data в кэш, генерируя ключ из $options

  • getCache(array $options) - выдает данные, согласно $options

Здесь данные уже сохраняются на диск, параметры кэширования можно передавать при инициализации pdoTools:


$pdo = $modx->getService('pdoTools', array(
    'cache_key' => 'resource',
    'cache_handler' => 'xPDOFileCache',
    'cacheTime' => 10,
));

Первые 2 параметра обычно не нужны, нас интересует только время жизни - cacheTime.

Пример:


$pdo = $modx->getService('pdoTools');
$options = array(
    'user' => $modx->user->get('id'),
    'page' => @$_REQUEST['page'],
);
$pdo->addTime('pdoTools загружен');
if (!$data = $pdo->getCache($options)) {
    $pdo->addTime('Кэш не найден, генерируем данные');
    $data = array();
    for ($i = 1; $i <= 100000; $i ++) {
        $data[] = rand();
    }
    $data = md5(implode($data));
    $pdo->setCache($data, $options);
    $pdo->addTime('Данные сохранены в кэш');
}
else {
    $pdo->addTime('Данные загружены из кэша');
}
print_r($data);

Таким образом, в зависимости от юзера и страницы будут получены какие-то данные и сохранены в кэш. Если зайдёт другой юзер - он получит свой кэш.

В первый раз наш код покажет примерно такое


0.0000281: pdoTools загружен
0.0004001: No cached data for key "default/e713939a1827e7934ff0242361c06b4b10c53d97"
0.0000079: Кэш не найден, генерируем данные
0.0581820: Saved data to cache "default/e713939a1827e7934ff0242361c06b4b10c53d97"
0.0000181: Данные сохранены в кэш
0.0586412: Total time
1 835 008: Memory usage

А затем вот такое:

0.0000310: pdoTools загружен
0.0007479: Retrieved data from cache "default/e713939a1827e7934ff0242361c06b4b10c53d97"
0.0000081: Данные загружены из кэша
0.0007918: Total time
1 572 864: Memory usage

Как видите, pdoTools и сам прекрасно пишет работу с кэшем в лог, так что вам можно это не логировать.

Утилиты

Здесь всего два метода.

makePlaceholders(array $data, string $plPrefix, string $prefix [ '[[+' ], string $suffix [ ']]' ], bool $uncacheable [ true ])

Принимает массив ключ => значение и возвращает два массива плейсхолдеры => значения, используется для шаблонизации.

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


$data = array(
    'key1' => 'value1',
    'key2' => 'value2',
);

$pls = $pdo->makePlaceholders($data);
print_r($pls);

Результат:


Array
(
    [pl] => Array
        (
            [key1] => [[+key1]]
            [!key1] => [[!+key1]]
            [key2] => [[+key2]]
            [!key2] => [[!+key2]]
        )

    [vl] => Array
        (
            [key1] => value1
            [!key1] => value1
            [key2] => value2
            [!key2] => value2
        )

)

Дальше можно обработать какой-то html шаблон вот так:


$html = str_replace($pls['pl'], $pls['vl'], $html);

buildTree(array $resources) строит иерархическое дерево из массива ресурсов, используется pdoMenu.


$pdo = $modx->getService('pdoFetch');
$resources = $pdo->getCollection('modResource');
$tree = $pdo->buildTree($resources);
print_r($tree);

И вы увидите дерево ресурсов своего сайта. Обратите внимание, что для использования getCollection() нужно загружать pdoFetch.

Шаблонизация (работа с чанками)

Это, наверное, самая интересная часть класса pdoTools.

Метод здесь всего один - это getChunk(), однако вся его реализация рассчитана на максимальную производительность и функциональность.

Во-первых, все плейсхолдеры в чанки, какие только может, обрабатывает pdoParser. Условие одно - плейсхолдер должен быть без условий и фильтров. То есть:

  • [[%tag]] - строка лексикона

  • [[~id]] - ссылка

  • [[+tag]] - обычные плейсхолдеры

  • [[++tag]] - системные плейсхолдеры

  • [[*tag]] - плейсхолдеры ресурса

  • [[#tag]] - плейсхолдеры FastField

Если с обычными плейсхолдерами всё понятно, то про FastField нужно показать примеры, что вы можете:

  • Выводить поля ресурсов: [[#15.pagetitle]], [[#20.content]]

  • Выводить ТВ параметры ресурсов: [[#15.date]], [[#20.some_tv]]

  • Выводить поля товаров miniShop2: [[#21.price]], [[#22.article]]

  • Выводить массивы ресурсов и товаров: [[#12.properties.somefield]], [[#15.size.1]]

  • Выводить глобальные массивы: [[#POST.key]], [[#SESSION.another_key]]

  • Распечатывать массивы для отладки: [[#15.colors]], [[#GET]], [[#12.properties]]

Цифра после решетки - это id ресурса, от которого нужно выбрать данные.

Все эти теги pdoTools обрабатывает без создания объектов modElement, поэтому работает немного быстрее чем родные методы MODX. Если же плейсхолдер вызван с какими-то параметрами, то он уйдёт в родной modParser.

Еще getChunk в pdoTools умеет работать с разными типами чанков:

  • @INLINE, @CODE - чанк создаётся из полученной строки.

  • @FILE - чанк получается из файла. Для исключения инъекций, файлы могут быть только с расширением html и tpl, а директория для их выборки задаётся параметром $tplPath=`` в конфиге. По умолчанию, чанки выбираются из MODX_ASSETS_PATH . 'elements/chunks/'.

  • @TEMPLATE - чанк создаётся из шаблона сайта, можно указывать его id или имя.

  • @CHUNK или просто строка, без @префикса - выборка обычного чанка из БД.

Рабочий пример:


$tpl = '@INLINE <p>[[+param]] - [[+value]]</p>';
$res = '';
for ($i = 1; $i <= 10000; $i++) {
    $pls = array('param' =>$i, 'value' => rand());
    $res .= $pdo->getChunk('test', $pls);;
}
echo '<pre>';
print_r($pdo->getTime());
print_r($res);

Вот вам и простейшая шаблонизация при помощи pdoTools.

Этот код выводит 10 000 строк всего за 0.17 секунды! Причем, неважно, что чанк @INLINE, обычный работает с той же скоростью. А если заменить $pdo->getChunk() на $modx->getChunk(), то выходит уже 8 секунд!

То есть, в данном конкретном примере парсинг чанков MODX медленее pdoTools в 3000 раз - 8 секунд, против 0.17.

Это говорит о том, что нужно максимально упрощать свои чанки, поменьше использовать условий и использовать pdoTools.

Чем же можно заменить условия? Самые простые "пусто\не пусто" заменяются "быстрыми плейсхолдерами".

Работает это так:

  1. В чанке должен быть какой-то тег, например [[+tag]].

  2. В чанке должен быть специальный html комментарий в таком виде:


<!--pdotools_tag значение, если тег не пуст-->
<!--pdotools_!tag значение, если тег пуст, появилось только в версии 1.9.3, выпущенной сегодня-->

Как видите, комментарий именуется исходя из префикса pdotools_ и имени тега. Префикс меняется параметром &nestedChunkPrefix=``.

Почему именно такие условия, зачем держать быстрый плейсхолдер в комментарии? Очень просто - это на случай обработки чанка не pdoTools.

Пример:

$tpl = '@INLINE
<p>[[+tag]]</p>
<!--pdotools_tag [[+tag]] - значение, если тег не пуст-->
<!--pdotools_!tag значение, если тег пуст, появилось только в версии 1.9.3, выпущенной сегодня-->';

$pls = array('tag' => 1);
echo $pdo->getChunk($tpl, $pls);

$pls = array('tag' => 0);
echo $pdo->getChunk($tpl, $pls);

Получаем


1 - значение, если тег не пуст

значение, если тег пуст, появилось только в версии 1.9.3, выпущенной сегодня

Как видите, внутрь быстрого плейсхолдера можно вставлять и другие плейсхолдеры, и его оригинальное значение. Конечно, это небольшой функционал, по сравнению с фильтрами MODX, но зато очень быстро.

Есть еще один интересный параметр обработки плейсхолдеров - &fastMode. Он выключает передачу плейсхолдеров в родной парсер MODX, и то, что не смог обработать pdoParser просто будет вырезано.

В последних версиях pdoTools его использовать нет нужды, потому что если pdoParser всё обработал, и в чанке не осталось ни одного [[+tag]], то он сразу отдаёт результат, не трогая modParser. Но вы можете его включить как принудительное требование для тех людей, которые меняют чанки - чтобы они не могли использовать трехэтажные конструкции.

Заключение

Рассказ про pdoTools пришлось разбить на две части, потому что объём выходит немаленький.

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

← Предыдущая заметка
Базовые дополнения: Theme.Bootstrap, MinifyX и немного pdoTools
Следующая заметка →
Большой рассказ про pdoTools, часть вторая
Комментарии (18)
Наумов Алексей
29.07.2014 06:08
<!--!pdotools_tag значение, если тег пуст, появилось только в версии 1.9.3, выпущенной сегодня-->

За это спасибо, ждал-ждал!

bezumkinВасилий Наумкин
29.07.2014 06:55

Ай, очепятка. Правильный тег вот такой:

<!--pdotools_!tag значение -->

То есть, восклицательный знак перед именем поля, а не префиксом.

kondakovДмитрий Кондаков
30.07.2014 07:13

Василий, а можешь рассмотреть в уроках такую нестандартную ситуацию: выборка файлов в тикетах не используя TicketMeta?

bezumkinВасилий Наумкин
30.07.2014 07:15

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

Сегодня допишу и опубликую.

kondakovДмитрий Кондаков
30.07.2014 07:18

Спасибо!

jean179Е. Вершинин
30.07.2014 09:55

Странно, в новую статью "Большой рассказ про pdoTools, часть вторая" не пускает - говорит не оплатил :) Кажется галку Василий забыл поставить где-то.

bezumkinВасилий Наумкин
30.07.2014 10:10

Василий её в первый курс опубликовал.

Поправил, заходи!

jean179Е. Вершинин
30.07.2014 12:04

Спасибо :)

Макс
30.07.2014 15:29

Василий! Предыдущие два поста понял почти все. Два поста про pdoTools не понял почти ничего :) Намекни пожалуйста мне по какой теме почитать литературу? HTML, CSS, PHP или все перечисленное? ну чтобы хоть приблизительно понять про что ты тут пишешь, а то вон народ одобрительно гудит, а я как-то совсем не в теме :)

bezumkinВасилий Наумкин
30.07.2014 15:46

Ну, во-первых не факт, что тебе это нужно. Это тайная сторона pdoTools о которой знают далеко не все пользователи сниппетов, поэтому решил рассказать.

А во-вторых, читать нужно про PHP и SQL.

Макс
30.07.2014 16:05

Про во-первых я тоже думал :) За во-вторых - спасибо! может и доберусь :)

Дмитрий Аверин
21.10.2014 07:14

Василий, вопрос новичка в Modx про pdoCrumbs, про крошки для вложенных документов (SEO!) Мы знаем, что нет ничего лучше для внутренней перелинковки и передачи веса, чем хлебные крошки.

Я бы хотел сделать так: Главная-Один-Два-Три site.ru/odin/dva/tri Для передачи веса на главную, которая будет продвигаться по определенным ключам.

Пока что у меня получается - site.ru/index/odin/dva/tri

Т.е. в крошки добавляется алис главной.

Как это исправить/настроить? Скорее всего я чего то не знаю пока. Спасибо

bezumkinВасилий Наумкин
21.10.2014 07:58

А зачем у тебя все документы внутри контейнера index?

Вытащи их оттуда, и не будет в пути /index/.

Дмитрий Аверин
21.10.2014 08:11

И получится структура site.ru/odin/dva/tri ? Я не знал о такой возможности. Думал, что надо создавать дочерние ресурсы, чтобы получить такую структуру. Т.к. я хочу, чтобы в крошках всегда присутствовала главная и передавать на ней вес.

bezumkinВасилий Наумкин
21.10.2014 08:13

У pdoCrumbs есть возможность выводить ссылку на главную, независимо от ссылки документа.

Обрати внимание на домик в крошках, вверху этой страницы.

Дмитрий Аверин
21.10.2014 08:21

Всё, врубился. Спасибо! Первые дни копаюсь с MODX после Joomla! Торможу еще.

Дмитрий Аверин
23.10.2014 08:33

У меня очень странно ЧПУ формируются в крошках. Вот пример теста- http://x0w.ru/odin/dva/tri/chetyire/pyat/shest Если кликнуть по крошке они все перемешиваются. Что не так сделал?

bezumkinВасилий Наумкин
24.10.2014 02:51

Ты слышал что-нибудь про тег base?

Добавь внутрь head

<base href="[[++site_url]]" />
bezumkin
Василий Наумкин
04.07.2022 23:34
Что-то странное у тебя произошло: миграция есть, и вроде как выполнена, но таблицы при этом отсутств...
inetlover
Александр Наумов
03.07.2022 20:36
Василий, спасибо! Все понятно!
bezumkin
Василий Наумкин
02.07.2022 20:28
Спасибо, поправил!
bezumkin
Василий Наумкин
30.06.2022 03:58
Есть ли возможность формировать &quot;friendly URL aliases&quot;, используя аналог translit MODx? ...
bezumkin
Василий Наумкин
27.06.2022 03:32
Спасибо за исправления, очень выручаешь =) Но учитывая количество не описаных в заметке дополнительн...
bezumkin
Василий Наумкин
27.06.2022 03:10
что будет использоваться для вывода многоуровневого меню Посмотри как работают комментарии на этом ...
bezumkin
Василий Наумкин
25.06.2022 11:56
Поправил, спасибо!
bezumkin
Василий Наумкин
21.06.2022 01:58
onLoad(data) { this.total = data.total }, и onLoad({total}) { this.total = total }, В нашем случ...
bezumkin
Василий Наумкин
20.06.2022 14:01
Прекрасно тебя понимаю, я когда сам в этом разбирался - голова дымилась. Но зато теперь прямо-таки п...
bezumkin
Василий Наумкин
20.06.2022 09:30
Не надо, оно по умолчанию так - я просто чуть более подробно написал.