Как работать с меню компонента в админке Joomla с помощью preset.xml и плагина

У компонента Joomla есть несколько уровней присутствия в панели администратора. Самый простой уровень создаётся установщиком компонента: если в манифесте есть <administration><menu>...</menu></administration>, Joomla добавит пункт в защищённый администраторский menutype=main. Но этого часто мало. Нормальному компоненту нужны группы ссылок, быстрые действия, ссылки на категории, поля, workflow, дашборд, отдельные наборы пунктов для разных административных модулей и возможность добавлять или скрывать пункты динамически.

Для этого в Joomla 6.1 используется механизм пресетов меню.

preset.xml
Это XML-файлы с деревом административных пунктов меню. Их читает хелпер панели управления Joomla administrator/components/com_menus/src/Helper/MenusHelper.php, а выводят модули типов Меню панели управления (administrator/modules/mod_menu) и Подменю панели управления (administrator/modules/mod_submenu). Плагин может подключиться к этому механизму двумя способами: зарегистрировать дополнительный preset через MenusHelper::addPreset() или изменить уже загруженное дерево пунктов на событии onPreprocessMenuItems.

В этой статье preset.xml используется как общее название подхода. В реальном коде Joomla файл не обязан называться именно preset.xml: имя XML-файла становится именем пресета. Например, administrator/components/com_content/presets/content.xml даёт пресет с именем content.

Базовое меню компонента Joomla из XML манифеста расширения

Перед рассказом о preset-файлах важно отделить обычное меню компонента от расширенного меню.

При установке компонента Joomla\CMS\Installer\Adapter\ComponentAdapter::_buildAdminMenus() читает секцию <administration><menu> в XML-манифесте компонента и создаёт записи в таблице #__menu базы данных для панели администратора.

Упрощённо логика такая:

$menuElement = $this->getManifest()->administration->menu;

if (!$menuElement) {
    return true;
}

if (\in_array((string) $menuElement['hidden'], ['true', 'hidden'])) {
    return true;
}

$data['menutype']     = 'main';
$data['client_id']    = 1;
$data['title']        = (string) trim($menuElement);
$data['type']         = 'component';
$data['parent_id']    = 1;
$data['component_id'] = $componentId;
$data['img']          = ((string) $menuElement->attributes()->img) ?: 'class:component';
$data['link']         = 'index.php?option=' . $option . $qstring;

Если в XML-манифесте расширения есть <submenu>, установщик создаёт дочерние пункты под главным пунктом компонента. Для них поддерживаются атрибуты alias, type, img, link, act, task, controller, view, layout, sub. Это меню живёт в базе данных и попадает в стандартный контейнер “Компоненты”, потому что метод MenusHelper::getMenuItems('main', ...) читает записи из таблицы #__menu в базе данных.

Секция компонентов в меню joomla в админке

Пример кода для XML-манифеста ниже использует несуществующий компонент com_example. Он нам нужен только для того, чтобы показать поля манифеста, которые читает ComponentAdapter::_buildAdminMenus().

 

<extension type="component" method="upgrade">
    <name>com_example</name>

    <administration>
        <menu img="class:puzzle-piece" view="items">COM_EXAMPLE</menu>
        <submenu>
            <menu view="items">COM_EXAMPLE_ITEMS</menu>
            <menu view="categories" link="option=com_categories&amp;view=categories&amp;extension=com_example">
                COM_EXAMPLE_CATEGORIES
            </menu>
        </submenu>
    </administration>
</extension>

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

Где Joomla ищет пресеты для меню и дашбордов?

Список пресет-файлов строит метод MenusHelper::getPresets(). Метод лениво заполняет статическое свойство MenusHelper::$presets и ищет XML файлы пресетов в двух местах.

Первое место - каталог presets включённых компонентов:

$components = ComponentHelper::getComponents();

foreach ($components as $component) {
    if (!$component->enabled) {
        continue;
    }

    $folder = JPATH_ADMINISTRATOR . '/components/' . $component->option . '/presets/';

    if (!is_dir($folder)) {
        continue;
    }

    $presets = Folder::files($folder, '.xml');

    foreach ($presets as $preset) {
        $name  = File::stripExt($preset);
        $title = strtoupper($component->option . '_MENUS_PRESET_' . $name);
        static::addPreset($name, $title, $folder . $preset);
    }
}

Второе место - папка с переопределениями активного шаблона админки:

$tpl = JPATH_THEMES . '/' . $app->getTemplate() . '/html/com_menus/presets';

Если там есть XML-файлы, Joomla тоже добавит их в список пресетов. При создании своего компонента самый очевидный путь - положить файл в administrator/components/com_example/presets/example.xml и не забыть включить папку presets в манифест компонента:

<administration>
    <files folder="admin">
        <folder>presets</folder>
        <folder>services</folder>
        <folder>src</folder>
        <folder>tmpl</folder>
    </files>
</administration>

Название пресета будет example. Заголовок в списке пресетов Joomla попробует перевести по ключу COM_EXAMPLE_MENUS_PRESET_EXAMPLE, потому что getPresets() строит ключ как strtoupper($component->option . '_MENUS_PRESET_' . $name).

Есть важное ограничение: MenusHelper::addPreset() хранит пресеты в массиве по имени. Если имя повторяется, новый пресет может заменить ранее добавленный. В комментарии к методу сказано, что пресеты с одинаковым именем заменяют предыдущие, кроме пресета с именем joomla; в самом коде защита стоит именно на имени joomla. Поэтому для своего компонента лучше не называть файл default.xml, system.xml, components.xml, content.xml, menus.xml или users.xml, если вы не хотите конфликтовать с пресетами ядра Joomla.

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

Модули типов "Меню панели администратора" и "Подменю панели администратора" умеют работать в двух режимах.

Например, если параметр menutype модуля Меню панели администратора не равен *, модуль берёт пункты из базы данных:

$this->root = MenusHelper::getMenuItems($menutype, true);

Если menutype равен * (символ звёздочки означает "по умолчанию", аналогично выбору языка в списке языков), модуль не читает дерево пунктов меню из таблицы #__menu в базе, а загружает XML-пресет:

$name       = $this->params->get('preset', 'default');
$this->root = MenusHelper::loadPreset($name);

Вот пример изменения внешнего вида и содержимого меню панели администратора с помощью переключения пресетов в настройках.

В mod_submenu логика такая же, только пресет по умолчанию используется другой:

$menutype = $data['params']->get('menutype', '*');

if ($menutype === '*') {
    $name         = $data['params']->get('preset', 'system');
    $data['root'] = MenusHelper::loadPreset($name);
} else {
    $data['root'] = MenusHelper::getMenuItems($menutype, true);
}

Это даёт два сценария для работы:

Первый сценарий - живой preset. Вы создаёте модуль mod_menu или mod_submenu, ставите menutype = * и выбираете свой пресет. Пункты не импортируются в таблицу #__menu, а каждый раз строятся из XML и проходят через обработку прав, проверки ограничений, доступности компонентов и плагинов.

joomla выбор пресета меню в настройках модуля меню в панели администратора
Это поле и есть menutype - выбор пресета меню в настройках модуля меню в панели администратора Joomla

Второй сценарий - импорт preset в пользовательское администраторское меню. В форме создания меню com_menus есть поле preset типа MenuPreset. Оно доступно для административных меню.

joomla admin menu import preset into menu

При сохранении метод MenuController::save() вызывает:

MenusHelper::installPreset($preset, $menutype);

Метод installPreset() в свою очередь загружает XML через loadPreset($preset, false) и затем создаёт реальные записи в таблице #__menu в базе данных. Такой вариант удобен, если администратор должен потом руками менять порядок, скрывать пункты или дополнять меню через стандартный интерфейс управления меню.

Формат preset.xml

Формат описан в administrator/components/com_menus/presets/menu.xsd. Корневой элемент называется <menu>, внутри идут вложенные <menuitem>. Тип пункта обязателен и может быть одним из пяти значений:

  • component;
  • container;
  • heading;
  • separator;
  • url.

Вот реальный пример из файла ядра Joomla - компонента материалов administrator/components/com_content/presets/content.xml:

<menuitem
    title="COM_CONTENT_MENUS_ARTICLE_MANAGER"
    type="component"
    element="com_content"
    link="index.php?option=com_content&amp;view=articles"
    quicktask="index.php?option=com_content&amp;task=article.add"
    quicktask-title="COM_CONTENT_MENUS_NEW_ARTICLE"
/>

При парсинге метод MenusHelper::parseXmlNode() превращает XML-узел в объект класса Joomla\CMS\Menu\AdministratorMenuItem. Метод читает такие атрибуты:

$item->type       = (string) $node['type'];
$item->title      = (string) $node['title'];
$item->alias      = (string) $node['alias'];
$item->link       = (string) $node['link'];
$item->target     = (string) $node['target'];
$item->element    = (string) $node['element'];
$item->class      = (string) $node['class'];
$item->icon       = (string) $node['icon'];
$item->access     = (int) $node['access'];
$item->scope      = (string) $node['scope'] ?: 'default';
$item->ajaxbadge  = (string) $node['ajax-badge'];
$item->dashboard  = (string) $node['dashboard'];

Несколько атрибутов не остаются прямыми свойствами, а попадают в params пункта меню:

$params->set('menu-permission', (string) $node['permission']);
$params->set('menu-quicktask', (string) $node['quicktask']);
$params->set('menu-quicktask-title', (string) $node['quicktask-title']);
$params->set('menu-quicktask-icon', (string) $node['quicktask-icon']);
$params->set('menu-quicktask-permission', (string) $node['quicktask-permission']);
$params->set('ajax-badge', $item->ajaxbadge);
$params->set('dashboard', $item->dashboard);

У preset-файла нет отдельного класса модели. Всё поведение строится из атрибутов XML, класса AdministratorMenuItem, работой с Registry params и последующего preprocess() в модулях меню.

Пример preset для компонента

Это выдуманный пример для компонента com_example, структура которого повторяет пресеты из com_content и com_users.

<?xml version="1.0"?>
<menu
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="urn:joomla.org"
    xsi:schemaLocation="urn:joomla.org menu.xsd"
    >
    <menuitem
        title="COM_EXAMPLE_MENUS"
        type="heading"
        icon="puzzle-piece"
        class="class:puzzle-piece"
        dashboard="example"
        >
        <menuitem
            title="COM_EXAMPLE_MENUS_ITEMS"
            type="component"
            element="com_example"
            link="index.php?option=com_example&amp;view=items"
            quicktask="index.php?option=com_example&amp;task=item.add"
            quicktask-title="COM_EXAMPLE_MENUS_NEW_ITEM"
            quicktask-icon="plus"
        />

        <menuitem
            title="COM_EXAMPLE_MENUS_CATEGORIES"
            type="component"
            element="com_categories"
            link="index.php?option=com_categories&amp;view=categories&amp;extension=com_example"
            quicktask="index.php?option=com_categories&amp;extension=com_example&amp;task=category.add"
            quicktask-title="COM_EXAMPLE_MENUS_NEW_CATEGORY"
        />

        <menuitem type="separator" />

        <menuitem
            title="MOD_MENU_FIELDS"
            type="component"
            element="com_fields"
            link="index.php?option=com_fields&amp;view=fields&amp;context=com_example.item"
        />

        <menuitem
            title="MOD_MENU_FIELDS_GROUP"
            type="component"
            element="com_fields"
            link="index.php?option=com_fields&amp;view=groups&amp;context=com_example.item"
        />

        <menuitem
            title="COM_EXAMPLE_MENUS_WORKFLOWS"
            type="component"
            element="com_workflow"
            link="index.php?option=com_workflow&amp;view=workflows&amp;extension=com_example.item"
        />
    </menuitem>
</menu>

Такой пресет даст одну группу с пунктами компонента, категорий, полей и процессов (workflow). Но Joomla всё равно применит свои фильтры. Если компонент com_fields выключен, пункт полей будет удалён. Если у com_example выключены пользовательские поля, модуль меню удалит пункты com_fields по проверке custom_fields_enable. Если workflow для компонента не включён или пользователь не имеет core.manage.workflow, пункт com_workflow тоже будет удалён.

Логику можно посмотреть в хелпере CssMenu::preprocess() (файл administrator/modules/mod_menu/src/Menu/CssMenu.php): для com_fields проверяется параметр custom_fields_enable, для com_workflow - параметр workflow_enabled и право из ACL core.manage.workflow, а затем для обычных пунктов проверяются права доступа core.manage или core.create в зависимости от scope (default, help. edit, massmail). 

Ключевые атрибуты preset

Атрибут type="heading" создаёт заголовок группы. Такой пункт сам не ведёт на страницу, потому что метод хелпера MenusHelper::parseXmlNode() ставит link = '#' для типов heading и container.

Атрибут type="container" создаёт контейнер автоматических пунктов. В mod_menu и mod_submenu контейнер заполняется компонентами ис помощью метода хелпера MenusHelper::getMenuItems('main', false, $exclude). Параметр hideitems можно передать через <params>, чтобы скрыть часть компонентов. При импорте preset-файла в базу метод installPresetItems() умеет переводить значения hideitems из element компонента в ID пунктов меню.

Атрибуты class и icon влияют на иконку, но разные модули читают их по-разному. Для основного бокового меню важнее указать class="class:file-alt". Для модуля подменю дополнительно важен icon="file-alt". Подробно это разобрано ниже, потому что единого поля “иконка для всех случаев” в preset нет.

Атрибуты quicktask, quicktask-title, quicktask-icon, quicktask-permission добавляют маленькое быстрое действие рядом с пунктом меню в панели администратора Joomla. В presets ядра это обычно ссылка на создание записи: новый материал, новая категория, новый пункт меню, новый пользователь. У одного пункта может быть только одно штатное быстрое действие (quicktask), потому что шаблоны читают один набор params: menu-quicktask, menu-quicktask-title, menu-quicktask-icon, menu-quicktask-permission.

Атрибут dashboard добавляет ссылку на дашборд компонента. Шаблон модуля меню панели администратора Joomla строит ссылку вида index.php?option=com_cpanel&view=cpanel&dashboard=... .

У компонентов ядра Joomla это связано с <dashboards> в манифесте. Например, com_content объявляет:

<dashboards>
    <dashboard title="COM_CONTENT_DASHBOARD_TITLE" icon="icon-file-alt">content</dashboard>
</dashboards>

Атрибут ajax-badge задаёт URL для динамического информационного баджика. Ядро Joomla использует это в administrator/components/com_menus/presets/system.xml, например для показа обновлений, check-in (состояния блокировки пользователем при редактировании) и сообщений после установки.

joomla admin menu preset ajax badge

Атрибут scope в пресете влияет на фильтрацию. В XSD допустимы значения default, edit, help, massmail. В модуле меню при scope="edit" скрывается, если у модуля выключен параметр иконки "Создать" (shownew), а проверка прав использует core.create вместо core.manage. Значение scope="help" зависит от параметра Пункт меню "Помощь" (showhelp), значение scope="massmail" зависит от настроек почты.

Атрибут permission в XML попадает в параметр Права доступа. В модуле подменю этот параметр явно проверяется как строка action;asset. В модуле же меню в найденном коде отдельной проверки permission нет: там основная проверка строится по element, scope и правам core.manage или core.create.

Иконки и вложенность пунктов меню панели администратора Joomla при выводе

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

В preset есть несколько разных механизмов, которые визуально выглядят как “иконка”. Они не равнозначны.

Первый механизм - class у пункта меню. Модуль меню панели администратора Joomla вызывает метод CssMenu::getIconClass($node). Если значение начинается с class:, префикс отрезается и строится CSS-класс icon-... icon-fw:

<menuitem
    title="COM_EXAMPLE_MENUS"
    type="heading"
    class="class:puzzle-piece"
/>

Для этого примера в боковом меню получится класс icon-puzzle-piece icon-fw. Но есть ограничение: стандартный layout mod_menu выводит эту иконку только для первого уровня дерева:

$iconClass = ($icon != '' && $current->level == 1) ? '<span class="' . $icon . '" aria-hidden="true"></span>' : '';

Поэтому class="class:menu" у вложенного пункта полезен для данных и импорта, но в стандартном боковом меню админки Joomla он не даст отдельную иконку на втором, третьем или четвёртом уровне. Для первого уровня это основной штатный способ.

Если атрибут class не начинается со слова class:, метод getIconClass() воспринимает значение как старый путь или имя файла: берёт basename, отрезает расширение и всё равно строит CSS-класс icon-... icon-fw. То есть это не вывод картинки как <img>, а ещё один путь к CSS-иконке. При импорте preset в реальное меню метод installPresetItems() сохраняет $item->class в колонку img; потом метод getMenuItems() возвращает это как img и class. Поэтому для импортированного в базу данных меню модуль подменю уже может увидеть img и вывести class:..., image:... или прямой путь к картинке. Для живого XML preset в модуле подменю надёжнее использовать именно атрибут icon.

Второй механизм - icon у пункта. Метод хелпера MenusHelper::parseXmlNode() кладёт его в $item->icon. В mod_submenu этот атрибут используется для заголовка карточки, если у пункта нет img:

<menuitem
    title="COM_EXAMPLE_MENUS"
    type="heading"
    icon="puzzle-piece"
    class="class:puzzle-piece"
/>

В модуле меню обычное icon="puzzle-piece" не превращается в <span class="icon-puzzle-piece">. Там поле icon используется только в специальных случаях: class:icon-home для главной страницы и image:... как badge-текст рядом с пунктом. Поэтому для универсального top-level пункта компонента обычно стоит указывать оба атрибута: class="class:puzzle-piece" для модуля меню и icon="puzzle-piece" для модуля подменю.

Третий механизм - параметры menu_icon, menu_image, menu_image_css и menu_text. Их читают layout-файлы модулей, а не parseXmlNode() как отдельные XML-атрибуты. В модуле меню админки Joomla menu_icon может заменить отсутствующую иконку пункта меню верхнего уровня через готовый CSS-класс, а menu_image может вывести изображение рядом с текстом. В модуле подменю menu_image, menu_image_css и menu_text управляют изображением и текстом внутри элемента списка. Это полезнее для пунктов, импортированных в реальное меню #__menu, поскольку параметрами пункта меню можно управлять через форму создания/редактирования пункта меню. Для простого preset компонента обычно достаточно class и icon.

Четвёртый механизм - ajax-badge. Это не иконка пункта, а динамический индикатор. Атрибут задаёт URL, а layout выводит фиксированный spinner:

<menuitem
    title="COM_EXAMPLE_MENUS_QUEUE"
    type="component"
    element="com_example"
    link="index.php?option=com_example&amp;view=queue"
    ajax-badge="index.php?option=com_example&amp;task=queue.getMenuBadgeData&amp;format=json"
/>

HTML-иконка здесь не выбирается из preset: используется icon-spin icon-spinner, а данные для текста в бадже загружаются с указанного URL.

Пятый механизм - dashboard. Он добавляет маленькую ссылку на дашборд рядом с пунктом. Иконка тоже не выбирается в preset: оба стандартных layout используют SVG media/templates/administrator/atum/images/icons/dashboard.svg.

Формального списка допустимых имён в коде Joomla 6 нет. Layout берёт строку и собирает CSS-класс вида icon-.... Работоспособность имени зависит от набора CSS-иконок, доступного в админ-шаблоне. В presets ядра Joomla 6.1 встречаются такие значения class="class:...": component, cubes, file-alt, home, image, info-circle, list, menu, privacy, puzzle-piece, userlogs, users, wrench. В icon="..." встречаются bullhorn, cloud-download-alt и т.д., а в SQL-пресетах ещё возможна подстановка вроде icon="{sql:icon}".

Вложенность пунктов меню в панели администратора Joomla

С вложенностью ситуация такая: в XML-схеме (файл administrator/components/com_menus/presets/menu.xsd) menuitemType содержит вложенный menuitem с maxOccurs="unbounded". Метод MenusHelper::loadXml() обходит дочерние узлы рекурсивно, класс \Joomla\CMS\Menu\AdministratorMenuItem хранит обычное дерево, а метод installPresetItems() при импорте тоже обрабатывает дочерние пункты рекурсивно. То есть на уровне схемы данных preset может иметь 3-4 уровня и даже глубже:

<menuitem title="COM_EXAMPLE_MENUS" type="heading" class="class:puzzle-piece" icon="puzzle-piece">
    <menuitem title="COM_EXAMPLE_MENUS_CONTENT" type="heading">
        <menuitem title="COM_EXAMPLE_MENUS_REPORTS" type="heading">
            <menuitem
                title="COM_EXAMPLE_MENUS_REPORTS_DAILY"
                type="component"
                element="com_example"
                link="index.php?option=com_example&amp;view=reports"
            />
        </menuitem>
    </menuitem>
</menuitem>

Модуль меню панели администратора Joomla тоже рекурсивен: макет default_submenu.php вызывает метод renderSubmenu(__FILE__, $current) для каждого пункта с дочерними пунктами и добавляет классы уровня вроде item-level-3 и collapse-level-3. Жёсткого PHP-лимита глубины там нет, поэтому 3-4 уровня в боковом меню технически возможны.

Но для модуля субменю (mod_submenu) стандартный лейаут другой. Он берёт детей корня как карточки и затем выводит только непосредственных детей каждой карточки - первый уровень вложенности. Дальше внутрь - на уровень "внуков" - он не уходит. Поэтому глубокое дерево в данных сохранится, но в стандартном макете mod_submenu третий и четвёртый уровень не будут показаны без собственного переопределения макетов модуля в шаблон админки или отдельной логики вывода.

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

Как создать dashboard-меню компонента Joomla

Preset особенно полезен для дашборда компонента. В Joomla есть helper-код в установочных скриптах: Joomla\CMS\Installer\InstallerScript::addDashboardMenu() и protected-вариант в InstallerScriptTrait::addDashboardMenuModule().

Оба метода создают административный модуль mod_submenu в позиции 'position' => 'cpanel-' . $dashboard и передают ему параметры:

'params' => [
    'menutype' => '*',
    'preset'   => $preset,
    'style'    => 'System-none',
]

То есть dashboard-меню компонента - это не отдельный тип меню в базе, а модуль mod_submenu, который берёт пункты из preset. Если ваш компонент объявляет dashboard example, а preset называется example, установочный скрипт может создать модуль для позиции cpanel-example, и на dashboard появится блок быстрых ссылок компонента.

Использовать в ядре Joomla 6 эти helper-методы можно следующим образом:

public function postflight($type, $parent)
{
    if ($type === 'install' || $type === 'update') {
        $this->addDashboardMenu('example', 'example');
    }
}

Этот пример использует публичный метод InstallerScript::addDashboardMenu(). Если ваш установочный скрипт построен с использованием InstallerScriptTrait, там метод называется addDashboardMenuModule() и имеет область видимости protected.

Динамические пункты через SQL в preset

Механизм preset.xml умеет повторять вложенные пункты по результатам SQL-запроса. Это уже не базовый сценарий меню компонента, а способ собрать дерево из данных базы. Реальный пример есть в ядре Joomla administrator/components/com_menus/presets/default.xml: Joomla строит список site-меню и administrator-меню из таблицы #__menu_types.

Фрагмент из пресета ядра:

<menuitem
    type="separator"
    title="JADMINISTRATOR"
    hidden="false"
    sql_select="title, menutype"
    sql_from="#__menu_types"
    sql_where="client_id = 1"
    sql_order="ordering ASC"
    >
    <menuitem
        title="{sql:title}"
        type="component"
        element="com_menus"
        link="index.php?option=com_menus&amp;view=items&amp;menutype={sql:menutype}"
        class="class:menu"
        quicktask="index.php?option=com_menus&amp;task=item.add&amp;client_id=1&amp;menutype={sql:menutype}"
    />
</menuitem>

Метод MenusHelper::loadXml() требует sql_select и sql_from, а остальные SQL-атрибуты добавляет при наличии: sql_where, sql_order, sql_group, sql_leftjoin, sql_innerjoin. Результаты подставляются в атрибуты вложенных пунктов через {sql:columnName}.

Здесь есть важные ограничения из кода. SQL собирается из XML-атрибутов напрямую, без bind-параметров, поэтому туда нельзя помещать пользовательский ввод. Подстановка {sql:...} выполняется не для всех возможных атрибутов, а только для title, element, link, class, icon и параметра menu-quicktask. Например, quicktask-title в цикле замены не обрабатывается.

Как зарегистрировать preset плагином

Иногда XML-файл лежит не в компоненте и не в шаблоне. Например, отдельный системный плагин должен добавить набор ссылок в список presets или изменить уже существующее меню (например, вытащить меню компонента в верхний уровень меню из контейнера "Компоненты"). Для этого в MenusHelper есть публичный метод addPreset($name, $title, $path, $replace = true).  Он принимает уникальное имя preset, переводимый заголовок, путь к XML-файлу и флаг замены существующего preset. Метод добавит preset только если файл существует.

А вот пример системного плагина Joomla 6, который добавляет preset в меню

namespace Joomla\Plugin\System\Examplemenu\Extension;

use Joomla\CMS\Event\Application\BeforeExecuteEvent;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Component\Menus\Administrator\Helper\MenusHelper;
use Joomla\Event\SubscriberInterface;

\defined('_JEXEC') or die;

final class Examplemenu extends CMSPlugin implements SubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            'onBeforeExecute' => 'onBeforeExecute',
        ];
    }

    public function onBeforeExecute(BeforeExecuteEvent $event): void
    {
        $app = $event->getApplication();

        if (!$app->isClient('administrator')) {
            return;
        }

        MenusHelper::addPreset(
            'example-tools',
            'PLG_SYSTEM_EXAMPLEMENU_PRESET_TOOLS',
            JPATH_PLUGINS . '/system/examplemenu/presets/tools.xml',
            false
        );
    }
}

Событие onBeforeExecute выбрано потому, что CMSApplication::execute() до этого события импортирует плагины групп behaviour и system. В обычной странице  админки это происходит до вывода com_menus, mod_menu и mod_submenu, поэтому системный плагин успеет добавить preset в MenusHelper::getPresets().

А это совершенно обычный наистандартнейший services/provider.php для такого плагина:

\defined('_JEXEC') or die;

use Joomla\CMS\Extension\PluginInterface;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Plugin\System\Examplemenu\Extension\Examplemenu;

return new class () implements ServiceProviderInterface {
    public function register(Container $container): void
    {
        $container->set(
            PluginInterface::class,
            function (Container $container) {
                $plugin = new Examplemenu(
                    (array) PluginHelper::getPlugin('system', 'examplemenu')
                );

                $plugin->setApplication(Factory::getApplication());

                return $plugin;
            }
        );
    }
};

Такой способ даёт расширению Joomla возможность добавить preset без компонента. Но он не меняет уже собранный статический список, если MenusHelper::getPresets() был вызван раньше. В стандартном жизненном цикле administrator это обычно решается ранним событием, но в тестах, CLI или нестандартном запуске фреймворка Joomla из стороннего файла порядок нужно контролировать.

Как изменить дерево меню панели администратора Joomla плагином

Второй способ - не регистрировать preset, а менять уже загруженные пункты перед выводом или импортом. Для этого есть:

  • событие - onPreprocessMenuItems
  • Класс события - Joomla\CMS\Event\Menu\PreprocessMenuItemsEvent

Его вызывают три места в Joomla 6.1:

  • administrator/modules/mod_menu/src/Menu/CssMenu.php с context com_menus.administrator.module;
  • administrator/modules/mod_submenu/src/Menu/Menu.php с context administrator.module.mod_submenu;
  • administrator/components/com_menus/src/Helper/MenusHelper.php при импорте preset с context com_menus.administrator.import.

Событие содержит context, массив пунктов subject, параметры модуля params и флаг enabled. В Joomla 6.1 конструктор ещё сохраняет subject по ссылке для обратной совместимости, но в комментарии события указано, что передача по ссылке deprecated и будет удалена в Joomla 7. Поэтому для нового кода лучше возвращать изменённый массив через метод $event->updateItems().

Вот пример кода плагина Joomla с использованием события onPreprocessMenuItems:

namespace Joomla\Plugin\System\Examplemenu\Extension;

use Joomla\CMS\Event\Menu\PreprocessMenuItemsEvent;
use Joomla\CMS\Menu\AdministratorMenuItem;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Event\SubscriberInterface;
use Joomla\Registry\Registry;

\defined('_JEXEC') or die;

final class Examplemenu extends CMSPlugin implements SubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            'onPreprocessMenuItems' => 'onPreprocessMenuItems',
        ];
    }

    public function onPreprocessMenuItems(PreprocessMenuItemsEvent $event): void
    {
        if ($event->getContext() !== 'com_menus.administrator.module') {
            return;
        }

        $items = $event->getItems();

        $item = new AdministratorMenuItem([
            'type'    => 'component',
            'title'   => 'COM_EXAMPLE_MENUS_REPORTS',
            'link'    => 'index.php?option=com_example&view=reports',
            'element' => 'com_example',
            'class'   => 'class:chart-line',
            'scope'   => 'default',
        ]);

        $item->setParams(new Registry([
            'menu-quicktask'       => 'index.php?option=com_example&task=report.add',
            'menu-quicktask-title' => 'COM_EXAMPLE_MENUS_NEW_REPORT',
            'menu-quicktask-icon'  => 'plus',
        ]));

        $items[] = $item;

        $event->updateItems($items);
    }
}

Такой плагин получает не весь корень меню  сразу, а текущий уровень, который сейчас обрабатывает CssMenu::preprocess() или Menu::preprocess(). Комментарий в обоих классах прямо предупреждает: плагин может пройти по всему дереву, но новые элементы будут обработаны этим методом только если их родители ещё не были обработаны. Поэтому надёжнее добавлять пункты на том уровне, который сейчас пришёл в событии, или добавлять дочерние пункты до рекурсивной обработки родителя.

Если нужно изменить именно импорт preset в базу, проверяйте context com_menus.administrator.import. Тогда добавленные пункты будут сохранены как записи #__menu, потому что событие вызывается в MenusHelper::installPresetItems() перед циклом сохранения записей меню в базе данных.

Что даёт preset.xml

Preset даёт компоненту декларативное меню. Это удобно, когда набор ссылок известен заранее и зависит в основном от включённых компонентов, прав пользователя и параметров Joomla.

Через preset можно:

  • собрать собственный набор ссылок для компонента;
  • сгруппировать пункты через heading;
  • добавить автоматический контейнер компонентов через container;
  • связать пункт с dashboard через dashboard;
  • добавить quicktask для создания записи;
  • добавить ajax badge;
  • скрывать help/edit/massmail пункты через scope и параметры модуля;
  • создать динамические пункты через SQL-итератор;
  • импортировать готовое дерево в пользовательское администраторское меню;
  • использовать тот же XML для mod_menu, mod_submenu и импорта в com_menus.

Главная сила preset - воспроизводимость. Компонент поставляет меню как часть своего пакета, и Joomla сама подхватывает его из administrator/components/com_example/presets. Администратор может выбрать этот preset при создании администраторского меню или разработчик может подключить его к dashboard-модулю.

Что даёт плагин

Плагин нужен там, где XML недостаточно. Он может учитывать состояние сайта, данные компонента, включённые интеграции, параметры плагина, роль пользователя или наличие внешнего сервиса.

Через плагин можно:

  • зарегистрировать preset, который лежит вне компонента;
  • добавить пункты в меню без изменения XML-файла компонента;
  • убрать пункт из чужого preset;
  • добавить дочерние пункты к существующему заголовку;
  • менять ajax-badge, dashboard, quicktask или params перед выводом;
  • по-разному вести себя для mod_menu, mod_submenu и импорта preset в базу;
  • добавлять пункты только в живой интерфейс, не сохраняя их в #__menu.

Но у плагина выше риск побочных эффектов. Он работает на общем событии onPreprocessMenuItems, поэтому должен обязательно проверять getContext(), типы объектов и нужный компонент. Иначе можно случайно изменить не только меню своего компонента, но и системное меню, подменю, дашборды или импортируемое администраторское меню.

Если у вас свой компонент Joomla

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

В манифесте компонента оставьте базовый <administration><menu>, чтобы Joomla создала главный пункт в main и компонент появился в стандартном контейнере "Компоненты".

В administrator/components/com_example/presets/example.xml опишите расширенное меню компонента: основные списки, категории, поля, workflow, быстрые действия и dashboard-ссылку. Добавьте папку presets в манифест, а языковые ключи preset - в com_example.sys.ini.

Если у компонента есть dashboard, объявите его в манифесте через <dashboards> и создайте модуль mod_submenu с menutype = * и preset = example. Для установочного скрипта можно использовать метод InstallerScript::addDashboardMenu() или InstallerScriptTrait::addDashboardMenuModule() в зависимости от структуры вашего установочного скрипта.

Плагин используйте только для динамики: зарегистрировать внешний preset, добавить пункт при наличии интеграции, скрыть ссылку по сложному условию или модифицировать дерево перед импортом. Для статического меню компонента плагин избыточен: preset проще, прозрачнее и лучше переносится между сайтами.

Об авторе

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

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

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

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

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

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

106 Всего расширений
12 Категорий
540 Выпущено версий
734790 Всего скачиваний