В процессе работы с Joomla бывает необходимо работать с пользовательским интерфейсом более тонко, чем обычно. Все формы Joomla состоят из стандартных полей, содержанием, стилем отображения, состоянием (включено/выключено, доступно для редактирования или нет и т.д.) можно управлять с помощью плагинов. Да и для нестандартных проектов хорошей практикой является создание одного системного или нескольких плагинов групп "под проект", в которых храниться весь "нестандарт".

В этой статье описаны все триггеры (события), которые вызываются через Event Dispatcher из administrator/components/com_fields/src/Helper/FieldsHelper.php, с привязкой к жизненному циклу (порядку этапов работы запроса), аргументам, изменяемым данным и дальнейшему распространению по Joomla. Это поможет вам работать с Joomla свободнее и не опасаясь при этом потерять изменения при очередном обновлении движка.

Подходы, описанные в статье, полезны в тех случаях, когда вы работаете с данными в com_fields - механизме создания и редактирования пользовательских полей ядра Joomla и при использовании FieldsHelper. Многие сторонние компоненты не используют эту возможность, поэтому данная статья будет полезна лишь частично.

Скрытый текст

Впервые решил попробовать напрячь ИИ и заставить его пособирать инфу по ядру Joomla и, надо сказать, впервые из этого вышло что-то более-менее удобоваримое, пригодное для дальнейшей обработки. Хотя стиль получился не совсем мой и читатель, думаю, легко это заметит - статья получилась шибко "структурированная", - но информация в ней полезная, поэтому оставим её в таком виде. После детальной проверки собранной информации можно сказать, что ИИ сэкономил времени час-полтора. Наверное, это неплохо.

Базовые понятия: context, item, subject

Перед разбором событий важно различать три важных понятия:

context (контекст)

  • Это строка формата компонент.секция, например com_content.article.

  • По этой строке Joomla понимает, для какого типа сущности загружать и рендерить кастомные поля.

  • Итого: context отвечает на вопрос «для чего именно сейчас работаем с полями?».

item (текущий объект данных)

  • Это объект конкретной записи, с которой вы работаете (статья, категория, контакт и т.д.).

  • Обычно в item есть как минимум id, часто catidlanguage и другие поля компонента.

  • Итого: item отвечает на вопрос «для какого конкретного объекта сейчас грузим/рендерим значения?».

subject (основной объект события)

  • В событиях onCustomFieldsBeforePrepareFieldonCustomFieldsPrepareField и onCustomFieldsAfterPrepareField это объект поля (одно кастомное поле).

  • В PrepareDomEvent это тоже объект поля, для которого строится XML-нода формы.

  • Итого: subject отвечает на вопрос «что именно мы сейчас меняем внутри обработчика события?».

Примеры context для разных компонентов Joomla

Контекст компонента материалов Joomla (com_content):

  • com_content.article — поля статьи.

  • com_content.categories — поля категории материалов.

Контекст компонента категорий Joomla (com_categories):

При редактировании категории через com_categories плагин fields приводит контекст к виду <extension>.categories (например, com_content.categories), чтобы выбрать правильные поля для целевого компонента. См.: plugins/system/fields/src/Extension/Fields.php:289 (onContentPrepareForm()).

Контекст компонента Контакты (com_contact):

  • com_contact.contact — поля контакта.

  • com_contact.mail — поля, которые могут использоваться в контексте отправки письма контакту.

Пример для новичков

Если событие пришло с  context = com_content.article ,  item->id = 35  и  subject->name = author_badge, то это значит:

  1. Работаем с полями статьи (com_content.article).

  2. Конкретная статья имеет ID 35.

  3. Сейчас в обработчике мы работаем с одним поле, у которого системное имя author_badge.

Где это в жизненном цикле полей Joomla?

Следует различать 2 процесса, у которых есть общие этапы (например, определение context, загрузка полей и обработка через плагины), но разные цели: цикл редактирования формы (нужно собрать и показать поля в админке) и цикл отображения значений (нужно подготов��ть и вывести значения полей в контенте снаружи сайта).

Цикл 1: Редактирование формы

  1. Модель компонента запускает preprocessForm(...).

  2. Базовый вызов события onContentPrepareForm происходит в FormBehaviorTrait::preprocessForm(), когда модель вызывает parent::preprocessForm($form, $data, $group), файл libraries/src/MVC/Model/FormBehaviorTrait.php:184.

  3. Примеры моделей:

    1. administrator/components/com_content/src/Model/ArticleModel.php:1007 (модель материала в админке Joomla)

    2. administrator/components/com_categories/src/Model/CategoryModel.php:394 (модель категории в стандартном компоненте категорий)

    3. components/com_contact/src/Model/FormModel.php:210 (модель формы обратной связи компонента контактов снаружи сайта)

    4. В отдельных моделях возможен ручной вызов события через dispatch(...) и объект события.

    5. components/com_contact/src/Model/ContactModel.php:384 (модель одного контакта во фронтенде, метод getForm())

  4. Системный плагин fields (plugins/system/fields/src/Extension/Fields.php) отрабатывает событие onContentPrepareForm и вызывает FieldsHelper::prepareForm(...). До вызова FieldsHelper плагин нормализует context (включая com_categories.category... -> <extension>.categories) и приводит $data к объекту.

  5. FieldsHelper::prepareForm(...) (\Joomla\Component\Fields\Administrator\Helper\FieldsHelper) строит поля формы.

  6. Далее FieldsHelper загружает список полей, строит XML группы com_fields, вызывает событие onCustomFieldsPrepareDom, загружает XML в Form (бывший JForm) и проставляет значения.

  7. Если метод FieldsHelper::extract(...) ничего не вернул или в контексте нет полей, форма не модифицируется.

  8. После сохранения/удаления системный плагин fields сохраняет или очищает значения полей.

  9. Сохранение: вычисляет итоговое значение по каждому полю и пишет в #__fields_values через FieldModel::setFieldValue(...).

  10. Удаление: очищает значения через cleanupValues(...).

  11. Для пользователей com_users (события onUserAfterSave/onUserAfterDelete) используется та же логика через проксирование данных в content-события. Например, внутри события onUserAfterSave вызывается событие onContentAfterSave с контекстом пользователя.

Цикл 2: Отображение значений

  1. Системный плагин fields подключается к событиям отображения контента. Основные точки: onContentPrepareonContentAfterTitleonContentBeforeDisplayonContentAfterDisplay.

  2. На этих шагах вызывается FieldsHelper::getFields(...).

  3. В onContentPrepare поля подготавливаются с prepareValue=true и складываются в $item->jcfields (для ручного использования в шаблонах/оверрайдах).

  4. В display-ветке (события onContentAfterTitleonContentBeforeDisplayonContentAfterDisplay) значения фильтруются по display-позиции и рендерятся через layout fields.render. И тогда в вашем материале или контакте вы получаете отрендеренные поля в позициях "до вывода контента", "после вывода контента", "после заголовка".

  5. Условия и ветвления отображения.

    1. Если context не поддерживается (FieldsHelper::extract(...)), обработка пропускается.

    2. Для com_tags.tag используется ветка с перекладкой контекста на type_alias.

Блок-схемы процессов

Ниже 2 блок-схемы для двух процессов из статьи.

Отображение поля для редактирования (форма создания/редактирования в Joomla)

Скрытый текст

Отображение значения поля на пользовательской части сайта Joomla

Скрытый текст

События из FieldsHelper

1) onCustomFieldsBeforePrepareField (перед рендером пользовательского поля)

Событие вызывается перед основным рендером каждого конкретного поля в методе FieldsHelper::getFields(). Условие вызова: только в ветке подготовки значения, когда в getFields() есть $item->id и установлен флаг $prepareValue==true.

Аргументом события onCustomFieldsBeforePrepareField является экземпляр класса объекта события $event - BeforePrepareFieldEvent, наследующийся от AbstractPrepareFieldEvent: - libraries/src/Event/CustomFields/BeforePrepareFieldEvent.php:21 (класс BeforePrepareFieldEvent) - libraries/src/Event/CustomFields/AbstractPrepareFieldEvent.php:31 (класс AbstractPrepareFieldEvent)

Из $event можно получить:

  1. $event->getContext(): string - контекст 

  2. $event->getItem(): object - материал, контакт, категорию и т.д. Уточняем ЧТО это за зверь по контексту. 

  3. $event->getField(): object - это subject, само поле.

Что можно менять:

  1. Свойства поля ($field->value$field->rawvalue, доп. свойства вроде $field->apivalue).

  2. Объект item и context менять напрямую как аргументы нельзя (immutable event, то есть событие с «непереназначаемыми» аргументами), но можно менять сам объект item, если нужно.

Куда изменения идут дальше: тот же объект поля передается в onCustomFieldsPrepareField, затем участвует в финальном значении field->value, дальше попадает в jcfields или layout поля.

2) onCustomFieldsPrepareField (момент рендера поля)

Момент вызова: сразу после Before... в FieldsHelper::getFields(). Условие вызова: только в той же prepare-ветке ($item->id + активный $prepareValue/совпадение display-режима поля). На этом событии плагины пользовательских полей подключают лейауты плагина пользовательского поля из папки tmpl.

Event class (класс объекта события): - libraries/src/Event/CustomFields/PrepareFieldEvent.php:25 (class PrepareFieldEvent)

Event-методы:

  1. $event->getContext(): string - получаем контекст выполнения

  2. $event->getItem(): object - получаем материал, категорию и иже с ними

  3. $event->getField(): object - получаем само поле

  4. $event->addResult(mixed $result): static - сохраняем результат работы (через ResultAware, то есть через встроенный механизм накопления результата)

  5. $event->getArgument('result', [])- метод для получения аргументов класса события. Обычно используется после вызова события в helper/dispatcher-коде.

Что можно менять:

  1. Добавлять результат рендера через $event->addResult(...) в адаптере базового плагина \Joomla\Component\Fields\Administrator\Plugin\FieldsPlugin, файл administrator/components/com_fields/src/Plugin/FieldsPlugin.php, от которого наследуются все плагины пользовательских полей.

  2. Модифицировать subject (поле) по месту.

Куда изменения идут дальше: FieldsHelper берет result, фильтрует пустые значения, склеивает массив в строку и передает в onCustomFieldsAfterPrepareField.

3) onCustomFieldsAfterPrepareField (после получения рендера поля)

Момент вызова: после того как получен рендер-результат поля. Условие вызова: только в той же prepare-ветке getFields(); если prepare не выполняется, событие не вызывается.

Event class (класс объекта события): класс AfterPrepareFieldEvent (файл libraries/src/Event/CustomFields/AfterPrepareFieldEvent.php).

Event API (методы):

  1. $event->getContext(): string - контекст вида com_content.article и т.д.

  2. $event->getItem(): object - сущность, для которой сделали поле.

  3. $event->getField(): object - само поле

  4. $event->getValue(): mixed - а вот тут уже можно получить финальное начение поля на текущий момент.

  5. $event->updateValue(mixed $value): static

На этом этапе можно менять финальный вывод через $event->updateValue(...).

Дальше результат изменений попадает в $field->value (смотрим снова FieldsHelper::getFields()).

Примечание по совместимости: передача value по ссылке сохранена для обратной совместимости со старым кодом, но помечена как устаревшая (deprecated).

4) onCustomFieldsPrepareDom

На этом этапе можно изменять поля XML-формы. Например, в зависимости от условий устанавливать полям атрибуты readonly и disabled, добавлять / удалять css-классы, описания, согласно синтаксису XML-манифестов Joomla, но программным способом.

<?php
public function onCustomFieldsPrepareDom($field, \DOMElement $parent, Form $form)
    {
        $fieldNode = parent::onCustomFieldsPrepareDom($field, $parent, $form);

        if (!$fieldNode) {
            return $fieldNode;
        }

        $fieldNode->setAttribute('disabled', 'true');
        $fieldNode->setAttribute('readonly', 'true');
        $fieldNode->setAttribute('class', 'text-danger fw-bold');

        // Возвращаем изменённое поле
        return $fieldNode;
    }

Момент вызова №1: при сборке XML формы в prepareForm().

Момент вызова №2: в FieldModel::checkDefaultValue() при проверке default value через правило валидации.

Event class для события - Joomla\CMS\Event\CustomFields\PrepareDomEvent:

Event API (методы):

  1. $event->getField(): object - (поле, основной объект события) 

  2. $event->getFieldset(): \DOMElement - филдсет поля

  3. $event->getForm(): \Joomla\CMS\Form\Form - форма целиком (Joomla\CMS\Form\Form)

Что можно менять:

  1. DOM-структуру поля (атрибуты, child-узлы, <option>validate и т.д.).

  2. Объект form (например, setFieldAttributesetValue).

Куда изменения идут дальше:

  1. XML загружается в Form. - administrator/components/com_fields/src/Helper/FieldsHelper.php:486 (prepareForm())

  2. Затем значения поля проставляются в form group com_fields. - administrator/components/com_fields/src/Helper/FieldsHelper.php:511 (prepareForm())

5) onCustomFieldsGetTypes

Момент вызова: при сборке списка типов полей (getFieldTypes()). Этот список мы видим при создании нового поля в панели администратора. Один плагин может реализовывать несколько типов полей. Для этого плагин должен иметь несколько лейаутов в папке tmpl и соответствующих им xml-файлов параметров в папке params. Например, tmpl/fieldtype1.php и params/fieldtype1.xml. Событие берёт типы полей именно по наличию php-файлов лейаутов.

Event class - \Joomla\CMS\Event\CustomFields\GetTypesEvent (файл libraries/src/Event/CustomFields/GetTypesEvent.php)

Event API (методы):

  1. $event->addResult(array $typeDefinitionList): static (через ResultAware, основной способ для обработчика)

  2. $event->getArgument('result', []) (обычно используется после вызова события в helper/dispatcher-коде)

Аргументы:

  1. Событие без обязательного subject payload (payload = набор данных, переданных в событие).

  2. Канал result (массив описаний типов).

Что можно менять:

  1. Добавлять описания типов через $event->addResult(...): - type - label - path (где form fields) - rules (где form rules)

  2. Базовый (родительский класс, от которого наследуются плагины полей) FieldsPlugin делает это автоматически.

Куда изменения идут дальше: FieldsHelper нормализует path и rules, затем использует их в prepareForm() для FormHelper::addFieldPath/addRulePath.

Ограничения мутабельности (возможности менять данные) и immutable events

CustomFields events наследуются от immutable-базы: libraries/src/Event/AbstractImmutableEvent.php:22. Это означает, что нельзя переназначать аргументы события (например, $event['context'] = ...). Но, можно сделать следующее:

  • Можно менять состояние объектов, переданных в аргументах (subjectformfieldset) — то есть менять их свойства/атрибуты.

  • Можно работать через ResultAware - есть метод addResult(), который позволяет добавлять результат обработчика в общий итог.

  • Для AfterPrepareFieldEvent можно менять значение через updateValue (обновить итоговый вывод поля).

Как данные прокидываются дальше по Приложению Joomla

Поток getFields(...)

  1. Загружает значения из #__fields_values (rawvalue/value), учитывает valuesToOverride и default_value.

    1. administrator/components/com_fields/src/Helper/FieldsHelper.php:184 (getFields())

    2. administrator/components/com_fields/src/Helper/FieldsHelper.php:195 (getFields())

    3. administrator/components/com_fields/src/Helper/FieldsHelper.php:204 (getFields())

    4. administrator/components/com_fields/src/Helper/FieldsHelper.php:207 (getFields())

  2. Прогоняет цепочку Before -> Prepare -> After (если активна prepare-ветка, то есть этап подготовки отображаемого значения).

  3. Возвращает массив полей; далее он используется:

    1. для $item->jcfields в onContentPrepare

    2. для layout рендера HTML-вёрстки поля в onContentAfterTitle/onContentBeforeDisplay/onContentAfterDisplay

Поток prepareForm(...)

  1. Строит XML <fields name="com_fields">.

  2. На каждый field вызывает onCustomFieldsPrepareDom, где можно работать с XML-формой через \DOMElement (fieldset) и объект Joomla Form.

  3. Загружает XML в форму и выставляет значения.

Практические примеры из com_fields и core plugins

  1. Пример BeforePrepareField: нормализация значения перед рендером (medialistradiosubform).

    1. plugins/fields/media/src/Extension/Media.php (beforePrepareField())

    2. plugins/fields/list/src/Extension/ListPlugin.php (beforePrepareField())

    3. plugins/fields/radio/src/Extension/Radio.php (beforePrepareField())

    4. plugins/fields/subform/src/Extension/Subform.php (beforePrepareField())

  2. Пример PrepareField: базовый HTML-рендер через layout.

    1. administrator/components/com_fields/src/Plugin/FieldsPlugin.php:211 (onCustomFieldsPrepareField())

  3. Пример AfterPrepareField: пост-обработка HTML (email cloak).

    1. plugins/content/emailcloak/src/Extension/EmailCloak.php:108 (onCustomFieldsAfterPrepareField())

  4. Пример PrepareDom: сборка XML ноды поля, валидации и options.

    1. administrator/components/com_fields/src/Plugin/FieldsPlugin.php:245 (onCustomFieldsPrepareDom())

    2. administrator/components/com_fields/src/Plugin/FieldsListPlugin.php:38 (onCustomFieldsPrepareDom())

    3. plugins/fields/subform/src/Extension/Subform.php:265 (onCustomFieldsPrepareDom())

Рецепты

Как прокинуть своё кастомное значение в layout поля через плагин

Допустим, что нам нужно передать собственное дополнительное значение из плагина в layout этого поля (файл plugins/fields/<your_plugin>/tmpl/<type>.php). Мы можем на событии onCustomFieldsBeforePrepareField добавить своё свойство в объект поля ($field). А в onCustomFieldsPrepareField (обычно базовый FieldsPlugin) этот же $field уже доступен в layout, поэтому наше уникальное свойство можно читать напрямую.

Пример плагина:

<?php
use Joomla\CMS\Event\CustomFields\BeforePrepareFieldEvent;
use Joomla\Component\Fields\Administrator\Plugin\FieldsPlugin;
use Joomla\Event\SubscriberInterface;

final class MyCustomSystemPlugin extends FieldsPlugin implements SubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            'onCustomFieldsBeforePrepareField' => 'beforePrepareField',
        ];
    }

    public function beforePrepareField(BeforePrepareFieldEvent $event): void
    {
        $field = $event->getField();

        /**
         * Этот метод срабатывает абсолютно для КАЖДОГО поля. Поэтому срабатываем только в нужном нам типе.
         * В данном случае это поле Яндекс.Карты wtyandexmap
         */
        if ($field->type !== 'wtyandexmap') { 
            return;
        }

         /**
          * Тут любая логика. А мы к примеру, добавим, 
          * свою картинку в класс поля, 
          * чтобы вытащить её напрямую в макете поля.
          * Наименования кастомных свойств лучше брендировать своим префиксом
          * или префиксом проекта, дабы не было случайных пересечений
          * с другими расширениями.
          */ 
        $field->wt_custom_layout_data = [
            'icon' => 'images/path/to/image.webp'
        ];
    }
}

Пример layout-файла поля (plugins/fields/mycustomsystem/tmpl/mycustomsystem.php):

<?php
/** @var \stdClass $field */

echo '<div class="cf-mytype">';
echo '<span class="cf-value">' . htmlspecialchars((string) $field->value, ENT_QUOTES, 'UTF-8') . '</span>';

// Теперь тут доступно наше кастомное свойство.  
if (!empty($field->wt_custom_layout_data['icon'])) {
    echo HTMLHelper::image($field->wt_custom_layout_data['icon'], 'icon-alt', ['class' => 'img-fluid', 'title'=>'icon title']);
}

echo '</div>';

Где это в ядре Joomla подтверждается:

  1. FieldsHelper передаёт поле как subject в BeforePrepareFieldEvent. - administrator/components/com_fields/src/Helper/FieldsHelper.php (метод getFields())

  2. Затем то же поле передаётся в PrepareFieldEvent. - administrator/components/com_fields/src/Helper/FieldsHelper.php (тот же метод getFields())

  3. Базовый FieldsPlugin внутри onCustomFieldsPrepareField() включает макет вывода, где переменная $field доступна напрямую. - administrator/components/com_fields/src/Plugin/FieldsPlugin.php:211 (метод onCustomFieldsPrepareField())

Примеры кода

Изменение или замена HTML-вывода значения (value) в момент рендера поля

Пример реализации onCustomFieldsPrepareField для изменения итогового HTML значения поля (то, что увидит пользователь на странице). В результате мы контролируем конечную разметку, которую FieldsHelper положит в $field->value.

<?php
public function onCustomFieldsPrepareField($context, $item, $field)
{
    if ($field->type !== 'mytype') {
        return '';
    }
    // Тут мы полностью заменяем HTML-вёрстку из $field->getInput() класса поля на своё 
    // или делаем return parent::onCustomFieldsPrepareField($context, $item, $field);
    return '<span class="text-danger">По каким-то причинам мы не хотим показывать value этого поля. Увы... а могли бы сделать <code>return htmlspecialchars((string) $field->value, ENT_QUOTES, "UTF-8");</code>.</span>';
}

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

Изменение или замена HTML-вывода значения (value) в ПОСЛЕ рендера поля

Комментарий к примеру: пример пост-обработки уже готового значения поля на onCustomFieldsAfterPrepareField. Результат: можно обернуть, заменить или дополнительно фильтровать HTML перед выводом.

<?php
use Joomla\CMS\Event\CustomFields\AfterPrepareFieldEvent;

public function onCustomFieldsAfterPrepareField(AfterPrepareFieldEvent $event): void
{
    if (empty($event->getValue())) {
        return;
    }

    $event->updateValue('<div class="wrapped-field">' . $event->getValue() . '</div>');
}

Работа с XML-формой поля Joomla. Добавление / изменение атрибутов поля и т.д.

Комментарий к примеру: пример построения DOM-ноды формы (XML-элемента <field>) на onCustomFieldsPrepareDom. Результат: поле появляется в Form с нужным атрибутом validate=color и участвует в стандартной обработке формы.

<?php
// Пример из плагина пользовательского поля Color
// файл plugins/fields/color/src/Extension/Color.php
use Joomla\CMS\Form\Form;

public function onCustomFieldsPrepareDom($field, \DOMElement $parent, Form $form)
{
  $fieldNode = parent::onCustomFieldsPrepareDom($field, $parent, $form);

  if (!$fieldNode) {
      return $fieldNode;
  }

  $fieldNode->setAttribute('validate', 'color');

  return $fieldNode;
}

Этот механизм может быть полезен тогда, когда вы работаете со стандартными полями Joomla в своём расширении и вам необходимо модифицировать состояние полей в зависимости от данных. Например, не показывать поле и исключить его из обработки в зависимости от наличия данных в других полях: не указал пользователь API-ключ для интеграции со сторонним сервисом - не показываем ему остальную форму вообще (можно вернуть null и не добавлять узел поля в поля в DOM-дерево, но тут нужно учитывать логику обработки данных в моделях - как они к этому отнесутся). Или не даём редактировать эти поля с помощью readonly и disabledНо нужно учитывать, что это событие ограничивает применимость логики к сущности одного конкретного поля и описывает его поведение. Если нужно работать большими блоками в масштабах всей формы, то разумнее использовать событие onContentPrepareForm и манипулировать формой на том этапе. Также нужно не забывать о том, что если поле не пришло в  data['com_fields'], при сохранении в поле может остаться прежнее rawvalue.

Также эта статья на сайте автора.

Ресурсы сообщества:

Telegram: