Большой рассказ про 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 и как с его помощью выбирать что угодно на сайте.

18 комментариев

Наумов Алексей
<!--!pdotools_tag значение, если тег пуст, появилось только в версии 1.9.3, выпущенной сегодня-->
За это спасибо, ждал-ждал!
Василий Наумкин
Ай, очепятка. Правильный тег вот такой:
<!--pdotools_!tag значение -->
То есть, восклицательный знак перед именем поля, а не префиксом.
Дмитрий Кондаков
Василий, а можешь рассмотреть в уроках такую нестандартную ситуацию: выборка файлов в тикетах не используя TicketMeta?
Василий Наумкин
Выборки в следующем уроке, там примеры про ms2Gallery - очень похоже.
Сегодня допишу и опубликую.
Дмитрий Кондаков
Спасибо!
Е. Вершинин
Странно, в новую статью "Большой рассказ про pdoTools, часть вторая" не пускает - говорит не оплатил :) Кажется галку Василий забыл поставить где-то.
Василий Наумкин
Василий её в первый курс опубликовал.
Поправил, заходи!
Е. Вершинин
Спасибо :)
Василий! Предыдущие два поста понял почти все. Два поста про pdoTools не понял почти ничего :) Намекни пожалуйста мне по какой теме почитать литературу? HTML, CSS, PHP или все перечисленное? ну чтобы хоть приблизительно понять про что ты тут пишешь, а то вон народ одобрительно гудит, а я как-то совсем не в теме :)
Василий Наумкин
Ну, во-первых не факт, что тебе это нужно. Это тайная сторона pdoTools о которой знают далеко не все пользователи сниппетов, поэтому решил рассказать.
А во-вторых, читать нужно про PHP и SQL.
Про во-первых я тоже думал :) За во-вторых - спасибо! может и доберусь :)
Дмитрий Аверин
Василий, вопрос новичка в Modx про pdoCrumbs, про крошки для вложенных документов (SEO!) Мы знаем, что нет ничего лучше для внутренней перелинковки и передачи веса, чем хлебные крошки.
Я бы хотел сделать так: Главная-Один-Два-Три site.ru/odin/dva/tri Для передачи веса на главную, которая будет продвигаться по определенным ключам.
Пока что у меня получается - site.ru/index/odin/dva/tri
Т.е. в крошки добавляется алис главной.
Как это исправить/настроить? Скорее всего я чего то не знаю пока. Спасибо
Василий Наумкин
А зачем у тебя все документы внутри контейнера index?
Вытащи их оттуда, и не будет в пути /index/.
Дмитрий Аверин
И получится структура site.ru/odin/dva/tri ? Я не знал о такой возможности. Думал, что надо создавать дочерние ресурсы, чтобы получить такую структуру. Т.к. я хочу, чтобы в крошках всегда присутствовала главная и передавать на ней вес.
Василий Наумкин
У pdoCrumbs есть возможность выводить ссылку на главную, независимо от ссылки документа.
Обрати внимание на домик в крошках, вверху этой страницы.
Дмитрий Аверин
Всё, врубился. Спасибо! Первые дни копаюсь с MODX после Joomla! Торможу еще.
Дмитрий Аверин
У меня очень странно ЧПУ формируются в крошках. Вот пример теста- http://x0w.ru/odin/dva/tri/chetyire/pyat/shest Если кликнуть по крошке они все перемешиваются. Что не так сделал?
Василий Наумкин
Ты слышал что-нибудь про тег base?
Добавь внутрь head
<base href="[[++site_url]]" />
bezumkin.ru
Personal website of Vasily Naumkin
Прямой эфир
Александр Наумов
23.07.2024, 00:20:37
Василий, спасибо большое!!
Василий Наумкин
01.07.2024, 11:56:41
Да, верно, именно так. А в контроллере, скорее всего, ловить данные методом post.
Василий Наумкин
26.06.2024, 09:38:15
О, точно, вылезает если не залогинен. Спасибо, исправил!
Василий Наумкин
09.04.2024, 04:45:01
> Ошибка 500 Это не похоже на ошибку Nginx, это скорее всего ошибка PHP - надо смотреть его логи. ...
Василий Наумкин
20.03.2024, 21:21:52
Volledig!
Андрей
14.03.2024, 13:47:10
Василий! Как всегда очень круто! Моё почтение!
russel gal
09.03.2024, 20:17:18
> А этот стоило написать хотя бы затем, чтобы получить комментарий от юзера, который ничего не писал...
Александр Наумов
27.01.2024, 03:06:18
Василий, спасибо! Извини, тупанул.
Василий Наумкин
22.01.2024, 07:43:20
Давай-давай!
Василий Наумкин
24.12.2023, 14:26:13
Спасибо!