For the complete documentation index, see llms.txt. This page is also available as Markdown.

Вывод пользовательского контента

Как текст пользователя превращается в готовую разметку страницы

Сообщение форума, комментарий, статья, описание файла — всё это текст, который написал посетитель, и по пути на страницу с ним нужно сделать несколько вещей подряд: очистить разметку, превратить вставленную ссылку на видео в плеер, нарисовать смайлы. Этим занимается конвейер контентаJohncms\Content\ContentRendererInterface.

Главное правило

Один вызов вместо цепочки. Не собирайте последовательность «санитайзер → медиа → смайлы» вручную: конвейер и появился затем, чтобы она была в одном месте.

Как пользоваться

Внедрите интерфейс через конструктор и передайте текст в render():

<?php

declare(strict_types=1);

namespace Johncms\Modules\MyModule\Application\Services;

use Johncms\Content\ContentContext;
use Johncms\Content\ContentRendererInterface;
use Twig\Markup;

final readonly class ReviewTextFormatter
{
    public function __construct(
        private ContentRendererInterface $content,
    ) {
    }

    public function format(Review $review): Markup
    {
        return $this->content->render(
            $review->text,
            new ContentContext(adminSmilies: $review->authorIsStaff),
        );
    }
}

Результат — Twig\Markup, то есть «готовая разметка» по договорённости. Шаблон печатает такое значение обычным способом, без |raw:

Три метода

Метод
Что возвращает

render()

разметку всегда: у пустого текста будет пустой Markup

renderOrNull()

то же самое, но null, если показывать нечего

toPlainText()

тот же контент без разметки — для превью в списке, заголовка страницы, уведомления

Markup — объект, а объект истинный, даже когда он пустой, поэтому {% if text %} в шаблоне на пустом Markup сработает всегда. Если у источника «текста может не быть» — берите renderOrNull().

toPlainText() рисует медиа и смайлы и только потом снимает теги — так содержимое удалённого элемента не всплывёт видимым текстом:

Контекст

ContentContext — это то, что конвейер знает о тексте. Оба поля необязательны:

Поле
Значение

policy

Политика очистки: значение enum HtmlPolicy или имя политики, которую объявил модуль. По умолчанию HtmlPolicy::RichContent — см. Очистка HTML

adminSmilies

Рисовать ли смайлы, доступные только персоналу. По умолчанию false

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

Когда конвейер не нужен

Он предназначен для контента, который выводится на страницу как разметка. Отдельно от него остаётся санитайзер HtmlSanitizerInterface — для значений, которые чистят, но не выводят как контент: заголовок для тега <title>, подпись, текст для поиска. Такие вещи проходят через sanitize() или toPlainText() санитайзера, а не через конвейер.

Шаги конвейера

Шаг — это сервис, реализующий Johncms\Content\Transformer\ContentTransformerInterface. Встроенных три:

Шаг
Приоритет
Что делает

OembedTransformer

100

превращает <oembed url="…"> редактора в плеер

ImagePopupTransformer

50

оборачивает картинку в ссылку, открывающую просмотрщик

SmiliesTransformer

−100

рисует смайлы по готовому тексту

Приоритет задаёт порядок: чем больше, тем раньше.

Свой шаг

Править ядро не нужно — контейнер вешает тег на сервис по одному тому, что он реализует интерфейс. Достаточно, чтобы сервис попал в контейнер обычным способом (load() с autoconfigure()).

Правила для шага:

  • Документ разбирается один раз и общий для всех. Шаг правит дерево, которое ему дали, и ничего не возвращает. Он не разбирает и не собирает текст сам и не работает с HTML как со строкой.

  • Разметка попадает в дерево через DOM. setAttribute() и createElement() экранируют значения сами. Для куска разметки крупнее одного элемента есть HtmlFragment::nodes().

  • Шаг выполняется на каждом тексте сайта. Ограничивайтесь тем селектором, который вам действительно нужен.

Медиа: свой сайт видеохостинга

Редактор сохраняет только то, что вставил автор — <oembed url="…">. Разметки плеера в базе нет намеренно: иначе внешний вид всех старых сообщений застыл бы навсегда. Плеер строится на выводе, поэтому правка шаблона меняет и уже написанные сообщения.

Провайдер узнаёт адрес и называет шаблон. С документом он не работает — обход <oembed> делает OembedTransformer один раз для всех провайдеров:

Шаблон — обычный шаблон, поэтому тема переопределяет внешний вид плеера, повторив его путь (см. Создание собственного шаблона). Шаблоны встроенных плееров лежат в themes/default/templates/content/embeds/:

Провайдеры опрашиваются по убыванию приоритета до первого, который вернул не null. Так модуль может заменить встроенный провайдер: пусть он объявляет те же адреса с приоритетом выше.

Адрес, который не признал никто, — не ошибка. Элемент остаётся в тексте как есть, ссылка не теряется, и провайдер, добавленный позже, подхватит уже написанные сообщения.

Разбор HTML

Разбирает текст единственный класс — Johncms\Content\Html\HtmlFragment. За ним стоит HTML5-парсер, который PHP 8.4 поставляет в расширении ext-dom (Dom\HTMLDocument); отдельной библиотеки для работы с DOM в JohnCMS нет.

Сообщение — это фрагмент страницы, а не документ, поэтому оно разбирается как содержимое body (там, где фрагменту и положено быть по правилам HTML5), и наружу отдаётся только это содержимое.

Как проверить свой шаг

Шаг тестируется без контейнера и без базы: разобрать фрагмент, выполнить шаг, собрать обратно.

Готовые тесты конвейера лежат в tests/Unit/Content/ — их можно взять за образец.

Последнее обновление

Это было полезно?