Joomla 4 "под капотом" претерпела немало изменений относительно предыдущих версий. Её кодовую базу сообщество разработчиков регулярно подтягивают до современных реалий, вводя актуальные технологии в ядро CMS. Так, например, если раньше загрузка классов была вариациями на тему include, то в Joomla 4 появился лоадер, приведённый к PSR-4. Ядро CMS переводится на концепцию сервис-провайдеров, внедрены DI-контейнеры, переработанная система событий для плагинов позволила увеличить производительность при генерации страниц более чем в два раза. Эти изменения влекут за собой изменения в структуре компонентов, модулей и плагинов.
В данной статье пойдёт речь о том, как создать модуль для Joomla 4 с новой структурой файлов и классов. К слову сказать, legacy ещё работает и многие расширения, созданные по канонам Joomla 3 (а не работавшие на Joomla 3, но написанные по канонам Joomla 1.5) ещё долго будут работать на Joomla 4.
Отступление
Я предполагаю, что часть читателей имеет опыт работы с Joomla, но не имеет опыта создания модулей, поэтому постараюсь описать создание модуля как можно подробнее. Статья имеет сугубо прикладной характер, без погружения в теорию ООП и его реализацию в Joomla. Основная цель - подсказать что "делать руками", когда поставлена определённая задача.
Рассказывать о создании модуля я буду на примере своего модуля WT Yandex map items - модуля вывода материалов Joomla на Яндекс.карты по координатам из пользовательских полей, который создавался под проект на Joomla 4, поэтому названия файлов, классов и namespace будут содержать название именно этого модуля. При создании своего модуля, естественно, нужно изменить их на свои.
Файловая структура модуля Joomla 3 vs Joomla 4 и распределение функционала
Было (Joomla 3)
Для создания модуля было необходимо как минимум 3 файла:
-
mod_wtyandexmapitems.xml - описание модуля для установщика расширений Joomla (системное имя, дата, версия, сайт разработчика и т.д.), параметры конфигурации, сервер обновлений и т.д.
-
mod_wtyandexmapitems.php - "точка входа" в модуль. С этого файла начинается работа Вашего кода.
-
tmpl/default.php - макет вывода для модуля. Здесь находится HTML-вёрстка Вашего модуля. При необходимости, можно скопировать и переименовать этот файл, изменить вывод HTML по своему вкусу и выбрать в настройках свой новый макет вывода.
Этот же способ позволяет выполнять любой свой PHP-код в нужном месте и в нужное время.
Именно такую структуру мы видим в одном из простейших модулей Joomla - mod_custom - "HTML-код".
Хелпер (helper) модуля Joomla
Если наш модуль делает что-то более сложное, чем просто вывод значений из настроек модуля, например:
-
отображает список новых статей на сайте;
-
показывает карусель товаров из компонента интернет-магазина;
-
выводит популярные комментарии или фотографии из фото-галереи ;
-
подтягивает данные модуля по ajax (ajax-корзина товаров, к примеру).
то все функции, выполняющие эту работу, помещаются в хелпер модуля. В Joomla 3 он помещался в файл helper.php, находящийся рядом с основным php-файлом. Подключение хелпера было в "точке входа" с помощью JLoader::register('ModWtyandexmapitemsHelper', __DIR__ . '/helper.php');
Стало (Joomla 4)
Для создания модуля в Joomla 4 нужны следующие файлы (с меньшим количеством можно поэкспериментировать спортивного интереса ради):
Файл mod_wtyandexmapitems.xml
Этот файл содержит описание модуля для установщика расширений Joomla (системное имя, дата, версия, сайт разработчика и т.д.), параметры конфигурации, сервер обновлений, а также задаёт Namespace модуля и директории для автозагрузки классов.
<?xml version="1.0" encoding="utf-8"?>
<extension type="module" client="site" method="upgrade">
<name>MOD_WTYANDEXMAPITEMS</name>
<author>Sergey Tolkachyov</author>
<creationDate>13/09/2022</creationDate>
<copyright>(C) 2022 Sergey Tolkachyov.</copyright>
<license>GNU General Public License version 2 or later</license>
<authorEmail>info@web-tolk.ru</authorEmail>
<authorUrl>https://web-tolk.ru</authorUrl>
<version>1.0.0</version>
<description>MOD_WTYANDEXMAPITEMS_DESC</description>
<namespace path="src">Joomla\Module\Wtyandexmapitems</namespace>
<files>
<folder module="mod_wtyandexmapitems">src</folder>
<folder>language</folder>
<folder>services</folder>
<folder>tmpl</folder>
</files>
<languages>
<language tag="en-GB">language/en-GB/mod_wtyandexmapitems.ini</language>
<language tag="en-GB">language/en-GB/mod_wtyandexmapitems.sys.ini</language>
<language tag="ru-RU">language/ru-RU/mod_wtyandexmapitems.ini</language>
<language tag="ru-RU">language/ru-RU/mod_wtyandexmapitems.sys.ini</language>
</languages>
</extension>
Также обратите внимание, что для корректной установки и работы модуля нужно указывать атрибут module="mod_wtyandexmapitems"
в xml-манифесте. Если в Joomla 3 этот атрибут указывался для файла "точки входа" (<filename module="mod_wtyandexmapitems">mod_wtyandexmapitems.php</filename>
), то сейчас он указывается для папки src
модуля - <folder module="mod_wtyandexmapitems">src</folder>
.
Ещё одно нововведение связано с языковыми файлами: теперь в именах файлов не обязательно дублировать префикс языка - "ru-RU.mod_wtyandexmapitems.ini". Достаточно того, что файл лежит в папке "ru-RU".
Файл services/provider.php
Файл - сервис-провайдер Вашего модуля. Он сообщает Joomla, что Ваш модуль существует и регистрирует namespace модуля в глобальном пространстве имён.
<?php
/**
* @package WT Yandex Map items
*
* @copyright (C) 2022 Sergey Tolkachyov
* @link https://web-tolk.ru
* @license GNU General Public License version 2 or later
*/
defined('_JEXEC') or die;
use Joomla\CMS\Extension\Service\Provider\HelperFactory;
use Joomla\CMS\Extension\Service\Provider\Module;
use Joomla\CMS\Extension\Service\Provider\ModuleDispatcherFactory;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
/**
* The WT Yandex map items module service provider.
*
* @since 1.0.0
*/
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)
{
// Основной namespace модуля
$container->registerServiceProvider(new ModuleDispatcherFactory('\\Joomla\\Module\\Wtyandexmapitems'));
// Namespace модуля для хелпера
$container->registerServiceProvider(new HelperFactory('\\Joomla\\Module\\Wtyandexmapitems\\Site\\Helper'));
// Namespace модуля для своих типов полей
$container->registerServiceProvider(new HelperFactory('\\Joomla\\Module\\Wtyandexmapitems\\Site\\Fields'));
$container->registerServiceProvider(new Module);
}
};
Некоторые модули для Joomla могут быть довольно сложными, использовать дополнительные PHP-библиотеки и SDK, поэтому в папке src
модуля могут быть самые разные Namespace
, которые можно зарегистрировать в сервис-провайдере, дабы они были доступны глобально. Однако, я предпочитаю библиотеки оформлять отдельными расширениями Joomla и устанавливать их в папку libraries
в корне сайта, а обращаться к ним уже по namespace
. Это удобно для тех случаев, когда не одно Ваше расширение использует данную библиотеку, а несколько. В таком случае для библиотеки потребуется системный плагин, регистрирующий её namespace в глобальном пространстве имён.
Upd 16.02.2023г.: Для расширений типа library
также можно указывать свой namespace
в xml-манифесте. До версии Joomla 4.2.7 были проблемы с регистрацией namespace
вида VendorName\LibraryName
напрямую из xml-манифеста и системный плагин был необходим. Начиная с версии 4.2.7 этот баг был исправлен (GitHub).
Берём на заметку, что в Namespace
указывается "клиент" модуля - "Site" или "Administrator".
Файл src/Dispatcher/Dispatcher.php
Этот файл используется для того, чтобы передать данные из хелпера модуля в макет (layout).
<?php
/**
* @package WT Yandex Map items
*
* @copyright (C) 2022 Sergey Tolkachyov
* @link https://web-tolk.ru
* @license GNU General Public License version 2 or later
*/
namespace Joomla\Module\Wtyandexmapitems\Site\Dispatcher;
\defined('JPATH_PLATFORM') or die;
use Joomla\CMS\Application\CMSApplicationInterface;
use Joomla\CMS\Dispatcher\AbstractModuleDispatcher;
use Joomla\CMS\Extension\ModuleInterface;
use Joomla\Input\Input;
use Joomla\Module\Wtyandexmapitems\Site\Helper\WtyandexmapitemsHelper;
use Joomla\Registry\Registry;
/**
* Dispatcher class for mod_wtyandexmapitems
*
* @since 1.0.0
*/
class Dispatcher extends AbstractModuleDispatcher
{
/**
* The module extension. Used to fetch the module helper.
*
* @var ModuleInterface|null
* @since 1.0.0
*/
private $moduleExtension;
public function __construct(\stdClass $module, CMSApplicationInterface $app, Input $input)
{
parent::__construct($module, $app, $input);
$this->moduleExtension = $this->app->bootModule('mod_wtyandexmapitems', 'site');
}
/**
* Returns the layout data.
*
* @return array
*
* @since 1.0.0
*/
protected function getLayoutData()
{
$data = parent::getLayoutData();
// Вариант использования хелпера через Namespace
$data['placemarks'] = (new WtyandexmapitemsHelper)->getPlacemarks($data['params'], $this->getApplication());
// ИЛИ
// Вариант использования хелпера через $this->moduleExtension,
// который мы загрузили в конструкторе класса
$helper = $this->moduleExtension->getHelper('WtyandexmapitemsHelper');
// ИЛИ
// Вариант использования хелпера напрямую из этого метода,
// не загружая модуль в $this->moduleExtension.
// Тогда строка $this->moduleExtension в __construct() не нужна.
$helper = $this->app->bootModule('mod_wtyandexmapitems', 'Site')->getHelper('WtyandexmapitemsHelper');
$data['placemarks'] = $helper->getPlacemarks($data['params'], $this->getApplication());
return $data;
}
}
Особый интерес для нас представляет функция getLayoutData()
, так как именно в ней мы обращаемся к методам нашего хелпера модуля и помещаем полученные данные в массив $data
. Ключ массива $data
может быть любым и может быть не единственным. Можно провести параллель с Model в MVC, когда мы из разных мест собираем данные и передаём их для отображения.
В хелпере модуля можно собрать один результирующий массив данных и в диспетчере передать на рендер только его (обратившись только к одному методу хелпера - точке входа в хелпер). А можно обращаться к разным методам хелпера в диспетчере, присваивая данные разным элементам массива $data
. Подход к реализации Вы выбираете сами, распределяя функционал между этими двумя файлами.
Файл src/Helper/WtyandexmapitemsHelper.php
Хелпер модуля. Имя файла = имя модуля без суффикса "mod_" + Helper (с заглавной буквы).
Namespace хелпера - Joomla\Module\Wtyandexmapitems\Site\Helper
. Вместо "Site" может быть "Administrator", если у Вас модуль для панели администратора, например для дашбордов Joomla 4. Имя класса совпадает с именем файла. Внутри - нужные Вам функции.
<?php
/**
* @package WT Yandex Map items
*
* @copyright (C) 2022 Sergey Tolkachyov
* @link https://web-tolk.ru
* @license GNU General Public License version 2 or later
*/
namespace Joomla\Module\Wtyandexmapitems\Site\Helper;
use Joomla\CMS\Access\Access;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Helper\ModuleHelper;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Layout\FileLayout;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Uri\Uri;
use Joomla\Component\Content\Site\Helper\RouteHelper;
use Joomla\Component\Fields\Administrator\Helper\FieldsHelper;
use Joomla\Registry\Registry;
\defined('_JEXEC') or die;
/**
* Helper for mod_wtyandexmapitems
*
* @since 1.0
*/
class WtyandexmapitemsHelper
{
public function getPlacemarks($params, $app):array
{
/**
* Этот метод мы вызывали в файле
* src/Dispatcher/Dispatcher.php
* в строке
* $data['placemarks'] = (new WtyandexmapitemsHelper)->getPlacemarks($data['params'], $this->getApplication());
*/
}
}
В данном случае в методе getPlacemarks()
я получаю с помощью нескольких методов список материалов Joomla 4, их пользовательские поля, выбираю (сообразно настройкам модуля) поле, в котором хранятся координаты, а затем собираю массив со структурой, необходимой для Яндекс карт.
Работа с Ajax в модулях Joomla 4
Если Вашему модулю есть что отдать по ajax на фронт, то для этого нужно в хелпере модуля создать метод getAjax()
. В нашем случае на Яндекс.карты будет загружаться более 100 меток с текстами и картинками. Поэтому целесообразнее получать эти данные по ajax.
Согласно документации Joomla по использованию ajax Вы можете в запросе указывать конкретный метод хелпера. В таком случае имя метода должно заканчиваться на "Ajax": например method=mySuperAwesomeMethodToTrigger
вызовет метод mySuperAwesomeMethodToTriggerAjax
модуля.
Пример ajax-запроса, реализованного нативными средствами Joomla (статья-мануал о нативном ajax в Joomla).
Joomla.request({
url: window.location.origin + "/index.php?option=com_ajax&module=wtyandexmapitems&format=raw",
onSuccess: function (response, xhr){
if (response !== ""){
let placemarks = JSON.parse(response);
console.log(placemarks);
objectManager.add(placemarks);
myMap' . $module->id . '.geoObjects.add(objectManager);
}
}
});
}
Файл tmpl/default.php - макет вывода в Joomla
Здесь по-прежнему находится HTML-вёрстка Вашего модуля. Его по-прежнему можно скопировать в ту же папку или в папку с Вашим шаблоном (сделать переопределение), переименовать и изменить вывод HTML, не отказывая себе в самых страшных извращениях и при этом не опасаясь того, что Ваши изменения будут затёрты при обновлении движка.
В файлах макетов вывода, как правило, находится цикл вывода данных foreach.
В Joomla 3 нередко можно было встретить следующую конструкцию:
// Файл "точка входа" модуля mod_menu.php
// $list - массив с пунктами меню, которые передаются в макет вывода
$list = ModMenuHelper::getList($params);
// Файл tmpl/default.php - макет вывода модуля mod_menu
// $list - массив с пунктами меню, которые передаются в макет вывода
foreach ($list as $i => &$item){
// здесь работа по отображению HTML меню, с учетом настроек модуля и данных в массиве
}
В Joomla 4 в принципе осталось то же самое, за небольшими изменениями:
-
данные мы получаем из хелпера и передаём не в "точке входа" (которой теперь нет по определению), а в файле
src/Dispatcher/Dispatcher.php
. Те самые$data['placemarks']
в макете вывода становятся просто$placemarks
. -
"рядом" с Вашими переменными передаются следующие:
-
$module
- объект модуля. Оттуда Вы можете взять id модуля ($module->id
), заголовок модуля, его позицию и т.д. -
$app
- объект приложения. Это значит, что Вам не нужно самостоятельно вызыватьJoomla\CMS\Factory::getApplication()
. Он уже есть для Вашего удобства. -
$input
- также в макете модуля теперь сразу доступен объект Input (через него мы получаем GET, POST параметры, SERVER и т.д.), который раньше приходилось вызывать самостоятельно. -
$params
- параметры модуля. Получаем их как раньше:$params->get('param_name' , 'default_value_if_value_is_empty')
. Эти параметры мы собираем с помощью различных типов полей Joomla в xml-манифесте модуля. -
$template
- параметры настроек стиля текущего шаблона. У шаблонов Joomla есть templateDetails.xml, в которых можно задавать различные параметры шаблона: логотипы, шрифты, пользовательские скрипты в <head> и <body> и всё, что душе угодно. Теперь в модуле Вы имеете возможность без лишних шевелений получить доступ к этим параметрам. Однако, стоит помнить, что многие студийные шаблоны (JoomShaper Helix и иже с ними) не используют стандартное место хранение параметров, поэтому там может оказаться пусто.
-
Свои типы полей Joomla для модуля
Как и в Joomla 3, в Joomla 4, если Вам не хватает стандартных типов полей, у Вас есть возможность создавать свои типы полей. Это могут быть нестандартные выборки из базы данных, получение значений списка из сторонних сервисов по API и т.д.
Возможность создавать свои пользовательские типы полей открывает широкие возможности Joomla. Наглядный пример:
Joomla 3
В Joomla 3 Вам надо было указать свой тип поля и назначить атрибут addfieldpath
родительскому <fieldset>
или напрямую <field>
. Например
<field addfieldpath="modules/mod_wtyandexmapitems/fields" type="moduleinfo" name="moduleinfo"/>
Php-файл поля находится в папке с модулем modules/mod_wtyandexmapitems/fields
.
Joomla 4
В Joomla 4 атрибут addfieldpath
не работает. Вместо него используется атрибут addfieldprefix
, в котором нужно указать namespace для пользовательских полей модуля.
Поля мы складываем в src/Fields
. У файлов полей должен быть namespace namespace Joomla\Module\Wtyandexmapitems\Site\Fields
. Я использую собственный тип поля, расширяющий тип поля spacer (пробел), для вывода своего логотипа, версии модуля, ссылки на сайт и иногда дополнительной информации.
<field type="moduleinfo" addfieldprefix="Joomla\Module\Wtyandexmapitems\Site\Fields" name="moduleinfo"/>
А вот пример использования в Joomla 3. Плагин для двухсторонней интеграции Joomla с CRM Битрикс 24 в настройках показывает информацию об аккаунте, из-под которого создан вебхук на стороне Битрикс 24. Если информация отображается, значит плагин настроен верно.
А здесь в настройках плагина отображается список стадий лида (или сделки), получаемый по API из CRM Битрикс 24. Это так же реализовано с помощью пользовательских типов полей (пример из версии для Joomla 3).
Гибридный вариант модуля
Если у Вас совсем нет времени, а завести "со шморгалкой" старый модуль на Joomla 4 всё-таки надо, поддерживается (пока что) как старый, так и гибридный вариант структуры модуля.
-
Пока что можно обойтись без сервис-провайдера. Вообще. Тогда нужен файл "точки входа"
mod_wtyandexmapitems.php
и соответствующая строка в xml-манифесте модуля. -
Пока что можно обойтись без папки src. И подключать хелпер по старинке через
JLoader::register('ModWtyandexmapitemsHelper', __DIR__ . '/helper.php')
. Соответственно файл helper.php должен лежать рядом с "точкой входа" в модуль. -
Можно переместить хелпер в папку
src
, переименовать файл, назначить ему namespace (и в xml-манифесте модуля тоже) и использовать в "точке входа" простоuse Joomla\Module\Wtyandexmapitems\Site\Helper\WtyandexmapitemsHelper
- namespace хелпера. На момент написания статьи (15 сентября 2022 года) большая часть даже стандартных модулей Joomla переделана именно так, с частичным сохранением старой структуры. Полностью новым канонам пока что соответствует лишь модуль панели управленияmod_quickicon
- иконок быстрого доступа. - Upd 25.02.2023г. Начиная с версии Joomla 4.2.0 можно посмотреть примеры новой структуры модулей Joomla 4 в модулях для фронтенда
mod_articles_latest
,mod_articles_news.
Полезные дополнения
Об использовании \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 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' регистрозависимый). -
Получение объекта пользователя: вместо
JFactory::getUser()
(илиJoomla\CMS\Factory::getUser()
) используем$app->getIdentity()