🚀 PageBlocks 2.8.0 — большой шаг вперёд

Вышла новая версия PageBlocks, и это не просто обновление, а большой шаг вперёд. Главная новинка — pbQuery, удобный конструктор запросов, который делает работу с базой данных быстрой и читаемой. Если раньше приходилось писать громоздкие xPDO-запросы или вручную собирать SQL, теперь всё решается лаконичной цепочкой методов.



Что такое pbQuery?


pbQuery — это современный «Query Builder» в стиле Laravel, но адаптированный под MODX. Он поддерживает все привычные операции: выборку, фильтрацию, сортировку, группировку, подзапросы, объединение (union) и даже работу с JSON-полями.

Почему это удобно?

  • Компактный код:
    $rows = query(modResource::class)
          ->where(['id:BETWEEN' => [10, 20]])
          ->fetchAll();
    Всё понятно сразу: выбираем ресурсы с id от 10 до 20.
  • Дополнительные операторы: поддержка BETWEEN, FIND_IN_SET, сортировки в порядке указанных ID.
  • JSON-поля: можно не только читать, но и сортировать по данным внутри JSON.
  • Union: легко объединять выборки, например ресурсы из разных шаблонов.
  • Модификация данных: методы create, insert, updateOrCreate упрощают работу с моделями.
  • Шаблоны вместо сниппетов: вывод данных можно сразу рендерить через tpl(), tplWrapper().
  • Кеширование: встроенное кеширование запросов ускоряет работу сайтов с высокой нагрузкой.

Примеры:


1. Работа с JSON-полями и сортировка по ним

pbQuery автоматически подставляет значения из JSON-колонки (если указан путь) — можно читать поля вида properties->seosuite->uri и сразу присваивать псевдонимы. Также сортировка по несуществующему полю понимается как сортировка по JSON-полю values->FIELD. Пример:

$rows = query(modResource::class)
    ->select('id, pagetitle, properties->seosuite->uri as seouri')
    ->fetchAll();

// или сортировка по JSON-полю values->price
$rows = query(modResource::class)
    ->orderBy('price', 'DESC') // подразумевается values->price
    ->get();
Это очень удобно для современных схем, где часть данных хранится в JSON-структуре.

2. Подзапросы (subqueries) — удобно и читабельно

В select() и в orderBy() можно передать Closure и построить подзапрос прямо «в месте», без громоздких ручных конструкций. Пример: добавить в выборку количество дочерних страниц каждого ресурса (подзапрос в SELECT):

$resources = query(modResource::class)
    ->alias('resource')
    ->select('id, pagetitle')
    ->select(['children_count' => function ($q) {
        $q->selectRaw('COUNT(*)')
          ->whereColumn(['parent' => 'resource.id']);
    }])
    ->fetchAll();
SQL:
SELECT `resource`.`id`, `resource`.`pagetitle`, (
    SELECT COUNT(*) 
    FROM `modx_site_content` AS `modResource` 
    WHERE `modResource`.`parent` = `resource`.`id` 
) as `children_count` 
FROM `modx_site_content` AS `resource`

Или сортировка по значению, получаемому подзапросом:
$query = query(modResource::class)
    ->select('id, pagetitle')
    ->orderBy(function ($q) {
        $q->table(\modUser::class)
          ->select('createdon')
          ->whereColumn(['modUser.id' => 'modResource.createdby']);
    }, 'DESC')
    ->fetchAll();
SQL:
SELECT `modResource`.`id`, `modResource`.`pagetitle` 
FROM `modx_site_content` AS `modResource` 
ORDER BY (
    SELECT `modUser`.`createdon` 
    FROM `modx_users` AS `modUser` 
    WHERE `modUser`.`id` = `modResource`.`createdby` 
) DESC

3. BETWEEN в where

В отличие от xPDO, в pbQuery можно использовать дополнительные операторы прямо в условиях. Например, найти ресурсы с id от 10 до 20:

$rows = query(modResource::class)
    ->where(['id:BETWEEN' => [10, 20]])
    ->fetchAll();
Получаем чистый и читаемый синтаксис без ручного SQL.

4. FIND_IN_SET без лишнего кода

Если у ресурса есть поле tags (например, список id через запятую), можно отфильтровать ресурсы по наличию конкретного id. Для этого есть оператор FIND_IN_SET прямо в where:

$rows = query(modResource::class)
    ->where(['tags:FIND_IN_SET' => 42])
    ->fetchAll();
Простая выборка по тегам, которую раньше нужно было «колдовать» через SQL.

5. UNION — объединение запросов

Получаем ресурсы сразу из двух шаблонов (1 и 2):
query(modResource::class)
    ->select(['id', 'pagetitle'])
    ->where(['template' => 1])
    ->union(function ($query) {
        $query->table(modResource::class)
            ->select(['id', 'pagetitle'])
            ->where(['template' => 2]);
    })
    ->fetchAll();
SQL:
(SELECT `modResource`.`id`, `modResource`.`pagetitle`
 FROM `modx_site_content` AS `modResource`
 WHERE `modResource`.`template` = 1)
UNION
(SELECT `modResource`.`id`, `modResource`.`pagetitle`
 FROM `modx_site_content` AS `modResource`
 WHERE `modResource`.`template` = 2)

6. Модификация данных — удобные методы (create/insert/update/firstOrCreate и т.д.)

pbQuery содержит набор удобных методов для изменения данных: create, insert, update, remove, firstOrCreate, updateOrCreate, increment, decrement, а также быстрые SQL-команды command('UPDATE') / command('DELETE'). Примеры:

// Создать объект через xPDO (будет newObject + save)
$created = query(modResource::class)->create([
    'pagetitle' => 'New Resource',
    'context_key' => 'web'
]);

// Быстрая вставка без xPDO-событий (insert via SQL)
query(modResource::class)->insert([
    'pagetitle' => 'Fast Resource',
    'context_key' => 'web'
]);

// firstOrCreate
$u = query(modUser::class)->firstOrCreate(['username' => 'john'], ['email' => 'john@example.com']);

7. Шаблонизация результатов: tpl(), render(), tplWrapper() — заменяет сниппеты

pbQuery умеет не только получать данные, но и сразу рендерить их через шаблоны Fenom:

$output = query(modResource::class)
    ->where(['published' => 1])
    ->tpl('tpl.resource.row')        // шаблон на каждый элемент
    ->tplWrapper('tpl.resource.wrapper', 'list') // обёртка для списка
    ->render();                      // возвращает final HTML
Это фактически позволяет заменять множество небольших сниппетов, т.к. вы получаете линейный и однотипный вывод из БД в HTML с минимальным кодом. Полезные возможности: tplParent, outputSeparator, filter() для пост-обработки и т.д.

8. Кеширование запросов (cache()) и отладка (debug())

Конструктор запросов имеет встроенный метод cache() для кеширования результатов и debug() — для вывода SQL, времени выполнения и информации о кешировании:

// кеширование выборки на 10 минут
$resources = query(modResource::class)
    ->where(['template' => 5])
    ->cache(600) // lifetime = 600s
    ->fetchAll();

// отладка: покажет SQL, время и флаг кеша
$resources = query(modResource::class)
    ->where(['published' => 1])
    ->debug()
    ->fetchAll();
Это упрощает оптимизацию и снижает нагрузку на базу при повторяющихся запросах.

9. Использование в Fenom-шаблонах

PageBlocks документирован так, что query() можно вызывать в Fenom-шаблонах — это даёт быстрый доступ к данным прямо в шаблоне. Пример (получить количество ресурсов и вывести в шаблоне):

{* Fenom template *}
{set $resCount = query('modResource')->where(['published' => 1])->count()}
<p>Всего опубликованных страниц: {$resCount}</p>

Или пример списка страниц:
{foreach query('modResource')->where(['template' => 4])->orderBy('menuindex')->get() as $page}
  <a href="{$page->get('uri')}">{$page->get('pagetitle')}</a>
{/foreach}
Это мощный и быстрый способ строить динамический вывод прямо в шаблонах без отдельного сниппета.

10. Пагинация с pbQuery: серверная и AJAX

pbQuery поддерживает встроенный метод paginate(), который возвращает объект Paginator. Он упрощает постраничный вывод данных, избавляя от ручного расчёта LIMIT и OFFSET. При этом пагинация работает сразу в двух режимах:

  • Без JavaScript — стандартная серверная пагинация. При клике на ссылку с номером страницы страница перезагружается, а нужные данные подставляются автоматически.
  • С JavaScript — динамическая подгрузка через AJAX с помощью скрипта pb.pagination. Это позволяет обновлять контент без полной перезагрузки страницы, улучшая UX на сайтах с большим количеством данных.

Пример в Fenom-шаблоне (Без JavaScript)
{set $users = query('modUser')
    ->where(['active' => 1])
    ->paginate(15)}

<ul>
    {foreach $users->items() as $user}
        <li>{$user.username}</li>
    {/foreach}
</ul>

<div class="pb-pagination" pb-pagination>
    {$users->links()}
</div>
  • $users->items() — массив пользователей для текущей страницы
  • $users->links() — HTML для навигации между страницами
  • $pageName (по умолчанию 'page') — параметр в query string для текущей страницы, например /products?page=2

Подключение AJAX-пагинации
Если на странице подключён JavaScript-файл pb.pagination (см. документация), пагинация будет работать динамически. Для этого необходимо указать маршрут через метод route():

1. Создание маршрута
Route::get('users/items','UserController@index')->name('user.index');

2. Контроллер для получения пользователей с фильтрацией
class UserController extends Controller {
    public function index(Request $request)
    {
        $paginator = query('modUserProfile')
            ->alias('Profile')
            ->select('modUser.id,modUser.username,modUser.active')
            ->select('Profile.fullname,Profile.email,Profile.phone,Profile.country,Profile.city,Profile.lastlogin')
            ->leftJoin('modUser', 'modUser', 'Profile.internalKey = modUser.id')
            ->where(['modUser.active' => 1])
            ->when($request->query, function ($query, $search) {
                $query->where([
                    'modUser.username:LIKE' => "%$search%",
                    'OR:Profile.fullname:LIKE' => "%$search%",
                    'OR:Profile.email:LIKE' => "%$search%",
                ]);
            })
             ->when($request->country, function ($query, $country) {
                $query->where(['Profile.country' => $country]);
            })
            ->when($request->city, function ($query, $city) {
                $query->where(['Profile.city' => $city]);
            })
            ->orderBy('lastlogin', 'desc')
            ->paginate(21)
            ->route('user.index');

        return array_merge($paginator->toArray(), [
            'data' => pbQuery::tpl('file:chunks/user/item.tpl')->render($paginator->items()),
            'links' => $paginator->links(),
        ]);
    }
}
  • route('user.index') указывает, куда отправлять AJAX-запрос при клике на номер страницы.
  • Метод toArray() возвращает все данные пагинатора для JSON-ответа.
  • pbQuery::tpl()->render() позволяет сразу отрендерить элементы через шаблон, а links() — генерирует ссылки на страницы.

3. Вывод данных на странице через ResourceController
Если нужно интегрировать вывод на странице без отдельного JS-запроса, можно использовать контроллер ресурса:

class ResourceController extends Controller
{
    public function index(Request $request) {
        
        if ($this->modx->resource->id === 70) {
            // Подключаем наш контроллер пользователей
            $controller = new UserController($this->modx);
            // Получаем данные в виде массива
            $data = $controller->index($request);
        }

        return view('file:templates/base', [
            'data' => $data ?? [],
        ]);
    }
}
Такой подход позволяет:
  • Динамически подгружать данные через AJAX при использовании pb.pagination.
  • Одновременно выводить данные на странице без JS, если требуется серверный рендеринг.
  • Применять фильтры, поиск и сортировку напрямую через pbQuery, сохраняя код чистым и читаемым.

Ключевые преимущества pbQuery:

  • Экономия времени — больше не нужно писать громоздкий xPDO-код
  • Читаемость — цепочечный синтаксис делает код самодокументируемым
  • Безопасность — автоматическое экранирование параметров
  • Гибкость — поддержка сложных сценариев (JSON, подзапросы, UNION)
  • Производительность — встроенное кеширование и оптимизированные запросы
Больше примеров, а также подробную документацию можно найти по ссылке.

Fenom (View) в PageBlocks — мощная, безопасная и быстрая шаблонизация


Класс View в PageBlocks — это обёртка над Fenom, которая превратила работу с шаблонами в удобный, защищённый и расширяемый инструмент. Он поддерживает разные типы шаблонов (`@INLINE`, файлы `@FILE:`, MODX-чанки и `template:`), даёт авто-поиск по директории элементов и настраиваемые политики производительности (автоперезагрузка, кэш и пр.).

Структура файлов (куда складывать расширения Fenom)

По умолчанию пользовательские расширения для Fenom лежат в core/App/Helpers/fenom/ и содержат пять ключевых файлов:

App/
└── Helpers/
    └── fenom/
        ├── modifiers.php         ← Модификаторы
        ├── inline_tags.php       ← Псевдо-теги
        ├── block_tags.php        ← Блочные функции
        ├── data.php              ← Глобальные данные
        └── php_functions.php     ← Разрешённые PHP-функции
Эта структура делает код организованным: каждая точка расширения — в своём файле и легко поддерживается.

1. Модификаторы — короткие трансформации прямо в шаблоне

Модификаторы (modifiers) дают лёгкий способ форматировать данные в шаблонах. Их удобно использовать для обрезки строк, форматирования дат, безопасного вывода и т.п.

Пример App/Helpers/fenom/modifiers.php:
return [
    'upper' => function($value) {
        return mb_strtoupper($value);
    },
    'users' => function () {
        return query('modUser')
            ->alias('user')
            ->select('user.*,profile.*')
            ->where(['active' => 1])
            ->join('modUserProfile', 'profile', 'user.id = profile.internalKey')
            ->get();
    },
];

Вызов модификаторов в шаблоне:

Как фильтр для значения:
<h2>{$title|upper}</h2>

Как функция:
{foreach users() as $user}
    <li>{$user->email}</li>
{/foreach}

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

2. Псевдо-теги — компактные функции для HTML-фрагментов

Псевдо-теги (inline tags) — это именованные функции, которые вызываются прямо в шаблонах как {icon 'edit' class='icon-sm'}. Они заменяют постоянное подключение фрагментов через include/insert и удобны для повторяющихся элементов (иконки, кнопки, бейджи).

Пример inline_tags.php:

return [
    'icon' => function($params) {
        $name = array_shift($params);
        $class = $params['class'] ?? '';
        return "<svg class='{$class}'><use xlink:href='#icon-{$name}'/></svg>";
    }
];
В шаблоне:

{icon 'user' class='avatar-lg'}
<button>{icon 'edit'} Редактировать</button>
Псевдо-теги сокращают шаблоны и упрощают масштабирование UI-компонентов.

3. Блочные функции — условные области с логикой доступа

Блочные теги (block_tags) принимают параметры и содержимое между открывающим/закрывающим тегами — идеально для условного вывода (доступ, роли, проверки).

Пример block_tags.php:

return [
    'auth' => function($params, $content) {
        if ($this->modx->user->isAuthenticated()) return $content;
        return '';
    },
    'superadmin' => function($params, $content) {
        if ($this->modx->user->id === 1) return $content;
        return '';
    }
];

В шаблоне:

{auth}
  <a href="/profile">Мой профиль</a>
{/auth}

{superadmin}
  <div class="admin-only">Секретная панель</div>
{/superadmin}
Блочные теги улучшают читабельность и позволяют скрывать/показывать части интерфейса на уровне представления.

4. Глобальные данные — общие настройки и константы в шаблонах

data.php возвращает массив, который автоматически доступен в шаблонах как $.pb. Это удобно для site-wide настроек: название проекта, e-mail поддержки, пути к ассетам.

Пример data.php:

return [
  'package' => 'MyProject',
  'support_email' => 'support@site.example',
  'assets_url' => '/assets/'
];

Использование в шаблоне:

<footer>© {$.pb.package} — <a href="mailto:{$.pb.support_email}">support</a></footer>
<img src="{$.pb.assets_url}logo.svg" alt="logo">
Глобальные данные делают шаблоны более декларативными и избавляют от дублирования настроек.

5. Разрешённые PHP-функции — безопасный набор и расширения

По умолчанию Fenom в PageBlocks позволяет безопасный набор PHP-функций (count, is_array, json_encode, strtotime, strip_tags и др.). Дополнительно вы можете перечислить своё в php_functions.php — например mb_strlen, htmlspecialchars.

Пример php_functions.php:

return [
  'mb_strlen',
  'htmlspecialchars',
  'lcfirst'
];

В шаблоне:

{$title|truncate:100} — {count($items)} элементов
{$safe = htmlspecialchars($raw_html)}

Больше информации можно найти в документации PageBlocks

PageBlocks 3.0.0 — что готовится


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

1. Универсальные контроллеры вместо множества процессоров

Планируется убрать «100500» отдельных процессоров и заменить их на универсальные контроллеры. Это позволит:
  • централизовать логику действий;
  • уменьшить дублирование кода;
  • упростить расширение функционала.

2. Кастомные таблицы и управление ими в админке

В 3.0.0 появится возможность создавать свои таблицы в базе данных и управлять ими прямо через панель администратора:
  • добавление, редактирование и удаление записей через UI;
  • интеграция с pbQuery для удобной работы с данными;
  • возможность подключать кастомные модели под конкретные задачи.

3. Рефакторинг полей и новые типы данных

Поля будут переработаны:
  • упрощение работы с типами данных;
  • добавление новых полей для гибкой настройки контента;
  • улучшенная совместимость с Fenom-шаблонами и pbQuery.

4. Обновление сниппетов и переход на pbQuery

Сниппеты будут переписаны с использованием pbQuery:
  • лаконичные цепочки методов вместо сложного xPDO-кода;
  • работа с JSON-полями и подзапросами;
  • поддержка фильтров, сортировки и пагинации.
В целом PageBlocks 3.0.0 нацелен на унификацию, ускорение работы и расширяемость, чтобы разработчикам и администраторам было проще создавать, поддерживать и масштабировать сайты на MODX.

Документация
Aleksandr Huz
9 часов назад
modx.pro
204
+7
Поблагодарить автора Отправить деньги

Комментарии: 12

Miša Bulic
9 часов назад
+3
Браво.
Каждый раз читаю ченжлоги, радуюсь, крутое обновление, много всего нового и интересного в одного приносит на Саша. Но после прочтения статьи я… не обновляюсь. Очень много изменений и как будто бы весь сайт надо переписать заново чтобы использовать новые крутые штуки, плюс еще разобраться как оно все работать будет.
В конце статьи объявлено полное изменение компонента с нуля и соответсвенно опять полная переделка.
Я использую с первых релизов компонент, и мне честно нравится как он развивается, но использую пока старые версии.
Что бы мне хотелось:
  • Максимально подробная документация с кучей примеров и объяснениями для самых маленьких(благо нейронки с этим отлично справляются. )
  • Готовый настроенный сайт с кучей примеров реализации и с комментариями кода, чтобы можно было себе куда то его поставить и эксперементировать.
Спасибо за самый мощный и актуальный компонент на Modx
    Сергей Сергеевич
    8 часов назад
    0
    Горячо плюсую за комментарий Мишы (Živeli brate!)
    Реально компонент — «космолет», но использовать на боевых сайтах страшно. Проще использовать компоненты общества, т.к в долгую это надёжнее: найдешь чье либо решение и адаптируешь под себя, и больше круг использования. И переписывать текущие сайты страшно, т.к что-то постоянно идет не так…
      Aleksandr Huz
      8 часов назад
      0
      Реально компонент — «космолет», но использовать на боевых сайтах страшно
      А почему страшно? я с 2022 года использую его с modx3 и все нормально. Сейчас делаю большой проект полностью без чанков, сниппетов и тивишек и это реально удобно и быстро. Так что страшного здесь ничего нет.
        Сергей Сергеевич
        4 часа назад
        0
        А что за тип сайта? Услуги, ИМ, портал?
          Aleksandr Huz
          4 часа назад
          +1
          Портал. Если заказчик будет не против, то потом смогу показать репозиторий, ведь вся разработка ведется в файлах. Вместо сниппетов использую модификаторы.

          Например, создал модификатор меню
          'menu' => function (int $rootId = 0, $level = 3) {
                  return query('modResource')
                  ->where([
                      'published' => 1,
                      'hidemenu' => 0,
                  ])
                  ->select('id,class_key,template,pagetitle,menutitle,introtext,alias,uri,link_attributes,parent')
                  // получаем дополнительные поля, если меню нужно построить от родителя с id 12
                  ->when($rootId === 12, function ($query) {
                      $query->select('cargo_tooltip,transport_tooltip');
                  })
                  // кешируем результат навечно, пока не сбросим кеш
                  ->cache(0)
                  ->menu($rootId, $level);
          },

          и в файловом чанке
          {foreach menu() as $item}
              <li class="nav-item{($item.id == $modx->resource->id) ? ' active' : ''}">
                  <a class="nav-link" href="{$item.uri}">{$item.menutitle ?: $item.pagetitle}</a>
               </li>
          {/foreach}
          
          или вывести меню с другой категории
          {foreach menu(12) as $item}
              <li class="nav-item{($item.id == $modx->resource->id) ? ' active' : ''}">
                  <a class="nav-link" href="{$item.uri}">{$item.menutitle ?: $item.pagetitle}</a>
               </li>
          {/foreach}
          Артур Шевченко
          3 часа назад
          0
          Ты используешь свой компонент, знаешь как он работает, где что подкрутить если вдруг не работает, а когда используешь чужой компонент, на который мало примеров, документация не вся, надо быть готовым изучать исходники и быть уверенным, что автор на связи и сможет оперативно поправить найденные баги.
            Aleksandr Huz
            2 часа назад
            +1
            Согласен. Но сейчас документация уже почти полная, примеры постепенно будут добавлятся.

            В режиме менеджера там и так все интуитивно понятно:
            • В меню компонента создаёшь блок с нужными полями.
            • В ресурсе добавляешь этот блок и заполняешь контентом.
            • В шаблоне выводишь блоки через сниппет [[!pbBlocks]].
            И все. Получается аналог мигса, только с визуальным конструктором и более мощным функционалом.

            А для более профессиональной разработки, конечно, нужно читать документацию. Я советую начать с маршрутов, контроллеров и конструктора запросов. Этого хватит, чтобы закрыть 80% задач.
              Артур Шевченко
              2 часа назад
              +2
              Да я всё понимаю. Моя причина в том, что времени нет, как только станет посвободнее изучу. Я просто описал причины, по которым другим может быть страшно использовать компонент. Плюсом ещё идёт отсутствие инфраструктуры для сегмента электронной коммерции платёжки, доставки, интеграции с CRM. Пойми правильно, это не критика твоей разработки, это просто факт, который, надеюсь, мы со временем исправим, а твой компонент может стать неплохой базой для внедрения современных подходов к разработке в наш любимый Modx.
        Aleksandr Huz
        8 часов назад
        0
        Согласен, примеров катастрофически не хватает как и времени для них, но они будут постепенно появляются.
        Документация потихоньку заполняется, и на данный момент все классы уже задокументированы.
        Артур Шевченко
        9 часов назад
        +1
        Что ж ты творишь-то? Это ж теперь ещё больше хочется попробовать)) Очень здоровское обновление. И планы огонь! Сюдя по всему ты решил написать свою CMS по мотивам Modx, ещё несколько месяцев и будет готово))
          Aleksandr Huz
          8 часов назад
          +1
          CMS писать точно не планирую)) Мне нравится админка MODX3, но хочется добавить больше гибкости и удобства, как в ларавел. И еще, чтобы админку можно было легко кастомизировать под проект.
            Артур Шевченко
            4 часа назад
            +1
            Могу только повторить: идея крутая, обязательно найду время изучить работу PageBlocks в деталях.
          Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
          12