Создание модулей с учётом новой структуры Joomla 4

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)

Старая файловая структура модуля Joomla 3
Старая файловая структура модуля Joomla 3

Для создания модуля было необходимо как минимум 3 файла:

  • mod_wtyandexmapitems.xml - описание модуля для установщика расширений Joomla (системное имя, дата, версия, сайт разработчика и т.д.), параметры конфигурации, сервер обновлений и т.д.

  • mod_wtyandexmapitems.php - "точка входа" в модуль. С этого файла начинается работа Вашего кода.

  • tmpl/default.php - макет вывода для модуля. Здесь находится HTML-вёрстка Вашего модуля. При необходимости, можно скопировать и переименовать этот файл, изменить вывод HTML по своему вкусу и выбрать в настройках свой новый макет вывода.
    Этот же способ позволяет выполнять любой свой PHP-код в нужном месте и в нужное время.

Именно такую структуру мы видим в одном из простейших модулей Joomla - mod_custom - "HTML-код".

Файловая структура модуля типа HTML-код в Joomla 3.
Файловая структура модуля типа HTML-код в Joomla 3.

Хелпер (helper) модуля Joomla

Если наш модуль делает что-то более сложное, чем просто вывод значений из настроек модуля, например:

  • отображает список новых статей на сайте;

  • показывает карусель товаров из компонента интернет-магазина;

  • выводит популярные комментарии или фотографии из фото-галереи ;

  • подтягивает данные модуля по ajax (ajax-корзина товаров, к примеру).

то все функции, выполняющие эту работу, помещаются в хелпер модуля. В Joomla 3 он помещался в файл helper.php, находящийся рядом с основным php-файлом. Подключение хелпера было в "точке входа" с помощью JLoader::register('ModWtyandexmapitemsHelper', __DIR__ . '/helper.php');

Стало (Joomla 4)

Новая файловая структура модуля для Joomla 4
Новая файловая структура модуля для Joomla 4

Для создания модуля в Joomla 4 нужны следующие файлы (с меньшим количеством можно поэкспериментировать спортивного интереса ради):

Файл mod_wtyandexmapitems.xml

Этот файл содержит описание модуля для установщика расширений Joomla (системное имя, дата, версия, сайт разработчика и т.д.), параметры конфигурации, сервер обновлений, а также задаёт Namespace модуля и директории для автозагрузки классов.

Namespace JoomlaModuleWtyandexmapitems начинается в modules/mod_wtyandexmapitems/src
Namespace Joomla\Module\Wtyandexmapitems начинается в modules/mod_wtyandexmapitems/src
<?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 для пользовательских полей модуля.

Собственные типы полей в Joomla 4
Собственные типы полей в Joomla 4

Поля мы складываем в src/Fields. У файлов полей должен быть namespace namespace Joomla\Module\Wtyandexmapitems\Site\Fields. Я использую собственный тип поля, расширяющий тип поля spacer (пробел), для вывода своего логотипа, версии модуля, ссылки на сайт и иногда дополнительной информации.

<field type="moduleinfo" addfieldprefix="Joomla\Module\Wtyandexmapitems\Site\Fields" name="moduleinfo"/>
 Вариант использования пользовательского типа поля в Joomla 4
Вариант использования пользовательского типа поля в Joomla 4

А вот пример использования в Joomla 3. Плагин для двухсторонней интеграции Joomla с CRM Битрикс 24 в настройках показывает информацию об аккаунте, из-под которого создан вебхук на стороне Битрикс 24. Если информация отображается, значит плагин настроен верно.

Вариант использования пользовательского типа поля в Joomla 3
Вариант использования пользовательского типа поля в Joomla 3

А здесь в настройках плагина отображается список стадий лида (или сделки), получаемый по API из CRM Битрикс 24. Это так же реализовано с помощью пользовательских типов полей (пример из версии для Joomla 3).

Получение данных для поля из API стороннего сервиса в Joomla 3
Получение данных для поля из API стороннего сервиса в Joomla 3

Гибридный вариант модуля

Если у Вас совсем нет времени, а завести "со шморгалкой" старый модуль на Joomla 4 всё-таки надо, поддерживается (пока что) как старый, так и гибридный вариант структуры модуля.

  1. Пока что можно обойтись без сервис-провайдера. Вообще. Тогда нужен файл "точки входа" mod_wtyandexmapitems.php и соответствующая строка в xml-манифесте модуля.

  2. Пока что можно обойтись без папки src. И подключать хелпер по старинке через JLoader::register('ModWtyandexmapitemsHelper', __DIR__ . '/helper.php'). Соответственно файл helper.php должен лежать рядом с "точкой входа" в модуль.

  3. Можно переместить хелпер в папку src, переименовать файл, назначить ему namespace (и в xml-манифесте модуля тоже) и использовать в "точке входа" просто use Joomla\Module\Wtyandexmapitems\Site\Helper\WtyandexmapitemsHelper - namespace хелпера. На момент написания статьи (15 сентября 2022 года) большая часть даже стандартных модулей Joomla переделана именно так, с частичным сохранением старой структуры. Полностью новым канонам пока что соответствует лишь модуль панели управления mod_quickicon - иконок быстрого доступа.

  4. Upd 25.02.2023г. Начиная с версии Joomla 4.2.0 можно посмотреть примеры новой структуры модулей Joomla 4 в модулях для фронтенда mod_articles_latestmod_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

  1. Статья Как правильно подключать JavaScript и CSS в Joomla 4

  2. Статья Использование 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()

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

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

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

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

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

89 Всего расширений
11 Категорий
395 Выпущено версий
380534 Всего скачиваний
Корзина
Корзина пуста