В процессе работы с 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, часто catid, language и другие поля компонента.
  • Итого: item отвечает на вопрос «для какого конкретного объекта сейчас грузим/рендерим значения?».
subject (основной объект события)
  • В событиях onCustomFieldsBeforePrepareField, onCustomFieldsPrepareField и 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.articleitem->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 подключается к событиям отображения контента. Основные точки: onContentPrepare, onContentAfterTitle, onContentBeforeDisplay, onContentAfterDisplay.
  2. На этих шагах вызывается FieldsHelper::getFields(...).
  3. В onContentPrepare поля подготавливаются с prepareValue=true и складываются в $item->jcfields (для ручного использования в шаблонах/оверрайдах).
  4. В display-ветке (события onContentAfterTitle, onContentBeforeDisplay, onContentAfterDisplay) значения фильтруются по 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, но программным способом.

 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 (например, setFieldAttribute, setValue).

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

  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'] = ...). Но, можно сделать следующее:

  • Можно менять состояние объектов, переданных в аргументах (subject, form, fieldset) — то есть менять их свойства/атрибуты.
  • Можно работать через 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: нормализация значения перед рендером (media, list, radio, subform).
    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.

Об авторе

Толкачев Сергей Юрьевич

Толкачев Сергей Юрьевич

Joomla-разработчик. Контрибьютер ядра Joomla. Один из ведущих Telegram-канала русскоязычного Joomla-сообщества JoomlaFeed, один из модераторов чата русскоязычного Joomla-сообщества. Мои расширения в официальном маркетплейсе расширений Joomla - Joomla Extensions Directory. Имею публикации в официальном журнале международного Joomla-сообщества - Joomla Community Magazine и на официальном сайте русскоязычного Joomla-сообщества.

Муж. Отец 3 детей.

Россия, Саратов.

Расширения Joomla WebTolk

102 Всего расширений
12 Категорий
518 Выпущено версий
648311 Всего скачиваний