Pull to refresh
8
1.8

Joomla-разработчик

Send message

Использование MVC-фабрики в плагинах Joomla 5

Обычно, концепция Model‑View‑Controller используется в компонентах Joomla, но я хочу поделиться опытом её применения в плагинах.

Для чего это надо? — Сделать код более простым и понятным.

Задача плагина Joomla — подписываться на события и выполнять определенные действия. Но не стоит размещать весь код в классе плагина, лучше разделить его на несколько классов. Особенно, если для ваших задач Joomla предлагает готовые родительские классы.

Почему я предлагаю использовать MVC-фабрику? - Она не только создаёт экземплр класса, но и устанавливает основные зависимости из DI контейнера.

Например, плагин должен ответить на AJAX-запрос и вернуть массив данных. Для этого надо написать свой класс модели, наследующий Joomla\CMS\MVC\Model\ListModel.

Как это сделать? - Примерно так же как и в компонентах.

Классу плагина подключаем трейт Joomla\CMS\MVC\Factory\MVCFactoryAwareTrait.

В файле provider.php плагина регистрируем сервис-провайдер MVC-фабрики и передаём ему пространство имён плагина.

$container->registerServiceProvider(new MVCFactory('Joomla\\Plugin\\RadicalMart\\WishboxCdek'));

Устанавливаем плагину экземпляр фабрики, полученный из контенера.

$plugin->setMVCFactory($container->get(MVCFactoryInterface::class));

В src папке плагина, создаём папку Administrator, в ней папку Model и в ней файл с классом модели. То есть классы моделей будут расположены по пути Root\plugins\radicalmart\wishboxcdek\src\Administrator\Model.

Обратите внимание на папку Administrator, метод создания моделей обязательно требует либо Site либо Administrator в namespace.

namespace Joomla\Plugin\RadicalMart\WishboxCdek\Administrator\Model;

Ещё в класс модели надо установить свойство option:

protected $option = 'com_radicalmart';

Теперь мы можем получать экземпляр модели плагина Orders , следующим образом:

/** @var OrdersModel $ordersModel */
$ordersModel = $app->bootPlugin('wishboxcdek', 'radicalmart')
	->getMVCFactory()
	->createModel('Orders', 'Administrator');

В своём плагине я наследовал класс модели от BaseDatabaseModel, для работы с другими классами может потребоваться переопределение каких-либо свойств и методов.

Tags:
+1
Comments0

Для чего нужен атрибут useglobal в полях форм и как его применять в Joomla 5

В официальной документации есть краткая статья Conditional Field Display Customizing Forms Based on Global Settings, у меня не получилось перевести её дословно, поэтому расскажу своими словами.

Давайте разберёмся что это такое на примере компонента com_contact. Посмотрим настройки компонента.

(Здесь должна была быть картинка, но в посте она может быть только одна.)

Видим что параметр Contact Category в положении Hide (у вас может быть в другом).

Теперь откроем страницу создания/изменения контакта и перейдём на вкладку Display.

Мы видим что параметр Category имеет первый вариант Use Global (Hide). То есть нам не надо помнить значение из общих настроек компонента, оно перед нами. Если мы изменим его в компоненте, то оно изменится и здесь.

Как это использовать разработчику компонентов

У меня есть простой компонент для экспорта товаров из JoomShopping в XML com_wishboxjshoppingxmlexport.

Добавлю в настройки компонента поле test_mode в файле: Root/administrator/components/com_wishboxjshoppingxmlexport/config.xml

<field
		name="test_mode"
		type="list"
		label="COM_WISHBOXJSHOPPINGXMLEXPORT_FIELD_TEST_MODE_LABEL"
		default="0"
>
	<option value="0">JNO</option>
	<option value="1">JYES</option>
</field>

И добавлю такое же поле в сущность (item) в файле: Root/administrator/components/com_wishboxjshoppingxmlexport/forms/item.xml

<field
		name="test_mode"
		type="list"
		label="COM_WISHBOXJSHOPPINGXMLEXPORT_FIELD_TEST_MODE_LABEL"
		default="0"
>
	<option value="0">JNO</option>
	<option value="1">JYES</option>
</field>

Добавим полю в форме Item атрибут useglobal="true"

<field
		name="test_mode"
		type="list"
		label="COM_WISHBOXJSHOPPINGXMLEXPORT_FIELD_TEST_MODE_LABEL"
		default="0"
        useglobal="true"
>
	<option value="0">JNO</option>
	<option value="1">JYES</option>
</field>

Готово! На странице создания/изменения Item‑а экспорта у поля test_mode появилась опция Use Global (Yes). То есть достаточно добавить атрибут useglobal="true".

Из каких глобальных настроек берётся значение

  • из настроек компонента (компонент определяется параметром option в URL, кроме редактирования пункта меню, для меню подставляется компонент из ссылки пункта меню);

  • из общих настроек Joomla если не получили из настроек компонента.

В каких типах полей стандартных полей работает этот атрибут

  • ListField — Добавляется опция Use Global (...);

  • NumberField — Значение из глобальных настроек отображается как placeholder;

  • TextField — Placeholder.

А так же во всех полях, которые наследуют одно их вышеперечисленных.

Tags:
+3
Comments0

Работаем с событиями без плагина или как перехватить редирект в Joomla 5

В расширении быстрого заказа для JoomShopping. Я создаю экземпляр модели Checkout и вызываю метод checkStep().

$checkoutModel = JSFactory::getModel('checkout', 'Site');
$checkoutModel->checkStep(2);

В методе checkStep есть условия, при выполнении которых происходит установка сообщения и редирект.

if (!$check)
{
    JSError::raiseNotice('', 'Message');
    $mainframe->redirect('Url');
    exit();
}

Но мне надо получить результат метода checkStep или исключение. И продолжить работу.

Смотрим какое событие вызывается при редиректе — ApplicationEvents::BEFORE_RESPOND.

Подписываться на события могут не только плагины, но и любые другие callable объекты.

Создадим функцию которая получает заголовки редиректа, удаляет их и бросает исключение:

$onApplicationBeforeRespond = function()
{
	$app = Factory::getApplication();
	$response = $app->getResponse();
	$status = $response->getHeader('Status');
	$location = $response->getHeader('Location');
	$response = $response->withoutHeader('Status');
	$response = $response->withoutHeader('Location');
	$app->setResponse($response);

	throw new RedirectCaughtException($status[0], $location[0]);
};

Перед вызовом $checkoutModel->checkStep(2); подпишем нашу функцию на прослушивание события, а после отпишем. И обернём вызов в try/catch.

$dispatcher->addListener(
	ApplicationEvents::BEFORE_RESPOND,
	$onApplicationBeforeRespond
);

try
{
	$checkoutModel->checkStep(2);
}
catch (RedirectCaughtException $e)
{
    // Обрабатываем исключение
}

$dispatcher->removeListener(
	ApplicationEvents::BEFORE_RESPOND,
	$onApplicationBeforeRespond
);

Таким образом мы можем перехватывать редиректы внутри блока try.

Tags:
+1
Comments0

Класс расширения (Extension) для компонентов Joomla 5

Перевод с английского: Joomla! Programmers Documentation for Joomla 5.2

В ряде случаев Joomla может взаимодействовать с нашим компонентом. Например:

  • Роутер Joomla может использовать роутер нашего компонента для анализа и создания ЧПУ-адресов;

  • Если наш компонент поддерживает категории, то com_categories будет отображать в представлении категорий сводку по каждой категории, содержащую количество материалов с этой категорией, с разбивкой по статусу публикации;

  • Если наш компонент поддерживает пользовательские поля, то com_fields будет вызывать метод getContexts(), чтобы получить типы материалов, к которым можно привязать пользовательские поля;

  • Если наш компонент поддерживает мультиязычные связи, то com_associations потребуется знать типы материалов, для которых возможны связи;

  • И, конечно же, Joomla потребуется запустить наш компонент, чтобы получить вывод.

Причины введения класса расширения становятся понятнее, если рассмотреть, как другие части кода Joomla взаимодействовали с нашим расширением в Joomla 3.

В Joomla 3 все эти части кода обращались к кодовой базе нашего компонента довольно хаотичным образом — вызывая функции из различных вспомогательных файлов.

А в Joomla 4 это упрощено:

Начиная с Joomla 4, другие компоненты получают доступ к нашему компоненту com_example, вызывая метод:

$extension = $app->bootComponent("com_example");

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

Справка:

Класс расширения (Extension) компонента вы можете найти в административной части компонента. Например, для компонента com_content это файл Root/administrator/components/com_content/src/Extension/ContentComponent.php, для com_exampleRoot/administrator/components/com_example/src/Extension/ExampleComponent.php

Сразу после создания экземпляра класса расширения компонента код библиотеки Joomla вызовет метод boot вашего класса расширения, передавая дочерний экземпляр Контейнера внедрения зависимостей (Dependency Injection Container):

$extension->boot($container);

По сути, это возможность делать буквально всё, что вам нужно. Например, метод иногда используется для настройки определённых классов, которые будут использоваться с вызовами HtmlHelper::_(). Или же можно сохранить ссылку на ваш дочерний DI-контейнер (который иначе может быть сложно получить).

После первого создания экземпляра вашего компонента Joomla кэширует этот экземпляр. При повторном вызове

$extension = $app->bootComponent("com_example");

Joomla просто возвращает существующий экземпляр вашего класса расширения, вместо повторного создания объекта и вызова метода boot(). Вы даже можете вызвать bootComponent, передав свой компонент, чтобы получить ссылку на свой собственный объект расширения.

Tags:
Total votes 1: ↑1 and ↓0+1
Comments0

Расширения и дочерние контейнеры в Joomla 5

Перевод с английского: Joomla! Programmers Documentation for Joomla 5.2

Всякий раз, когда Joomla загружает расширение, она создает дочерний Dependency Injection Container (далее контейнер), исключительно для использования этого расширения. Это показано на схеме ниже.

Child DIC
Child DIC

Дочерний контейнер содержит указатель на родительский контейнер и функционирует аналогично основному контейнеру, но с некоторыми отличиями:

  • При каждом вызове метода set() для этого дочернего контенера пара ключ-значение добавляется в дочерний контейнер;

  • При каждом вызове метода get() для этого дочернего контейнера ресурс извлекается из дочернего контейнера, но если он там не найден, то поиск выполняется также в родительском контейнере.

Начиная с версии Joomla 4, разработчики Joomla рекомендуют создателям расширений использовать внедрение зависимостей (dependency injection) для своих расширений, определяя файл services/provider.php. Загрузка расширения теперь выполняется в два этапа, которые обрабатываются внутри файла services/provider.php:

  1. Класс расширения регистрируется в дочернем контейнере;

  2. Класс расширения извлекается из дочернего контейнера, создавая экземпляр этого класса.

Давайте рассмотрим минимальный пример этого для компонента com_example с пространством имён Mycompany\Component\Example.

use Joomla\CMS\Extension\ComponentInterface;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Mycompany\Component\Example\Administrator\Extension\ExampleComponent;
return new class implements ServiceProviderInterface
{
  public function register(Container $container): void
  {
    $container->set(
      ComponentInterface::class,
      function (Container $container)
      {
        $component = new ExampleComponent();
        return $component;
      }
    );
  }
};

Мы видим, что когда Joomla выполняет require этого PHP-файла, возвращается экземпляр класса.

$provider = require $path;  // $path points to the relevant services/provider.php file

Переменная $provider указывает на объект, который является экземпляром этого анонимного класса. Кроме того, класс реализует интерфейс Joomla\DI\ServiceProviderInterface, что, по сути, означает, что он содержит метод register с указанной выше сигнатурой.

Когда Joomla выполняет

if ($provider instanceof ServiceProviderInterface)
{
  $provider->register($container);

будет вызван метод register, который добавит запись в дочерний контейнер с

  • key - ComponentInterface::class – это сокращённый способ в PHP указать строку 'Joomla\CMS\Extension\ComponentInterface';

  • value - функция, которая возвращает новый экземпляр класса ExampleComponent (основного класса расширения компонента com_example).

$extension = $container->get($type);

Функция выше будет запущена и вернет новый экземпляр ExampleComponent.

Вы можете самостоятельно изучить код Joomla в файле libraries/src/Extension/ExtensionManagerTrait.php и убедиться, что описанный выше шаблон также применяется к модулям и плагинам.

Обратите также внимание на следующую строку в этом файле:

if ($extension instanceof BootableExtensionInterface)
{
  $extension->boot($container);
}

Таким образом, если класс расширения реализует интерфейс BootableExtensionInterface, Joomla немедленно вызовет метод boot() экземпляра расширения, как описано в документации по расширениям.

Tags:
Total votes 2: ↑2 and ↓0+3
Comments0

Почему не следует напрямую обращаться к глобальному контейнеру в Joomla 5

Что общего у нижеперечисленных методов?

  • Joomla\CMS\Factory::getCache()

  • Joomla\CMS\Factory::getDbo()

  • Joomla\CMS\Factory::getMailer()

  • Joomla\CMS\Application\AdministratorApplication::getRouter()

  • Joomla\CMS\Application\Administrator\ApiApplication::getRouter()

  • Joomla\CMS\Toolbar\Toolbar::getInstance()

Они объявлены устаревшими с Joomla 4.x и вместо них предлагается использовать Joomla\CMS\Factory::getContainer()->get('Соответствующий_интерфейс').

Например, вместо Joomla\CMS\Factory::getDbo()Factory::getContainer()->get(DatabaseInterface::class);

Но давайте посмотрим на описание метода Joomla\CMS\Factory::getContainer():

Возвращает глобальный контейнер сервисов, создавая его только в случае отсутствия.

Этот метод рекомендуется использовать только в коде, ответственном за создание новых сервисов и требующем разрешения зависимостей. Его следует применять только когда контейнер недоступен другими способами.

Допустимые сценарии использования:

  • Статический метод getInstance(), вызывающий сервис из контейнера (пример: Joomla\CMS\Toolbar\Toolbar::getInstance());

  • Фронт-контроллер приложения, загружающий и исполняющий класс Joomla (пример: файл cli/joomla.php);

  • Получение опциональных зависимостей конструктора во время переходного периода для сохранения обратной совместимости (в этом случае следует добавлять уведомление об устаревании).

Не рекомендуется использовать этот метод как прямую замену статическим вызовам, например заменять Factory::getDbo() на Factory::getContainer()->get(DatabaseInterface::class). Вместо этого код следует рефакторить для использования внедрения зависимостей.

В последнем абзаце видим явное противоречие и рекомендацию использовать зависимости.

Как же внедрять зависимости?

Рассмотрим как это сделано в плагине joomla группы content:

В сервис-провайдере плагина Root/plugins/content/joomla/services/provider.php используются методы setDatabase() и setUserFactory() для установки зависимостей.

$plugin->setDatabase($container->get(DatabaseInterface::class));
$plugin->setUserFactory($container->get(UserFactoryInterface::class));

А в классе расширения плагина plugins/content/joomla/src/Extension/Joomla.php используются методы getDatabase() и getUserFactory(). Аналогично в компонентах.

Мой пример использования:

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

Выводы:

  • Сервис-провайдер расширения — единая точка установки зависимостей для своего расширения.

  • Событие onAfterExtensionBoot - точка для замены зависимости в любом расширении.

  • Если бы расширения напрямую брали зависимости из глобального контейнера, такая замена была бы невозможна.

Материалы по теме:

Tags:
Total votes 1: ↑1 and ↓0+1
Comments0

Работаем в лоб: Прямое редактирование XML форм Joomla

Используя событие onContentPrepareForm можно изменять почти любую форму Joomla, но методов класса Joomla\CMS\Form\Form обычно не хватает для работы со сложными формами (например с полями типа subform).

Но есть простое решение - работать с формой как c экземпляром SimpleXMLElement.

Получаем XML формы.

echo $form->getXml()->asXMl();
die;

Отправляем его в ChatGPT с описанием что и как надо изменить в форме, и просим написать PHP-код.

Например у меня такая форма:

<?xml version="1.0"?>
<form>
	<config>
		<fieldset label="PLG_CONTENT_WISHBOXRADICALMARTCDEKORDERREGISTRATOR_FIELDSET_LABEL" name="wishboxradicalmartcdekorderregistrator"
                  addfieldprefix="Joomla\Component\Wishboxradicalmartcdek\Administrator\Field">
			<field name="wishboxradicalmartcdekorderregistrator" type="subform"
                   label="PLG_CONTENT_WISHBOXRADICALMARTCDEKORDERREGISTRATOR_FIELD_REGISTRATOR_LABEL"
                   buttons="add,remove,move"
                   multiple="false"
                   hiddenLabel="true">
				<form>
					<fieldset>
						<field name="order_number_prefix" type="text"
                               label="COM_WISHBOXRADICALMARTCDEK_FIELD_ORDER_NUMBER_PREFIX_LABEL"
                               default="test_" />
					</fieldset>
				</form>
			</field>
		</fieldset>
	</config>
</form>

И для изменения атрибута default поля order_number_prefix получаем следующий код:

$fields = $xml->xpath('//field[@name="wishboxradicalmartcdekorderregistrator"]/form/fieldset/field[@name="order_number_prefix"]');

$fields[0]['default'] = 'Test ';

Это очень простой пример, но таким образом мы получаем почти неограниченые возможности для изменения форм.

Tags:
Rating0
Comments0

Заменяем ещё не устаревший метод Joomla\CMS\Toolbar\ToolbarHelper::custom

Недостаток данного метода заключается в том что мы не можем задать id для тега joomla-toolbar-button, он формируется из параметра icon. Это особенно неудобно когда требуется несколько кнопок с одинаковыми иконками.

было:

ToolbarHelper::custom(
	'cities.update',
	'refresh',
	'',
	Text::_('COM_WISHBOXCDEK_TOOLBAR_UPDATE_CITIES'),
	false
);
<joomla-toolbar-button id="toolbar-refresh" task="cities.update">
<button class="button-refresh btn btn-primary" type="button">
    <span class="icon-refresh" aria-hidden="true"></span>
    Обновить города
</button>
</joomla-toolbar-button>

стало:

Factory::getApplication()->getDocument()->getToolbar()->standardButton(
	'cities-update',
	'COM_WISHBOXCDEK_TOOLBAR_UPDATE_CITIES',
	'cities.update'
	)
	->icon('icon-refresh')
	->listCheck(false);
<joomla-toolbar-button id="toolbar-cities-update" task="cities.update">
<button class="button-cities-update btn btn-primary" type="button">
    <span class="icon-refresh" aria-hidden="true"></span>
    Обновить город</button>
</joomla-toolbar-button>
Tags:
Rating0
Comments0

Как в тулбар одного компонента добавить кнопку, которую будет обрабатывать другой компонент Joomla 5?

Для примера добавим в компонент стандартных материалов (com_content) кнопку из компонента очистки кэша (com_cache).

Как работают стандартные кнопки

Нажатия кнопок тулбара обычно обрабатываются компонентом, на странице которого мы находимся. Вот HTML-код кнопки «Создать» в стандартном компоненте материалов:

<joomla-toolbar-button id="toolbar-new" task="article.add">
    <button class="button-new btn btn-success" type="button">
        <span class="icon-new" aria-hidden="true"></span>
        Создать
    </button>
</joomla-toolbar-button>

Как вы видите, у тега joomla-toolbar-button есть аттрибут task, со значением article.add. Когда вы нажимаете на подобную кнопку, Joomla находит на странице форму adminForm, устанавливает в её поле с именем task значение из атрибута и отправляет форму. По значению поля task мы понимаем что будет вызван метод add контроллера article. А на компонент com_content нам указывает атрибут action формы.

Значит нам нужна отдельная форма

Создадим системный плагин и добавим в конец страницы форму с id 'cacheAdminForm':

/**
 * @return  void
 *
 * @since 1.0.0
 */
public function onAfterRender(): void
{
	$app = $this->getApplication();

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

	$option = $app->getInput()->getCmd('option', '');
	$view = $app->getInput()->getCmd('view', '');

	if ($option == 'com_content' && $view == 'articles')
	{
		$buffer = $app->getBody();

		$buffer .= '<form'
			. ' action="' . Route::_('index.php?option=com_cache') . '"'
			. ' method="post"'
			. ' name="cacheAdminForm"'
			. ' id="cacheAdminForm"'
			. '>'
			. '<input type="hidden" name="task" value="" />'
			. HTMLHelper::_('form.token')
			. '</form>';

		$app->setBody($buffer);
	}
}

А теперь добавим на тулбар нашу кнопку:

/**
 * @param   Event  $event  Event
 *
 * @return void
 *
 * @throws Exception
 *
 * @since 1.0.0
 *
 * @noinspection PhpUnused
 * @noinspection PhpUnusedParameterInspection
 */
public function onBeforeRender(Event $event): void
{
	$app = $this->getApplication();

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

	$option = $app->getInput()->getCmd('option', '');
	$view = $app->getInput()->getCmd('view', '');

	if ($option == 'com_content' && $view == 'articles')
	{
		/** @var Document $document */
		$document = $app->getDocument();

		/** @var Toolbar $toolbar */
		$toolbar = $document->getToolbar();

        // Метод standardButton добавляет кнопку и возвращает её
		$toolbar->standardButton(
			'cachedeleteall',
			'PLG_SYSTEM_WISHBOXTOOLBARBUTTONTODIFFERENTCOMPONENT_BUTTON_TO_DIFFERENT_COMPONENT',
			'deleteAll'
		)
			->icon('icon-remove')
			->listCheck(false)
			->buttonClass('button-remove btn btn-primary')
			->form('cacheAdminForm');
	}
}

Обратите внимание что в кнопку мы передали id формы 'cacheAdminForm'.

GitHub c кодом плагина

Tags:
Rating0
Comments0

Как вызвать событие только для указанной (одной или более) группы плагинов в Joomla 5

Обычно события в Joomla вызываются следующим образом:

Шаг 1: Получаем объект диспечера

В коде Joomla можно найти несколько способов получить объект диспечера:

От приложения:

$dispatcher = Joomla\CMS\Factory::getApplication()->getDispatcher();

Из контейнера:

$dispatcher = Joomla\CMS\Factory::getContainer()
 ->get(Joomla\Event\DispatcherInterface::class);

Свой диспечер (если ваш класс реализует Joomla\Event\DispatcherAwareInterface):

$dispatcher = $this->getDispatcher();

Шаг 2: Подключаем плагины нужной группы

PluginHelper::importPlugin('mycomponent');

Шаг 3: Создаём экземпляр события и вызываем метод диспечера dispatch

$dispatcher->dispatch('onMyComponentEvent', $event);

В результате будут вызваны плагины не только группы mycomponent, но и всех ранее подключенных групп (например, 'system'). Потому что любым из вышеперечисленных способов, мы получаем возвращают один и тот же экземпляр диспечера. И код PluginHelper::importPlugin('mycomponent'); работает с тем же экземпляром диспечера.

А следующим образом можно вызвать плагины только нужных групп:

// Создаём свой обьъект диспечера
$dispatcher = new Dispatcher;

// Подключаем нужную группу плагинов, и четвертым параметром передём наш диспечер
PluginHelper::importPlugin('mycomponent', null, true, $dispatcher);
// Так же можем подключить ещё одну группу
PluginHelper::importPlugin('content', null, true, $dispatcher);

// Создаём екземпляр события
// ...

// Вызываем метод dispatch
$dispatcher->dispatch('onMyComponentEvent', $event);

Tags:
Total votes 1: ↑1 and ↓0+1
Comments0

Управление очередностью плагинов в Joomla 5 с помощью приоритетов обработки событий

В Joomla 5 плагины подписываются на события с помощью интерфейса Joomla\Event\SubscriberInterface в нём всего один метод — getSubscribedEvents(), который должен вернуть массив соответствий событий, которые будет прослушивать этот плагин и их обработчиков.

Например, в плагине «Content — Email Cloaking» этот метод выглядит следующим образом:

    public static function getSubscribedEvents(): array
    {
        return ['onContentPrepare' => 'onContentPrepare'];
    }

Ключи массива — имена событий, а элементы масива — имена методов класса плагина.

Но если посмотреть на этот же метод плагина «Behaviour — Backward Compatibility»:

    public static function getSubscribedEvents(): array
    {
        /**
         * Note that onAfterInitialise must be the first handlers to run for this
         * plugin to operate as expected. These handlers load compatibility code which
         * might be needed by other plugins
         */
        return [
            'onAfterInitialiseDocument' => ['onAfterInitialiseDocument', Priority::HIGH],
        ];
    }

То видим что в нём элементы массива - тоже массивы, первый элементом которых - имя метода, второй - приоритет Joomla\Event\Priority::HIGH. И комментарий объясняет что событие onAfterInitialise этим плагином должно обрабатываться раньше чем другими плагинами, потому что этот плагин загружает код, необходимый для работы других плагинов.

Всего доступно семь уровней приоритета:

public const MIN = -3;
public const LOW = -2;
public const BELOW_NORMAL = -1;
public const NORMAL = 0;
public const ABOVE_NORMAL = 1;
public const HIGH = 2;
public const MAX = 3;

По-умолчанию устанавливается «нормальный» приоритет.

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

Tags:
Rating0
Comments0

Использование своего класса MVC фабрики в компоненте Joomla 5​

Давно назрела необходимость переопределить ->createModel() в своём компоненте. И я хотел сделать это правильно, заменив класс MVC фабрики своим.

Давайте разберемся как создаётся экземпляр MVCFactory в компоненте

Откройте файл administrator/components/com_mycomponent/services/provider.php одного из стандартных компонентов Joomla. В нём нас интересует строка:

$container->registerServiceProvider(new MVCFactory('\Joomla\Component\MyComponent'));

В этой строке создаётся объект класса сервис-провайдера Joomla\CMS\Extension\Service\Provider\MVCFactory реализующего интерфейс Joomla\DI\ServiceProviderInterface. В этом интерфейсе всего один метод register.

Вот содержимое этого метода:

$container->set(
	MVCFactoryInterface::class,
	function (Container $container) {
		if (\Joomla\CMS\Factory::getApplication()->isClient('api')) {
			$factory = new ApiMVCFactory($this->namespace);
		} else {
			$factory = new \Joomla\CMS\MVC\Factory\MVCFactory($this->namespace);
		}

		// ...
		return $factory;
	}
);

Как видим, в контейнер Joomla\DI\Container в метод set передаются два параметра:

  • имя ресурса string имя интерфейса Joomla\CMS\MVC\Factory\MVCFactoryInterface::class, который должна реализовывать MVC-фабрика;

  • функция callable функция, которая создаёт экземпляр класса MVC-фабрики, реализующего данный интерфейс.

Таким образом, для внедрения собственного класса MVC фабрики надо создать два новых класса (я не привожу код классов, так как вы можете просто наследовать их от стандартных.):

  • Собственно класс фабрики, реализующий интерфейс Joomla\CMS\MVC\Factory\MVCFactoryInterface (можно наследовать от стандартного Joomla\CMS\MVC\Factory\MVCFactory);

  • И класс сервис-провайдера реализующий интерфейс Joomla\DI\ServiceProviderInterface (можно наследовать от стандартного Joomla\CMS\Extension\Service\Provider\MVCFactory).

И в файле administrator/components/com_mycomponent/services/provider.php в методе register зарегистрировать свой сервис-провайдер вместо стандартного:

$container->registerServiceProvider(new MyMVCFactory('\\Joomla\\Component\\MyComponent'));

Теперь вы можете получить доступ к своей MVC фабрике следующим образом:

В контроллерах (MVC фабрику своего компонента):

$mvcFactory = $this->factory;

В классах использующих MVCFactoryAwareTrait , например в моделях наследующих класс BaseDatabaseModel(MVC фабрику своего компонента):

$mvcFactory = $this->getMVCFactory();

В любом месте можно получить MVC фаблику любого компонента:

$mvcFactory = Joomla\CMS\Factory::getApplication()
  ->bootComponent('my_component')
  ->getMVCFactory();

Tags:
Total votes 1: ↑1 and ↓0+1
Comments0

Заменяем устаревший метод Joomla\CMS\Toolbar\Toolbar::getInstance() в Joomla 5.2.5.

Joomla предлагает использовать Factory::getContainer()->get(ToolbarFactoryInterface::class)->createToolbar().

/**
 * @deprecated  4.0 will be removed in 6.0
 *              Use the ToolbarFactoryInterface instead
 *              Example:
 *              Factory::getContainer()->get(ToolbarFactoryInterface::class)->createToolbar($name)
 */

Но код:

$toolbar = Factory::getContainer()->get(ToolbarFactoryInterface::class)->createToolbar();

Создаст новый объект класса Toolbar и не является заменой коду:

$toolbar = Toolbar::getInstance();

Правильно будет получать объект Toolbar от объекта Document:

$toolbar = Factory::getApplication()->getDocument()->getToolbar();

Tags:
Total votes 1: ↑1 and ↓0+1
Comments0

$event->stopPropagation() когда, где и зачем надо останавливать события?

Метод stopPropagation(), есть у любого события, поддерживающего интерфейс Joomla\Event\EventInterface, по умолчанию метод описан в классе Joomla\Event\AbstractEvent.

Как это работает?

События обрабатываются плагинами поочередно, перед каждым вызовом проверяется не остановлено ли событие.

if (isset($this->listeners[$event->getName()])) {
    foreach ($this->listeners[$event->getName()] as $listener) {
          if ($event->isStopped()) {
              return $event;
          }

          $listener($event);
      }
}

Где можно использовать?

Существуют события которые могут или должны быть обработать только одним плагином. Например, событие onUserAuthenticate, работает до первого успешного результата, если пользователь уже авторизован по токену, то незачем пытаться авторизовать его другими способами.

Ещё пример: в моём компоненте импорта в каждом задании явно указан плагин, обеспечивающий получение файла с данными (по ссылке, почте, и др.)

Откуда вызывать?

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

public function onMyImportCheckNewFiles(CheckNewFilesEvent $event): void
{
	$stockTable = $event->getStockTable();

	if ($stockTable->plugin == 'email')
	{
		$event->stopPropagation();

		// ...
	}
}

Или после достижения цели, если событие обрабатывается всеми плагинами до первого результата.

public function onMyImportCheckNewFiles(CheckNewFilesEvent $event): void
{
	$stockTable = $event->getStockTable();

	if ($this->chekNewFiles($stockTable))
	{
		$event->stopPropagation();
	}
}

Если событие требует от плагинов какого либо результата, то остановку события можно перенести в метод установки этого результата.

public function onMyImportCheckNewFiles(CheckNewFilesEvent $event): void
{
	$stockTable = $event->getStockTable();

	if ($newFiles = $this->chekNewFiles($stockTable))
	{
		// $event->stopPropagation(); будет вызван в методе setNewFiles
		$event->setNewFiles($newFiles);
	}
}

Tags:
Rating0
Comments0

Изменение конфига MySQL в Joomla 5.2.1 с помощью плагина
Или решаем откладываем проблему с большим количеством характеристик товаров в JoomShopping.

Ситуация:
В JoomShopping для каждой характеристики товара создаётся столбец в таблице
#__jshopping_products_to_extra_fields. Со временем характеристик становится много и попытка создания новой характеристики приводит к ошибке. До сих пор помогало изменение типа столбцов с VARCHAR(100) на TEXT. Но на днях столкнулся с тем что после создания около 400 храктеристик проблема вернулась.

В качестве решения можно отключить строгий режим в MySQL.

Joomla позволяет сделать это даже если у вас нет прав изменять конфигурацию сервера.

В классе Joomla\Database\Mysqli\MysqliDriver в методе connect вызывается событие onAfterConnect. Напишем системный плагин и в обработчике этого события выполним запрос для установки innodb_strict_mode=0.

$driver = $event->getDriver();
$driver->connection->query('SET @@SESSION.innodb_strict_mode = 0;');

Таким образом запрос SET @SESSIONN.innodb_strict_mode = 0; будет выполняться при каждом соединении с базой.

Tags:
Total votes 1: ↑1 and ↓0+1
Comments0

Использование внешей базы данных в Joomla 5.2.1

Писать компонент импорта цен и остатков для Joomla 3 уже не актуально. Но можно написать компонент для Joomla 5, который будет работать с базой Joomla 3.

Добавляем поле в параметры:

<field name="external_database" type="subform" label="PLG_SYSTEM_MYPLUGGIN_FIELD_EXTERNAL_DATABASE_LABEL" multiple="false">
	<form>
		<field name="driver" type="databaseconnection" label="COM_CONFIG_FIELD_DATABASE_TYPE_LABEL" description="COM_CONFIG_FIELD_DATABASE_TYPE_DESC" supported="mysql,mysqli,pgsql,postgresql" filter="string" />
		<field name="host" type="text" label="COM_CONFIG_FIELD_DATABASE_HOST_LABEL" required="true" filter="string" />
		<field name="user" type="text" label="COM_CONFIG_FIELD_DATABASE_USERNAME_LABEL" required="true" filter="string" />
		<field name="password" type="password" label="COM_CONFIG_FIELD_DATABASE_PASSWORD_LABEL" description="COM_CONFIG_FIELD_DATABASE_PASSWORD_DESC" filter="raw" autocomplete="off" lock="true" />
		<field name="database" type="text" label="COM_CONFIG_FIELD_DATABASE_NAME_LABEL" required="true" filter="string" />
		<field name="prefix" type="text" label="COM_CONFIG_FIELD_DATABASE_PREFIX_LABEL" default="jos_" filter="string" />
	</form>
</field>

Создаём соединение в конструкторе модели:

$options = (array) $params->get('external_database');
$db = (new DatabaseFactory)->getDriver('mysqli', $options);

$this->setDatabase($db);

Теперь в модели $this->getDatabase() будет возвращать объект для работы с внешней базой.

Tags:
Total votes 1: ↑1 and ↓0+1
Comments0

Заменяем устаревший метод Joomla\CMS\Table\Table::getInstance в Joomla 5.1.4.

Раньше объект таблицы создавали методом getInstance класса Joomla\CMS\Table\Table.

<?php
$table = Joomla\CMS\Table\Table::getInstance('Content', 'Table');

Если класс таблицы принадлежал компоненту, то мы подключали папку с таблицами этого компонента.

Например, так создаётся объект таблицы Featured:

<?php
Joomla\CMS\Table\Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_content/table');
$featured = Table::getInstance('featured', 'ContentTable');

Но начиная с версии 4.3 метод getInstance объявлен устаревшим и будет удален в версии 6.0. В качестве подсказки разработчики предлагают следующий пример.

 /**
  * Example: Factory::getApplication()->bootComponent('...')->getMVCFactory()->createTable($name, $prefix, $config);
  */

Перепишем код для создания объекта таблицы Featured.

Было:

<?php
Joomla\CMS\Table\Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_content/table');
$featured = Table::getInstance('Featured', 'ContentTable');

Стало:

<?php
$table = Joomla\CMS\Factory::getApplication()
    ->bootComponent('com_content')
    ->getMVCFactory()->createTable('Featured', 'Administrator');

А вот как быть с таблицами ядра, например, Content? Ответ оказался очень прост - использовать конструктор класса.

Было:

<?php
$table = Joomla\CMS\Table\Table::getInstance('Content', 'Table');

Стало:

<?php
$table = new Joomla\CMS\Table\Content($db);

Tags:
Total votes 3: ↑2 and ↓1+3
Comments0

Заменяем устаревший метод CMSApplicationInterface::triggerEvent в Joomla 5.1.4.

Рассмотрим решение на примере события onContentPrepareForm. Раньше вызов выглядел следующим образом:

<?php
$app->triggerEvent('onContentPrepareForm', [$form, $data]);

А теперь документация подсказывает что данный метод устарел в 4.0 и будет удалён в 6.0. И предлагает следующий код:

<?php
Factory::getApplication()->getDispatcher()->dispatch($eventName, $event);

Очевидно что первый параметр метода dispatch - имя события.

А со вторым немного сложнее, теперь событие - это объект.

Создание объекта события

Joomla\CMS\Event\AbstractEvent::create($eventName, $arguments)

$eventName - имя события

$arguments - ассоциативный массив аргументов (aргумент subject - обязательный).

Класс объекта события

Для событий ядра AbstractEvent::create вернёт объект класса, соответствующий имени события (полный список вы найдёте в файле libraries/src/Event/CoreEventAware.php).

Для событий сторонних расширений будет создан объект класса Joomla\CMS\Event\GenericEvent .

В аргументе eventClass вы можете указать имя своего собственного класса события.

Пример вызова события onContentPrepareForm:

<?php
/** @var \Joomla\CMS\Event\Model\PrepareFormEvent $event */
$event = AbstractEvent::create(
    'onContentPrepareForm',
    // важно соблюдать порядок аргументов, для совместимости со старыми плагинами
    [
        'subject' => $form, 
        'data'    => $data
    ]
);
$app->getDispatcher()->dispatch($event->getName(), $event);

Tags:
Total votes 2: ↑2 and ↓0+4
Comments0

Заменяем устаревший метод \Joomla\CMS\Form::getInstance в Joomla.

Документация Joomla сообщает что метод getInstance устарел с версии 4.3 и будет удален в 6.0. И предлагает использовать FormFactory service из контейнера.

Factory::getContainer()->get(FormFactoryInterface::class)->createForm($name, $options);

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

Было так:

<?php
// подключаем папку
\Joomla\CMS\Form::addFormPath(JPATH_SITE . '/components/com_jshopping/addons/tags/forms');

$form = \Joomla\CMS\Form::getInstance(
    'configform',
    'configform',
    [
        'control' => 'params',
        'load_data' => true
    ],
    false
);

Или так, если если используются файлы форм с одинаковыми именами:

<?php
$form = \Joomla\CMS\Form::getInstance(
    'configform',
    JPATH_SITE . '/components/com_jshopping/addons/tags/forms/configform',
    [
        'control' => 'params',
        'load_data' => true
    ]
);

Пример исправленного кода с загрузкой xml-файла:

<?php
$form = Factory::getContainer()
    ->get(FormFactoryInterface::class)
    ->createForm(
        'com_jshopping.addons.tags.configform', // имя формы, которое позволит идентифицировать её в плагинах 
        [
            'control'   => 'params',
            'load_data' => true
        ]
    );

if (!$form->loadFile(JPATH_SITE . '/components/com_jshopping/addons/tags/forms/configform.xml'))
{
    throw new Exception('File loading error.');
}

Tags:
Total votes 1: ↑1 and ↓0+3
Comments0

Пример принципа единственной ответственности для Joomla

Имеем:

Форму с полями город и пункт выдачи. Поля наследуют ListField.

<field
	name="city_id"
	type="city"
	label="City"
/>
<field
	name="point_id"
	type="point"
	label="City"
/>

Задача:

В методе getOptions класса PointField получить id выбранного города.

Очевидное решение:

Получим id города из данных формы. Такой способ применяется в ядре Joomla.

<?php
class PointField
{
	protected function getOptions(): array
	{
		$cityId = $this->form->getValue('city_id');
	}
}

Но через некоторое время нам может потребоваться добавить в форму ещё пару аналогичных полей. recipient_city_id и recipient_point_id.

Но теперь второе поле точки типа Point зависит от не своего поля город.

Как это исправить? Неужели делать отдельные типы полей для отправителя и получателя? А если у нас вообще не будет поля город?

Проблема в том что мы поручили классу PointField получение id города.
$cityId = $this->form->getValue('city_id');

А его единственной ответственностью должен быть вывод списка точек доставки из заданного города.

Уничверсальное решение:

Добавим полю Point атрибут city_id прямо в XML, или программно, в событии onContentPrepareForm.

<field
	name="point_id"
	type="point"
	city_id=""
	label="City"
/>
<?php
class PointField
{
	protected function getOptions(): array
	{
		$cityId = $this->element['city_id'];
	}
}

Теперь поле зависит только от своего атрибута.

Так же можно передавать и более сложные данные. Например. размеры посылки: {width:10,height:7,length:20}.

Tags:
Total votes 3: ↑3 and ↓0+3
Comments0

Information

Rating
1,484-th
Registered
Activity

Specialization

Backend Developer, Web Developer
Middle
OOP
PHP
Joomla