Pull to refresh
8
1.9

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

Send message

Класс расширения (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:
+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:
+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:
+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:
0
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:
0
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,383-rd
Registered
Activity

Specialization

Backend Developer, Web Developer
Middle
OOP
PHP
Joomla