С момента выхода Joomla 4 прошло уже почти 2 года, поэтому слово "новой" будем понимать в контексте сравнения с Joomla 3. В Joomla 4 "под капотом" произошло немало изменений. Кодовая база движка постепенно отбрасывает legacy (старый код), встретившись с которым мы могли бы как на машине времени вернуться в середину 2000-х или начало 2010-х ????
Неизбежно, внедрение стандартов, новых технологий приводит к тому, что времена "Дикого Запада" уходят и нужно приспосабливать свои расширения к изменениям в структуре компонентов, модулей и плагинов.
В данной статье пойдёт речь о том, как создать плагин (или обновить старый) для Joomla 4 с новой структурой файлов и классов.
Отступление
Я предполагаю, что часть читателей имеет опыт работы с Joomla, но не имеет опыта создания плагинов, поэтому постараюсь описать создание плагина как можно подробнее. Статья имеет сугубо прикладной характер, без погружения в теорию ООП и его реализацию в Joomla. Основная цель - подсказать что "делать руками", когда поставлена определённая задача.
Также я предполагаю, что часть разработчиков могут встретиться с Joomla впервые. В терминологии Joomla "плагин" - это расширение, которое предоставляет функции, которые связаны с событиями (Event Dispatching). Когда происходит вызов конкретного события, то происходит последовательное выполнение всех функций подключаемых плагинов, связанных с этим событием. События могут вызывать как ядро Joomla, так и компоненты, да и в принципе любые расширения Joomla. Для сравнения, в терминологии WordPress "плагин" - это то, что в Joomla называется "компонент".
Плагины могут выполнять самый разный функционал в зависимости от времени и места вызываемого события, по которому они срабатывают: можно добавить кнопку like к материалу, а можно отправить данные заказа по API в CRM систему, "прочесать" весь готовый HTML-код страницы регуляркой или добавить новые команды в Joomla CLI интерфейс.
Поскольку в Joomla начиная с версий 3.2 начинают вводится namespaces
, а в Joomla 4 добавляется паттерн Service locator (подробнее о DI и Service locator в Joomla) - в примерах кода неизбежно будут встречаться специфичные для конкретного расширения имена классов, названия файлов. При создании своего плагина, естественно, нужно изменить их на свои.
Файловая структура плагина Joomla 3
Для создания плагина в Joomla 3 было необходимо минимум 2 файла:
xml-манифест плагина - описание плагина для установщика расширений Joomla (системное имя, дата, версия, сайт разработчика и т.д.), параметры конфигурации плагина, сервер обновлений и т.д.
файл класса плагина - "точка входа" в плагин. С этого файла начиналась работа Вашего кода.
Плагины, которые работают с HTML-кодом, могут иметь собственные макеты вывода. Такие макеты могут располагаться в папках tmpl
и/или layouts
, в зависимости от назначения этого макета (макет таба в табсете, макет для рендера изображения, макет ссылки, кнопки и любой другой запчасти - чаще всего layout
). Также на это влияет выбранный разработчиком метод работы с HTML в коде - использует ли он классы для работы макетами Joomla\CMS\Layout\FileLayout
(ex. JFileLayout) или Joomla\CMS\Layout\LayoutHelper
(ex.JLayoutHelper). Порой плагин и просто echo $any_text;
делает. Тогда у него может не быть макета вывода.
Некоторые плагины по функционалу приближаются к компонентам. В них сложнее файловая структура, они могут нуждаться в сторонних библиотеках для своей работы. Немало разработчиков, в попытке создать standalone-решение помещали всё необходимое в директорию с плагином. Для таких случаев в Joomla есть отдельный тип расширения - пакет (package), но это тема для отдельной статьи.
Файловая структура плагина в Joomla 4
Общие положения, как для модулей так и для плагинов Joomla 4: все необходимые для работы логики плагина файла мы помещаем в папку src
- файл плагина, дополнительные библиотеки, файлы классов полей и т.д.. Макеты по прежнему находятся в корне плагина в tmpl
и layouts
.
Файл XML-манифеста плагина
Этот файл содержит описание плагина для установщика расширений Joomla (системное имя, группу плагина, дата создания, версия, сайт разработчика и т.д.), параметры конфигурации, сервер обновлений, а также задаёт Namespace плагина и директории для автозагрузки классов. Регистрация Namespace
плагина в реестре происходит при установке расширения. На данный момент (Joomla 4.3.1) список классов для автозагрузки находится в файле administrator/cache/autoload_psr4.php
. Он обновляется после каждой установки или обновления расширения.
На заметку: Список соответствий названий старых классов Joomla и новых (алиасы) находится в libraries/classmap.php
.
Обратите внимание, что namespace
плагина должен содержать в себе группу плагина: Joomla\Plugin\System\Yourclass, Joomla\Plugin\Content\Yourclass и так далее.
Также стоит иметь в виду, что перевод расширений ядра Joomla на новую структуру на момент написания статьи ещё идёт и ожидается, что будет полностью завершён в Joomla 4.4. Например, для плагинов кнопки редактора (группа editors-xtd
) переход на новую структуру до версии Joomla 4.4 не имеет смысла, так как ядро 4.0-4.3 ещё не поддерживает этот namespace
.
Шаблон xml-манифеста для плагина Joomla 4.
<?xml version="1.0"?>
<extension type="plugin" method="upgrade" group="system">
<name>PLG_WT_AMOCRM_RADICALFORM</name>
<author>Sergey Tolkachyov</author>
<authorEmail>info@web-tolk.ru</authorEmail>
<authorUrl>https://web-tolk.ru/</authorUrl>
<creationDate>06/12/2022</creationDate>
<copyright>Sergey Tolkachyov</copyright>
<license>GNU General Public License v3.0</license>
<version>1.0.1</version>
<description>PLG_WT_AMOCRM_RADICALFORM_DESC</description>
<namespace path="src">Joomla\Plugin\System\Wt_amocrm_radicalform</namespace>
<media folder="media" destination="plg_system_wt_amocrm_radicalform">
<folder>js</folder>
</media>
<files>
<folder plugin="wt_amocrm_radicalform">src</folder>
<folder>services</folder>
<filename>wt_amocrm_radicalform.xml</filename>
</files>
<languages folder="language" client="administrator">
<language tag="en-GB">en-GB/plg_system_wt_amocrm_radicalform.ini</language>
<language tag="ru-RU">ru-RU/plg_system_wt_amocrm_radicalform.ini</language>
<language tag="en-GB">en-GB/plg_system_wt_amocrm_radicalform.sys.ini</language>
<language tag="ru-RU">ru-RU/plg_system_wt_amocrm_radicalform.sys.ini</language>
</languages>
<config>
<fields name="params">
<fieldset name="basic">
<!-- ЗДЕСЬ ПАРАМЕТРЫ ВАШЕГО ПЛАГИНА -->
<!-- ТИПЫ ПОЛЕЙ JOOMLA ЗДЕСЬ -->
<!-- https://docs.joomla.org/Standard_form_field_types -->
</fieldset>
</fields>
</config>
</extension>
Также обратите внимание, что для корректной установки и работы плагина нужно указывать атрибут plugin="wt_amocrm_radicalform"
в xml-манифесте. Если в Joomla 3 этот атрибут указывался для файла "точки входа" (<filename plugin="wt_amocrm_radicalform">wt_amocrm_radicalform.php</filename>
), то сейчас он указывается для папки src
плагина - <folder plugin="wt_amocrm_radicalform">src</folder>
.
Языковые файлы (файлы локализации)
Ещё одно нововведение связано с языковыми файлами: теперь в именах файлов не обязательно дублировать префикс языка - "ru-RU.plg_system_wt_amocrm_radicalform.ini". Достаточно того, что файл лежит в папке "ru-RU". Однако, по-прежнему в названии файла локализации должен быть указан тип расширения plg
, группа плагина (в данном случае system
) и системное имя - из атрибута plugin="wt_amocrm_radicalform"
- в сумме получается plg_system_wt_amocrm_radicalform
.
Также помним, что файлы с языковыми константами (файлы локализации) должны отправляться в папку administrator/language
, так как один и тот же плагин может работать и "внутри" и "снаружи". Для этого указываем атрибут client="administrator"
:
<languages folder="language" client="administrator">
Файл services/provider.php
Минимально необходимый набор для плагина увеличился на 1 файл. Аналогично, как и для модулей (статья Создание модулей с учётом новой структуры Joomla 4), плагину нужен файл сервис-провайдер Вашего плагина. Файл provider.php
позволяет регистрировать плагин в DI-контейнере Joomla и даёт возможность обращаться к методам плагина извне с помощью MVCFactory
.
<?php
/**
* @package WT AmoCRM Radical Form
* @copyright Copyright Sergey Tolkachyov
* @license GNU General Public License version 3, or later
*/
defined('_JEXEC') || die;
use Joomla\CMS\Extension\PluginInterface;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Joomla\Plugin\System\Wt_amocrm_radicalform\Extension\Wt_amocrm_radicalform;
return new class implements ServiceProviderInterface {
/**
* Registers the service provider with a DI container.
*
* @param Container $container The DI container.
*
* @return void
*
* @since 4.0.0
*/
public function register(Container $container)
{
$container->set(
PluginInterface::class,
function (Container $container) {
$subject = $container->get(DispatcherInterface::class);
$config = (array) PluginHelper::getPlugin('system', 'wt_amocrm_radicalform');
return new Wt_amocrm_radicalform($subject, $config);
}
);
}
};
Поскольку плагины по-прежнему расширяют класс CMSPlugin
(ex. JPlugin
), Вы можете установить объект приложения (Factory::getApplication()
) плагину прямо в сервис-провайдере. И затем в самом плагине Вы сможете обращаться к объекту приложения с помощью $app = $this->getApplication();
Аналогично для удобства можно один раз указать объект для работы с базой данных: $plugin->set Database($container->get(DatabaseInterface::class));
. И получить его в плагине $db = $this->getDatabase();
.
<?php
/**
* Пример взят из книги греческого разработчика
* Николаса Дионисопулоса, Akeeba Ltd.
* @link https://www.dionysopoulos.me/book/plg.html
*/
defined('_JEXEC') || 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\Event\DispatcherInterface;
use Joomla\Database\DatabaseInterface;
use Acme\Plugin\System\Example\Extension\Example;
return new class implements ServiceProviderInterface {
public function register(Container $container)
{
$container->set(
PluginInterface::class,
function (Container $container)
{
$config = (array)PluginHelper::getPlugin('system', 'example');
$subject = $container->get(DispatcherInterface::class);
$app = Factory::getApplication();
/** @var \Joomla\CMS\Plugin\CMSPlugin $plugin */
$plugin = new Example($subject, $config);
$plugin->setApplication($app);
$plugin->setDatabase($container->get(DatabaseInterface::class));
return $plugin;
}
);
}
};
Также обратите внимание на изменения в Joomla 4.4, в них классы модулей и плагинов становятся final
, добавляется использование trait
. В целом на работу Вашего кода это не должно влиять.
Файл класса плагина
Это файл, в котором содержится основной рабочий код Вашего плагина. Он должен находится в папке src/Extension
. Регистр названия имеет значение: Extension
и extension
- 2 разных папки.
Если Вы адаптируете старый плагин Joomla для работы в Joomla 4, то нужно сделать:
перенести php-файл плагина в папку
src/Extension
изменить имя файла так, чтобы оно начиналось с заглавной буквы -
Wt_amocrm_radicalform
,изменить имя класса с
PlgSystemWt_amocrm_radicalform
наWt_amocrm_radicalform
. Вся строка будет видаclass Wt_amocrm_radicalform extends CMSPlugin
в самом начале файла указать его
namespace
-namespace Joomla\Plugin\System\Wt_amocrm_radicalform\Extension;
проверить, чтобы все названия событий (функций в плагине) начинались с
"on"
-onBeforeCompileHead
,onAfterRender
и т.д.
Пример кода класса плагина Joomla 4
Если Ваш плагин был написан "по технологиям" Joomla 3.x, с использованием namespaces
, то в большинстве случаев он будет работать на Joomla 4. Если же плагин использовал код, работавший ещё в 1.5-1.6 и бывший устаревшим уже в Joomla 3, то, скорее всего, плагин нужно будет пересобрать.
<?php
/**
* @package WT Amocrm - Radical From
* @version 1.0.0
* @Author Sergey Tolkachyov, https://web-tolk.ru
* @copyright Copyright (C) 2022 Sergey Tolkachyov
* @license GNU/GPL3
* @since 1.0
*/
// No direct access
namespace Joomla\Plugin\System\Wt_amocrm_radicalform\Extension;
defined('_JEXEC') or die;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Factory;
use Joomla\CMS\Date\Date;
use Webtolk\Amocrm\Amocrm;
class Wt_amocrm_radicalform extends CMSPlugin
{
/**
* Добавляем js-скрпиты на HTML-фронт
*
* @throws \Exception
* @since 1.0.0
*/
function onAfterDispatch()
{
// We are not work in Joomla API or CLI ar Admin area
if (!Factory::getApplication()->isClient('site')) return;
$doc = Factory::getApplication()->getDocument();
// We are work only in HTML, not JSON, RSS etc.
if (!($doc instanceof \Joomla\CMS\Document\HtmlDocument))
{
return;
}
$wa = $doc->getWebAssetManager();
// Show plugin version in browser console from js-script for UTM
$wt_amocrm_radicalform_plugin_info = simplexml_load_file(JPATH_SITE . "/plugins/system/wt_amocrm_radicalform/wt_amocrm_radicalform.xml");
$doc->addScriptOptions('plg_system_wt_amocrm_radicalform_version', (string) $wt_amocrm_radicalform_plugin_info->version);
$wa->registerAndUseScript('plg_system_wt_amocrm_radicalform.wt_amocrm_radicalform_utm', 'plg_system_wt_amocrm_radicalform/wt_amocrm_radicalform_utm.js', array('version' => 'auto', 'relative' => true));
}
}
Новая система событий для плагинов в Joomla 4
Кратко: в Joomla 4 реализована новая система событий, основанная на SubscriberInterface
.Класс плагина реализует \Joomla\Event\SubscriberInterface
, Вы устанавливаете свойство protected $this->allowLegacyListeners = false;
. Это изменяет способ, которым Joomla регистрирует обработчики событий. В этом режиме Joomla не будет использовать рефлексию и искать публичные методы, чьи имена начинаются с on
. Вместо этого она вызовет публичный статический метод getSubscribedEvents
, который определен в интерфейсе и должен быть реализован в классе вашего плагина. Этот класс вернёт массив с маппингом событий и методов Вашего плагина. При этом внутри Вашего плагина методы уже не обязаны начинаться с on
и вообще могут иметь любое (в разумных пределах) название. Отсутствие необходимости проходить через рефлексию экономит много времени при загрузке каждой страницы сайта. Кроме того, события представляют собой автономные объекты, которые передаются, что снижает накладные расходы на вызов каждого обработчика событий. К ним добавляются десятки и сотни плагинов и обработчиков событий, работающих на типичном сайте Joomla, что экономит от нескольких десятков до нескольких сотен миллисекунд времени загрузки страницы. Это значительное улучшение производительности сайта.
Подробно: это потребует отдельной статьи ????
Пример кода плагина, использующего SubscriberInterface в Joomla 4.
namespace Joomla\Plugin\Quickicon\Joomlaupdate\Extension;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Event\SubscriberInterface;
use Joomla\Module\Quickicon\Administrator\Event\QuickIconsEvent;
class Joomlaupdate extends CMSPlugin implements SubscriberInterface
{
/**
* Returns an array of events this subscriber will listen to.
*
* @return array
*
* @since 4.0.0
*/
public static function getSubscribedEvents(): array
{
return [
'onGetIcons' => 'getCoreUpdateNotification',
];
}
/**
* This method is called when the Quick Icons module is constructing its set
* of icons. You can return an array which defines a single icon and it will
* be rendered right after the stock Quick Icons.
*
* @param QuickIconsEvent $event The event object
*
* @return void
*
* @since 4.0.0
*/
public function getCoreUpdateNotification(QuickIconsEvent $event)
{
}
}
Это несколько сокращённый код плагина, который добавляет иконку с уведомлениями о новых версиях Joomla, код можно посмотреть здесь: plugins/quickicon/joomlaupdate/src/Extension/Joomlaupdate.php
.
Что здесь происходит? Плагин реализует SubscriberInterface
и имеет статический публичный метод getSubscribedEvents
, который возвращает массив сопоставления событий и методов плагина. Обратите внимание, что по событию onGetIcons
будет вызван не метод плагина onGetIcons
(его там и нет вообще), а метод getCoreUpdateNotification
.
Ещё один пример кода (источник):
public static function getSubscribedEvents(): array
{
return [
'onSomething' => 'doSomething',
'onSomethingElse' => ['doSomething', \Joomla\Event\Priority::HIGH],
];
}
onSomething
- некое событие, вызывающее метод нашего плагина doSomething
. onSomethingElse
- другое событие. которое вызывает метод нашего плагина doSomething
и устанавливает исполнению этого метода приоритет.
Да-да, теперь можно влиять на порядок выполнения не только путём перетаскивания плагинов мышкой в админке, но и устанавливать приоритет выполнения прямо в коде. Подробнее читаем пока что в вышеуказанном источнике.
Небольшое "но"...
Ядро Joomla 4 (4.3.1 на момент написания статьи) до сих пор ещё содержит старые вызовы плагинов. Поэтому, перед тем, как переписать плагин на новую структуру - убедитесь, что событие плагина вызывается по-новому. Это будет выглядеть в коде примерно так:
$event = \Joomla\CMS\Event\AbstractEvent::create('onSomething', [$param1 => $value1, $param2 => $value2]);
$this->getDispatcher()->dispatch($event->getName(), $event);
$results = $event->getArgument('result', []);
Для сравнения, старый способ вызова событий выглядел примерно таким образом: $app->triggerEvent('onSomething', [$param1, $param2])
.
Самый простой способ выяснить как образом вызывается событие для плагина - выполнить поиск по текстовому содержимому движка (если Вы работаете без IDE), а затем найти вызов в файле
Для примера, я решил перевести один из своих плагинов для JoomShopping на новую структуру Joomla 4. Немного почитав код ядра и документацию стал пробовать новый метод, но на JoomShopping он не завёлся, так как JoomShopping - это не ядро Joomla, а сторонний компонент и вызовы событий в нём реализуются пока что старыми методами. В этом плане, Вы как разработчик зависите от компонента, под который Вы разрабатываете плагин. Будьте внимательны и осторожны ????.
Работа с Ajax в плагинах Joomla 4
В целом, в этом плане ничего не изменилось - среди методов плагина должен быть метод с префиксом onAjax
, например onAjaxDebug
. В случае, если Вы используете новый SubscriberInterface
, то в маппинг нужно добавить событие и корреспондирующий ему метод в плагине:
Пример ajax-запроса, реализованного нативными средствами Joomla (статья-мануал на Хабре).
Joomla.request({
url: window.location.origin + "/index.php?option=com_ajax&plugin=yourplugin&group=system&format=raw",
onSuccess: function (response, xhr){
if (response !== ""){
// Здесь делаем то, что нужно
}
}
});
}
Свои типы полей Joomla для плагина
Поля Joomla Form указываются в XML-манифесте и представляют собой удобный и быстрый конструктор интерфейса. Как и в Joomla 3, в Joomla 4, если Вам не хватает стандартных типов полей, у Вас есть возможность создавать свои типы полей. Это могут быть нестандартные выборки из базы данных, получение значений списка из сторонних сервисов по API и т.д.
Возможность создавать свои пользовательские типы полей открывает широкие возможности Joomla. Наглядный пример про Битрикс 24 приводил в статье про написание модулей по структуре Joomla 4 (хотя пример как раз из плагина... )).
Joomla 3
В Joomla 3 Вам надо было указать свой тип поля и назначить атрибут addfieldpath
родительскому <fieldset>
или напрямую <field>
. Например
<field addfieldpath="plugins/system/wt_amocrm_radicalform/fields" type="plugininfo" name="plugininfo"/>
Php-файл поля находился в папке с плагином plugins/system/wt_amocrm_radicalform/fields
.
Joomla 4
В Joomla 4 атрибут addfieldpath
не работает. Вместо него используется атрибут addfieldprefix
, в котором нужно указать namespace для пользовательских полей плагина.
Поля мы складываем в src/Fields
. У файлов полей должен быть namespace namespace Joomla\Plugin\System\Wt_amocrm_radicalform\Fields
. Я использую собственный тип поля, расширяющий тип поля spacer (пробел), для вывода своего логотипа, версии плагина, ссылки на сайт и иногда дополнительной информации.
К слову сказать, классы дополнительных типов полей могут находиться и не в папке с плагином, а, например, быть "в комплекте" с библиотекой и физически находиться в папке libraries
. Но, указав нужный namespace
в атрибуте addfieldprefix
мы можем легко их использовать.
Полезные дополнения
Об использовании \Joomla\CMS\Factory
Из статьи Распространенные ошибки при написании плагинов Joomla 4
Вы должны использовать ТОЛЬКО ДВА метода \Joomla\CMS\Factory
в Joomla 4:
getContainer()
- возвращает контейнер внедрения зависимостей Joomla (DI Container, иногда сокращенно DIC).getApplication()
- возвращает текущий объект приложения Joomla, обрабатывающий запрос.
Всё. Больше ничего другого использовать не нужно! Всё остальное предоставляется либо через DI-контейнер, либо через сам объект приложения.
Чтобы получить документ приложения используйте \Joomla\CMS\Factory::getApplication()->getDocument()
.
Правильное подключение CSS и JS в Joomla 4
Статья на хабре Использование WebAssetsManager Joomla 4 и добавление собственных пресетов с помощью плагина. Мы помним, что все CSS и JS файлы должны лежать в папке
media
. Подробнее в статьях.Статья на хабре Создание модулей с учётом новой структуры Joomla 4
Замена для популярных, но устаревших методов
Многие из этих методов работали ещё со времен Joomla 1.5 (с 2008 года!).
JRequest::getUri()
заменяем на $uri =Joomla\CMS\Uri::getInstance()
и читаем документацию к нему.методы
JRequest::getCmd
и аналогичные перекочевали вJoomla\Input\Input
или (что проще)$app->getInput()
. Пока что поддерживается устаревший синтаксис$app->input
, но в Joomla 5 (выйдет осенью 2023 года) он может быть удалён (план выпуска релизов и принципы удаления устаревшего кода в Joomla).$app->isAdmin()
и$app->isSite()
стали$app->isClient('Site')
и$app->isClient('Administrator')
.Подключение к базе данных: вместо
JFactory::getDbo()
(илиJoomla\CMS\Factory::getDbo
) используем$app->getContainer()->get('DatabaseDriver')
('DatabaseDriver' регистрозависимый). Или жеuse Joomla\Database\DatabaseInterface;
в начале файла в секции сuse
. ИFactory::getContainer()->get(DatabaseInterface::class)
.Получение объекта пользователя: вместо
JFactory::getUser()
(илиJoomla\CMS\Factory::getUser()
) используем$app->getIdentity()
Получение языка: вместо
Factory::getLanguage()
используемFactory::getApplication()->getLanguage();
Список соответствий названий старых классов Joomla и новых (алиасы) находится в
libraries/classmap.php
.
Заключение
Буду рад, если статья окажется полезной для разработчиков. Также буду признателен замечаниям и исправлениям, если таковые имеются у читателей.
Полезные ресурсы
Ресурсы сообщества:
https://vc.ru/s/1146097-joomla - Сообщество Joomla на VC.
Telegram:
Вакансии и предложения работы по Joomla: фуллтайм, частичная занятость и разовые подработки. Размещение вакансий здесь.
progreccor
Очень своевременная статья. Главное все подробно описано.