惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

小众软件
小众软件
N
News and Events Feed by Topic
A
About on SuperTechFans
aimingoo的专栏
aimingoo的专栏
The Cloudflare Blog
H
Heimdal Security Blog
Schneier on Security
Schneier on Security
Engineering at Meta
Engineering at Meta
Google Online Security Blog
Google Online Security Blog
宝玉的分享
宝玉的分享
AI
AI
The GitHub Blog
The GitHub Blog
MongoDB | Blog
MongoDB | Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
The Last Watchdog
The Last Watchdog
T
Troy Hunt's Blog
S
Security @ Cisco Blogs
H
Hacker News: Front Page
F
Fortinet All Blogs
博客园_首页
S
Secure Thoughts
N
News and Events Feed by Topic
P
Proofpoint News Feed
Microsoft Azure Blog
Microsoft Azure Blog
I
InfoQ
Spread Privacy
Spread Privacy
Hacker News - Newest:
Hacker News - Newest: "LLM"
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
Check Point Blog
Hugging Face - Blog
Hugging Face - Blog
Hacker News: Ask HN
Hacker News: Ask HN
C
CXSECURITY Database RSS Feed - CXSecurity.com
酷 壳 – CoolShell
酷 壳 – CoolShell
Stack Overflow Blog
Stack Overflow Blog
L
LINUX DO - 最新话题
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
S
Schneier on Security
Know Your Adversary
Know Your Adversary
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Scott Helme
Scott Helme
P
Privacy & Cybersecurity Law Blog
S
Securelist
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
O
OpenAI News
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
PCI Perspectives
PCI Perspectives
L
LangChain Blog
雷峰网
雷峰网
Security Archives - TechRepublic
Security Archives - TechRepublic
V2EX - 技术
V2EX - 技术

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Переопределение классов ядра Joomla с помощью плагина на примере MVCFactory
sergeytolkac · 2026-05-11 · via Все публикации подряд на Хабре

Время на прочтение16 мин

Охват и читатели272

Туториал

Разработчики сайтов, веб-мастера, рассматривая Joomla как CMS, чаще всего используют компоненты ядра такими, какие они есть. Но компоненты ядра, обеспечивающие CRUD-ы в Joomla, следует рассматривать ещё и как примеры использования Joomla в качестве фреймворка. Иногда реалии проекта таковы, что требуется внести изменения именно в логику классов ядра Joomla. Я покажу это на нескольких примерах: как исхитрялись раньше и какие возможности появились в современных версиях Joomla.

Сразу оговорюсь: речь не о том, чтобы править файлы ядра. Это плохая идея почти всегда. При обновлении Joomla такие изменения будут потеряны, а сопровождать их потом придётся вручную. Речь о другом: как изменить точку создания MVC-классов компонента через плагин и DI-контейнер, не залезая в core-файлы.

А зачем это надо?..

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

Простой пример: мультикатегории, когда материал находится в одной основной категории и нескольких дополнительных. Этого функционала до сих пор нет в составе ядра Joomla, но сама задача востребована. Поэтому сторонние разработчики по-разному решали вопрос: как добавить материалу Joomla несколько категорий, не переписывая весь com_content и не создавая отдельный компонент только ради этой логики.

Мне известно несколько плагинов, которые решали эту задачу через подмену стандартной модели MVC на собственную. В целом это нормальное направление мысли: если список материалов должен учитывать дополнительные категории, то логика выборки действительно живёт в модели. Вопрос только в том, каким способом эту модель подменять в разных поколениях Joomla. Эта статья написана спустя 1,5 года после выхода WT Multicategories плагина мультикатегорий Joomla.

Немного археологии: как раньше работала подмена моделей MVC ядра Joomla 2.5 - Joomla 3.x на onAfterRoute()

В старых решениях для Joomla подмена классов часто делалась следующим образом: системным плагином на событие onAfterRoute подключали файл со своим классом через require_once / include_once. Новое подключение класса файлом происходило раньше, чем ядро доходило до подключения своего класса модели.

Это не фокус, а следствие тогдашнего способа загрузки MVC-классов: ядро сначала проверяло, существует ли нужный PHP-класс, и только потом подключало файл модели из компонента. Если плагин успевал объявить класс с тем же именем, Joomla уже не подключала штатный файл.

Как это работало в Joomla 2.5.x

Для Joomla 2.5 цепочка загрузки моделей MVC была такая:

  • route() вызывал onAfterRoute, а компонент запускался позже в dispatch() через JComponentHelper::renderComponent().

  • Входной файл com_content создавал контроллер.

  • Стандартный display() создавал модель, а JModel::getInstance() сначала делал class_exists($modelClass) и только если класса ещё не было, выполнял require_once файла модели.

Значит, если системный плагин уже объявил ContentModelArticle, ядро файл components/com_content/models/article.php уже не подключало. Такой подход был хрупким, но понятным: нужно было выиграть гонку загрузки класса.

Как это работало в Joomla 3.x

Joomla 3 существовала очень долго, и внутри этой линейки были заметные архитектурные различия. Но общая логика подмены моделей оставалась похожей на Joomla 2.5.

  • Событие onAfterRoute для системных плагинов происходило до запуска активного компонента.

  • Компонент запускался позже через ComponentHelper::renderComponent().

  • Для legacy-MVC по умолчанию использовались механизмы, совместимые со старым подходом.

  • BaseDatabaseModel::getInstance() снова сначала проверял class_exists, и лишь потом подключал файл модели.

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

Древние изощрённые способы замены классов ядра Joomla от особо творческих личностей

Были также и другие, ещё более изощрённые способы: например, плагином переименовывать файл ядра, а на его место класть свой PHP-класс.  И делать это каждый раз, когда класс ядра изменялся после обновления.

Ещё один вариант мне встречался: в состав плагина включались классы оригинальные ядра. На onAfterInitialise() переименовывались классы ядра в $coreClass = $class . 'Core',  свои классы с изменениями наследовались от созданных Core -классов, а имена новых классов равны классам ядра. И дальше, при проверке наличия оригинальных Core-классов, заменялось содержимое файлов классов ядра с помощью file_put_contents. Ну как красиво же и как страшно!

<?php
// Фрагмент одного плагина

protected function overrideClass($class = null)
{
	$classes = array(
		'FileLayout'     => JPATH_ROOT . '/libraries/src/Layout/FileLayout.php',
		'HTMLHelper'     => JPATH_ROOT . '/libraries/src/HTML/HTMLHelper.php',
		'HtmlView'       => JPATH_ROOT . '/libraries/src/MVC/View/HtmlView.php',
		'ModuleHelper'   => JPATH_ROOT . '/libraries/src/Helper/ModuleHelper.php',
		'BaseController' => JPATH_ROOT . '/libraries/src/MVC/Controller/BaseController.php',
	);

	if (!empty($classes[$class]) && !class_exists($class))
	{
		$coreClass = $class . 'Core';
		if (!class_exists($coreClass))
		{
			$path     = Path::clean($classes[$class]);
			$core     = Path::clean(__DIR__ . '/classes/' . $coreClass . '.php');
			$override = Path::clean(__DIR__ . '/classes/' . $class . '.php');
			if (!file_exists($core))
			{
				file_put_contents($core, '');
			}

			$context = file_get_contents($path);
			$context = str_replace('class ' . $class, 'class ' . $coreClass, $context);
			if (file_get_contents($core) !== $context)
			{
				file_put_contents($core, $context);
			}

			require_once $core;
			require_once $override;
		}
	}
}

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

Что изменилось в Joomla 5 / 6+

В Joomla 4+ компоненты работают иначе. Поддержка Joomla 4 уже завершилась, поэтому мы не будем специально фокусироваться на старой версии CMS. Событие onAfterRoute никуда не исчезло, но современные компоненты создают MVC-объекты через MVCFactory. Фабрика использует namespace компонента и собирает полное имя класса: например, Joomla\Component\Content\Site\Model\ArticleModel, а не legacy-класс вида ContentModelArticle.

Поэтому старая идея "подключим файл с таким же именем класса пораньше" перестаёт быть нормальной точкой расширения. У современного компонента есть service provider, дочерний DI-контейнер, зарегистрированные сервисы и фабрика, которая знает, как создавать контроллеры, модели, представления и таблицы.

В Joomla 6.1 современная часть этой цепочки проверяется по следующим файлам ядра:

  • libraries/src/Extension/ExtensionManagerTrait.php - загрузка расширения и события onBeforeExtensionBoot / onAfterExtensionBoot;

  • libraries/src/Extension/Service/Provider/MVCFactory.php - регистрация MVCFactoryInterface в контейнере компонента;

  • libraries/src/MVC/Factory/MVCFactory.php - создание контроллеров, моделей, view и table;

  • libraries/src/Dispatcher/ComponentDispatcherFactory.php и libraries/src/Dispatcher/ComponentDispatcher.php - передача фабрики в диспетчер компонента;

  • libraries/src/MVC/Controller/BaseController.php - создание модели через фабрику контроллера;

  • administrator/components/com_content/services/provider.php - регистрация сервисов компонента ядра на примере материалов Joomla

Что такое DI-контейнер Joomla?

DI-контейнер Joomla хранит правила создания сервисов. В контейнер кладут данные вида ключ -> значение.

Ключ DI-контейнера - чаще всего это интерфейс или имя класса. Например, MVCFactoryInterface::class. Но ключом может быть и строка, и alias.

Значением DI-контейнера может быть готовый объект, но чаще - замыкание, которое создаёт объект в момент вызова $container->get().

Для современного компонента Joomla важнее всего не глобальный контейнер приложения, а дочерний контейнер конкретного компонента. Именно туда service provider компонента регистрирует его фабрики и сам объект компонента.

Что важно: в DI-контейнер компонента Joomla не помещается отдельный сервис для каждой модели, view или table. В контейнер помещается сервис MVCFactoryInterface, а уже фабрика знает, какой MVC-класс создать. Чисто теоретически компонент может использовать DI-контейнер как угодно и помещать туда и модели, но в стандартной архитектуре MVC Joomla таких примеров нет.

Отсюда следует главный вывод: если нужно повлиять на создание модели современного компонента, то подменять надо не файл модели через include, а поведение MVCFactoryInterface в контейнере этого компонента.

Дальше нашим модельным организмом и подопытным кроликом будет компонент материалов Joomla.

Как com_content регистрирует фабрику?

Все компоненты ядра Joomla (кроме com_ajax) работают по этому паттерну. В administrator/components/com_content/services/provider.php Joomla регистрирует несколько service provider-ов:

$container->registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Content'));
$container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Content'));
$container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Content'));
$container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Content'));

Затем регистрируется сам компонент:

$container->set(
    ComponentInterface::class,
    function (Container $container) {
        $component = new ContentComponent($container->get(ComponentDispatcherFactoryInterface::class));

        $component->setRegistry($container->get(Registry::class));
        $component->setMVCFactory($container->get(MVCFactoryInterface::class));
        $component->setCategoryFactory($container->get(CategoryFactoryInterface::class));
        $component->setAssociationExtension($container->get(AssociationExtensionInterface::class));
        $component->setRouterFactory($container->get(RouterFactoryInterface::class));

        return $component;
    }
);

Здесь видно две важные вещи. Во-первых, MVCFactoryInterface живёт в контейнере компонента. Во-вторых, фабрика нужна не только самому объекту компонента через setMVCFactory(), но и ComponentDispatcherFactory. А диспетчер компонента потом создаёт контроллер через эту фабрику.

Где нужно вмешиваться: ExtensionManagerTrait::loadExtension()

Современная загрузка расширения идёт через Joomla\CMS\Extension\ExtensionManagerTrait, метод loadExtension(). Нас интересует порядок действий внутри него:

  1. Joomla проверяет, не загружено ли расширение ранее.

  2. Создаётся дочерний контейнер расширения: $this->getContainer()->createChild().

  3. Диспетчер вызывает событие onBeforeExtensionBoot.

  4. Подключается services/provider.php расширения, и provider регистрирует сервисы в контейнере.

  5. Если provider не зарегистрировал объект расширения, Joomla включает fallback для legacy-компонента, модуля или плагина.

  6. Диспетчер вызывает событие onAfterExtensionBoot.

  7. Только после этого Joomla получает объект расширения из контейнера: $container->get($type).

  8. Если расширение реализует BootableExtensionInterface, вызывается boot($container).

  9. Объект расширения кешируется.

Для нашей задачи это почти готовая инструкция. На onBeforeExtensionBoot дочерний контейнер уже есть, но service provider компонента ещё не выполнился. Значит, MVCFactoryInterface у современного компонента обычно ещё не зарегистрирован. На onAfterExtensionBoot provider уже отработал, но объект компонента ещё не создан. Именно в этот момент можно заменить или расширить MVCFactoryInterface так, чтобы новая фабрика попала и в компонент, и в dispatcher factory.

Как не промахнуться? Для подмены MVCFactoryInterface современного компонента из системного плагина нужно на onAfterExtensionBoot брать контейнер, который пришёл в событии. Именно его использует компонент потом. Глобальный контейнер приложения здесь не тот уровень.

Почему не глобальный контейнер и не поздний setMVCFactory()

На первый взгляд может показаться, что достаточно сделать что-то вроде этого:

$component = $app->bootComponent('com_content');
$component->setMVCFactory(new MyFactory());

Иногда это действительно изменит результат прямого вызова:

$app->bootComponent('com_content')
    ->getMVCFactory()
    ->createModel('Article', 'Site');

Но для обычного рендера компонента этого недостаточно.  ComponentHelper::renderComponent()  вызывает:

$app->bootComponent($option)->getDispatcher($app)->dispatch();

getDispatcher() обращается к ComponentDispatcherFactory, а тот уже хранит фабрику, которую получил при создании объекта компонента. Если компонент уже создан, поздняя замена публичной фабрики через setMVCFactory() не обязана заменить фабрику внутри dispatcher factory.

Поэтому для стандартной MVC-цепочки важно успеть до создания ComponentInterface. В ExtensionManagerTrait::loadExtension() это как раз промежуток между регистрацией services/provider.php и $container->get(ComponentInterface::class), то есть событие onAfterExtensionBoot.

Способ 1. Расширить сервис через $container->extend()

У контейнера Joomla есть метод extend(). Он берёт уже зарегистрированный сервис и заворачивает его в новую фабрику. Это похоже на паттерн "декоратор": у нас остаётся исходный объект, но мы возвращаем наружу обёртку с дополнительным поведением.

Пример условного системного плагина:

<?php

namespace Webtolk\Plugin\System\MVCFactoryOverride\Extension;

\defined('_JEXEC') or die;

use Joomla\CMS\Event\AfterExtensionBootEvent;
use Joomla\CMS\Extension\ComponentInterface;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\DI\Container;
use Joomla\Event\SubscriberInterface;
use Webtolk\Plugin\System\MVCFactoryOverride\MVC\ContentMVCFactoryDecorator;

final class MVCFactoryOverride extends CMSPlugin implements SubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            'onAfterExtensionBoot' => 'onAfterExtensionBoot',
        ];
    }

    public function onAfterExtensionBoot(AfterExtensionBootEvent $event): void
    {
        if ($event->getExtensionType() !== ComponentInterface::class) {
            return;
        }

        if (strtolower($event->getExtensionName()) !== 'content') {
            return;
        }

        // Берём контейнер дочерний! Именно он используется компонентом
        $container = $event->getContainer();

        if (!$container->has(MVCFactoryInterface::class) || $container->isProtected(MVCFactoryInterface::class)) {
            return;
        }

        $container->extend(
            MVCFactoryInterface::class,
            static function (MVCFactoryInterface $factory, Container $container): MVCFactoryInterface {
                return new ContentMVCFactoryDecorator($factory);
            }
        );
    }
}

Обратите внимание на имя расширения: для com_content в событии будет content, без префикса com_. Так приложение вызывает bootComponent() и передаёт имя в loadExtension().

Чтобы современный плагин был создан через service provider, ему нужен  services/provider.php

<?php

\defined('_JEXEC') or die;

use Joomla\CMS\Extension\PluginInterface;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Webtolk\Plugin\System\MVCFactoryOverride\Extension\MVCFactoryOverride;

return new class () implements ServiceProviderInterface {
    public function register(Container $container): void
    {
        $container->set(
            PluginInterface::class,
            // $container->lazy начиная с Joomla 5.4.0.
            // Для Joomla ДО 5.4.0 нужно использовать анонимную функцию-замыкание.
            // Данный пример не будет работать на Joomla 4 и Joomla <5.4.0.
            $container->lazy(MVCFactoryOverride::class, function (Container $container) {
                $plugin = new MVCFactoryOverride(
                    (array) PluginHelper::getPlugin('system', 'mvcfactoryoverride')
                );

                $plugin->setApplication(Factory::getApplication());

                return $plugin;
            })
        );
    }
};

Декоратор фабрики Joomla на минималках

Декоратор реализует тот же MVCFactoryInterface и получает исходную фабрику в конструктор. Это позволяет не создавать заново штатную MVCFactory и не угадывать, какие зависимости Joomla уже передала в неё через service provider.

<?php

namespace Webtolk\Plugin\System\MVCFactoryOverride\MVC;

\defined('_JEXEC') or die;

use Joomla\CMS\Application\CMSApplicationInterface;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\Input\Input;

final class ContentMVCFactoryDecorator implements MVCFactoryInterface
{
    public function __construct(
        private readonly MVCFactoryInterface $inner
    ) {
    }

    public function createController($name, $prefix, array $config, CMSApplicationInterface $app, Input $input)
    {
        return $this->inner->createController($name, $prefix, $config, $app, $input);
    }

    public function createModel($name, $prefix = '', array $config = [])
    {
        if ($name === 'Article' && $prefix === 'Site') {
            // Здесь крутим и вертим модель ядра как нам нужно/
            // Или возвращаем собственный объект модели.
        }

        return $this->inner->createModel($name, $prefix, $config);
    }

    public function createView($name, $prefix = '', $type = '', array $config = [])
    {
        return $this->inner->createView($name, $prefix, $type, $config);
    }

    public function createTable($name, $prefix = '', array $config = [])
    {
        return $this->inner->createTable($name, $prefix, $config);
    }
}

Такой декоратор хорош как безопасная иллюстрация принципа и как рабочий путь для прямых вызовов $component->getMVCFactory()->createModel(...). Но у него есть важная тонкость, которую легко пропустить.

Важная ловушка: createController() и $controller->getModel()

В стандартной цепочке рендера компонента сначала создаётся контроллер. В ComponentDispatcher::getController() вызывается:

$controller = $this->mvcFactory->createController(
    $name,
    $client,
    $config,
    $this->app,
    $this->input
);

Если в контейнере лежит декоратор, этот вызов попадёт в декоратор. Но если декоратор просто делегирует  createController()  во внутреннюю фабрику, дальше вступает в дело  MVCFactory::createController()  ядра:

$controller = new $className($config, $this, $app, $input);

Здесь $this - уже внутренняя фабрика Joomla, а не ваш декоратор. Значит, контроллер сохранит внутри себя исходную фабрику. Когда потом BaseController::getModel() вызовет $this->factory->createModel(), он может пройти мимо вашего декоратора.

Отсюда практический вывод: минимальный декоратор createModel() не всегда достаточен для подмены модели в обычном рендере компонента. Он достаточен для прямых вызовов фабрики, но для контроллерной MVC-цепочки нужно отдельно проверить, какая фабрика попала в контроллер.

Теоретически можно сделать декоратор, который сам создаёт контроллер и передаёт в его конструктор именно себя. Но тогда вы начинаете повторять часть логики MVCFactory::createController(): вычисление класса, создание объекта, передачу зависимостей вроде form factory, dispatcher, router, cache controller, user factory, mailer factory и logger. В ядре часть этой инициализации находится в private-методах MVCFactory, поэтому аккуратно повторить её снаружи непросто.

Использовать рефлексию (reflection), чтобы после создания контроллера заменить protected-свойство factory, технически возможно, но это уже не хороший расширяемый API, а зависимость от внутреннего устройства BaseController

Способ 2. Полностью заменить MVCFactoryInterface через $container->set()

Другой путь - не декорировать фабрику, а зарегистрировать вместо неё свою. Например, наследоваться от Joomla\CMS\MVC\Factory\MVCFactory и переопределить getClassName(). Тогда все обычные методы createController()createModel()createView() и createTable() останутся из ядра, но имя класса можно будет заменить.

$container->set(
    MVCFactoryInterface::class,
    static function (Container $container) use ($extensionName) {
        $factory = new class ('Joomla\\Component\\' . ucfirst($extensionName)) extends MVCFactory {
            protected function getClassName(string $suffix, string $prefix)
            {
                $class = parent::getClassName($suffix, $prefix);

                return match ($class) {
                    \Joomla\Component\Content\Site\Model\ArticleModel::class
                        => \Webtolk\Plugin\System\MVCFactoryOverride\Model\ArticleModel::class,
                    default => $class,
                };
            }
        };

        // И мы помним, что фабрике нужны все зависимости, которые мы видим
        // в provider.php компонента. Устанавливаем их здесь.
        $factory->setFormFactory($container->get(FormFactoryInterface::class));
        $factory->setDispatcher($container->get(DispatcherInterface::class));
        $factory->setDatabase($container->get(DatabaseInterface::class));
        $factory->setSiteRouter($container->get(SiteRouter::class));
        $factory->setCacheControllerFactory($container->get(CacheControllerFactoryInterface::class));
        $factory->setUserFactory($container->get(UserFactoryInterface::class));
        $factory->setMailerFactory($container->get(MailerFactoryInterface::class));

        return $factory;
    }
);

Плюс этого подхода в том, что контроллер будет создан этой же фабрикой, а значит при последующем $controller->getModel() он снова обратится к ней. Для задачи "заменить вот эти модели ядра на мои классы" это понятная и прямая схема.

Минус тоже очевиден: теперь вы повторяете код из Joomla\CMS\Extension\Service\Provider\MVCFactory. В Joomla 6.1 provider выбирает между MVCFactory и ApiMVCFactory в зависимости от типа приложения и передаёт в фабрику несколько зависимостей: FormFactoryInterfaceDispatcherInterfaceDatabaseInterfaceSiteRouterCacheControllerFactoryInterfaceUserFactoryInterfaceMailerFactoryInterface. Если в будущей версии Joomla этот набор изменится, ваш код нужно будет проверить и обновить.

Что и как использовать? extend() меньше привязан к внутренней сборке фабрики, но минимальный декоратор может не перехватить модели, создаваемые через контроллер. Полная замена через set() лучше подходит для сопоставления "штатный класс - мой класс", но требует аккуратно повторять создание и настройку фабрики.

Если компонент ваш собственный - плагин обычно не нужен

Да, и всю эту статью можно было не читать. Можно просто написать сразу вменяемую модель и/или другие классы компонента. Компонент сам объявляет свою инфраструктуру, и вам не нужно зависеть от порядка системных плагинов.

Что с legacy-компонентами

Не каждый компонент в реальной жизни уже живёт в новой архитектуре. Если у компонента нет services/provider.php и Joomla включает fallback, создаётся LegacyComponent. Такой объект умеет вернуть LegacyFactory через getMVCFactory(), но это не тот же сценарий, где service provider компонента зарегистрировал MVCFactoryInterface в дочернем контейнере.

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

  • это действительно компонент, а не модуль или плагин;

  • имя компонента то, которое вам нужно;

  • в контейнере есть MVCFactoryInterface::class;

  • сервис не protected;

  • вы вмешиваетесь до создания ComponentInterface.

Что будет, если несколько плагинов Joomla меняют одну фабрику?

Это риск. Системные плагины выполняются в определённом порядке, установленным в админке сайта. Также на их выполнение влияет приоритет плагина, указанный в коде самого плагина для каждого конкретного триггера. Поэтому если несколько плагинов вмешиваются в MVCFactoryInterface одного и того же компонента, результат зависит от выбранного способа.

  • Если несколько плагинов делают $container->set(MVCFactoryInterface::class, ...), фактически выигрывает тот, кто перезаписал сервис последним. Дворовая шутка из за гаражей вспоминается: "Кто последний, тот и папа".

  • Если несколько плагинов делают $container->extend(), может получиться цепочка декораторов. Что само по себе не страшно, так как паттерн декоратор для этого и придуман был. Главное, что он не во всех сценариях может гарантированно отработать.

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

Поэтому хороший плагин должен ограничивать область вмешательства: только нужный компонент, только нужный client, только нужная модель или table. Не стоит превращать MVCFactory в универсальный перехватчик всего сайта. И в целом нужно помнить. что MVCFactory может работать в разных типах приложения (сайт, админка, CLI, REST API и даже Daemon-приложение). И системные плагины тоже очень острый инструмент.

Когда нужна подмена MVCFactory, а когда - нет?

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

Если у вас свой компонент - вам не нужна подмена MVCFactory таким изощрённым способом и эта статья тоже.

Полезные ресурсы Joomla

Эта статья на сайте автора.

Ресурсы сообщества:

Telegram:

Max