На основе официальной документации Joomla и практического опыта отладки, вот исчерпывающий набор файлов и правил, который закрывает все известные подводные камни.
Архитектурная схема (что происходит внутри Joomla)
Прежде чем писать код, поймите цепочку:
Route::_('index.php?option=com_example&view=item&id=5')вызываетSiteRouter.SiteRouterпроверяет, реализует ли класс компонентаRouterServiceInterface. Если нет — используетсяRouterLegacy, который не умеет строить SEF.- Если реализует — вызывается
$component->createRouter($app, $menu). RouterServiceTrait::createRouter()берёт сохранённыйRouterFactoryи вызывает$this->routerFactory->createRouter($app, $menu).RouterFactoryформирует имя класса:<namespace>\Site\Service\Router(на фронте) или<namespace>\Administrator\Service\Router(в админке).- Если класс существует — создаётся экземпляр
Router, который реализуетRouterInterface(или extendsRouterView). SiteRouterвызываетpreprocess()→build()(для генерации URL) илиparse()(для разбора URL).
Ключевой вывод: без implements RouterServiceInterface на классе компонента вся цепочка рвётся на шаге 2.
Структура файлов
administrator/components/com_example/
├── services/
│ └── provider.php
└── src/
└── Extension/
└── ExampleComponent.php
components/com_example/
├── services/
│ └── provider.php (дубль, Joomla может игнорировать)
└── src/
└── Service/
└── Router.php
1. administrator/components/com_example/services/provider.php
Регистрирует DI-сервисы. Должен быть в админской части — Joomla 5 использует его и на фронтенде.
<?php
\defined('_JEXEC') or die;
use Joomla\CMS\Component\Router\RouterFactoryInterface;
use Joomla\CMS\Dispatcher\ComponentDispatcherFactoryInterface;
use Joomla\CMS\Extension\ComponentInterface;
use Joomla\CMS\Extension\Service\Provider\ComponentDispatcherFactory;
use Joomla\CMS\Extension\Service\Provider\MVCFactory;
use Joomla\CMS\Extension\Service\Provider\RouterFactory;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
return new class implements ServiceProviderInterface
{
public function register(Container $container)
{
$namespace = '\\Vendor\\Component\\Example';
$container->registerServiceProvider(new ComponentDispatcherFactory($namespace));
$container->registerServiceProvider(new MVCFactory($namespace));
$container->registerServiceProvider(new RouterFactory($namespace));
$container->set(
ComponentInterface::class,
function (Container $container) {
$component = new \Vendor\Component\Example\Administrator\Extension\ExampleComponent(
$container->get(ComponentDispatcherFactoryInterface::class)
);
$component->setMVCFactory($container->get(MVCFactoryInterface::class));
$component->setRouterFactory($container->get(RouterFactoryInterface::class));
return $component;
}
);
}
};
Внимание: RouterFactory здесь — это провайдер Joomla\CMS\Extension\Service\Provider\RouterFactory, а не сам класс фабрики. Он регистрирует в контейнере RouterFactoryInterface, который при получении создаёт настоящий Joomla\CMS\Component\Router\RouterFactory.
2. administrator/components/com_example/src/Extension/ExampleComponent.php
Критическая строка — implements RouterServiceInterface. Без неё SiteRouter не вызовет createRouter().
<?php
namespace Vendor\Component\Example\Administrator\Extension;
\defined('_JEXEC') or die;
use Joomla\CMS\Component\Router\RouterServiceInterface;
use Joomla\CMS\Component\Router\RouterServiceTrait;
use Joomla\CMS\Extension\MVCComponent;
use Joomla\CMS\MVC\Factory\MVCFactoryServiceTrait;
class ExampleComponent extends MVCComponent implements RouterServiceInterface
{
use MVCFactoryServiceTrait;
use RouterServiceTrait;
}
RouterServiceTrait даёт методы createRouter() и setRouterFactory(), но не объявляет implements RouterServiceInterface. Это нужно сделать явно.
3. components/com_example/services/provider.php
Дублирует админский провайдер. Joomla 5 на фронтенде может использовать админский provider.php, но наличие дубля не мешает и соответствует структуре пакета.
<?php
// Идентичное содержимое с administrator/components/com_example/services/provider.php
4. components/com_example/src/Service/Router.php
Выбор подхода: RouterInterface или RouterView
RouterView — если структура повторяет com_content (категории → элементы, вложенные URL). Даёт готовые MenuRules и StandardRules.
RouterInterface — если структура простая (один список, одна карточка). Проще, предсказуемее, легче отлаживать.
Для простого компонента используйте RouterInterface:
<?php
namespace Vendor\Component\Example\Site\Service;
\defined('_JEXEC') or die;
use Joomla\CMS\Application\SiteApplication;
use Joomla\CMS\Categories\CategoryFactoryInterface;
use Joomla\CMS\Component\Router\RouterInterface;
use Joomla\CMS\Menu\AbstractMenu;
use Joomla\Database\DatabaseInterface;
use Joomla\Database\ParameterType;
class Router implements RouterInterface
{
private SiteApplication $app;
private AbstractMenu $menu;
private ?DatabaseInterface $db;
public function __construct(
SiteApplication $app,
AbstractMenu $menu,
?CategoryFactoryInterface $categoryFactory = null,
?DatabaseInterface $db = null
) {
$this->app = $app;
$this->menu = $menu;
$this->db = $db;
}
public function build(&$query)
{
$segments = [];
if (!isset($query['view'])) {
return $segments;
}
$view = $query['view'];
if ($view === 'item' && isset($query['id'])) {
$segments[] = 'item-' . (int) $query['id'];
unset($query['id'], $query['view']);
} elseif ($view === 'items') {
unset($query['view']);
}
return $segments;
}
public function parse(&$segments)
{
$vars = [];
if (empty($segments)) {
return $vars;
}
$first = array_shift($segments);
if (preg_match('/^item-(\d+)$/', $first, $m)) {
$vars['view'] = 'item';
$vars['id'] = (int) $m[1];
}
return $vars;
}
public function preprocess($query)
{
if (!empty($query['Itemid'])) {
return $query;
}
if (isset($query['view'])) {
// Для карточки элемента привязываем к пункту меню списка
$lookupView = ($query['view'] === 'item') ? 'items' : $query['view'];
$itemid = $this->findMenuId($lookupView);
if ($itemid > 0) {
$query['Itemid'] = $itemid;
}
}
return $query;
}
private function findMenuId(string $view): int
{
$db = $this->db;
$q = $db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__menu'))
->where($db->quoteName('link') . ' LIKE :link')
->where($db->quoteName('published') . ' = 1')
->where($db->quoteName('client_id') . ' = 0')
->setLimit(1);
// PHP 8.1: второй аргумент bind() должен быть переменной
$link = '%view=' . $view . '%';
$q->bind(':link', $link, ParameterType::STRING);
$db->setQuery($q);
return (int) $db->loadResult();
}
}
Частые ошибки и их точные причины
1. ?view= остаётся в URL, SEF не работает
Причина: класс компонента не реализует RouterServiceInterface. RouterServiceTrait сам по себе не включает instanceof-проверку в SiteRouter.
Решение: добавить implements RouterServiceInterface.
2. bind(): Argument #2 could not be passed by reference
Причина: PHP 8.1 требует, чтобы второй аргумент bind() был переменной. Конкатенация '%view=' . $view . '%' передаётся как выражение и не может быть ссылкой.
Решение:
$link = '%view=' . $view . '%';
$q->bind(':link', $link, ParameterType::STRING);
3. Карточка открывается по /item/item-5 вместо /items/item-5
Причина: preprocess() подставил Itemid пункта меню карточки (item), а не списка (items). Joomla привязывает URL к пункту меню с alias item → /item/....
Решение: для вложенного view искать Itemid родительского пункта меню:
$lookupView = ($query['view'] === 'item') ? 'items' : $query['view'];
4. class_exists('...Router') = true, но роутер не работает
Причина: class_exists проверяет только наличие класса. SiteRouter проверяет instanceof RouterServiceInterface на компоненте, а не на роутере. Если компонент не реализует интерфейс — роутер не будет вызван, даже если класс существует.
5. Роутер не вызывается, хотя файлы на месте
Проверьте:
provider.phpлежит вadministrator/components/com_example/services/(не только на фронте).- В
#__extensionsдля компонентаmanifest_cacheсодержит"namespace":"Vendor\\Component\\Example". administrator/cache/autoload_psr4.phpсодержит префиксVendor\Component\Example\Site\→components/com_example/src.
6. Изменения не подхватываются
Решение: очистить кэш Joomla (System → Maintenance → Clear Cache) и удалить administrator/cache/autoload_psr4.php (Joomla пересоберёт при следующем запросе).
Быстрая диагностика (если роутер не работает)
Добавьте временный лог в preprocess():
public function preprocess($query)
{
file_put_contents(JPATH_ROOT . '/router-debug.log', date('c') . ' preprocess: ' . json_encode($query) . "\n", FILE_APPEND);
// ...
}
Откройте страницу. Если router-debug.log не появился — preprocess() не вызывается. Проверьте:
implements RouterServiceInterfaceна компоненте.provider.phpв админке.- Кэш очищен.
Если preprocess вызывается, но build — нет, проверьте, что preprocess возвращает $query с Itemid.
Когда использовать RouterView
RouterView с MenuRules и StandardRules подходит, если компонент повторяет com_content (категории → элементы). Пример из документации:
class Router extends RouterView
{
public function __construct(SiteApplication $app, AbstractMenu $menu, DatabaseInterface $db)
{
$items = new RouterViewConfiguration('items');
$this->registerView($items);
$item = new RouterViewConfiguration('item');
$item->setKey('id')->setParent($items);
$this->registerView($item);
parent::__construct($app, $menu);
$this->attachRule(new MenuRules($this));
$this->attachRule(new StandardRules($this));
}
public function getItemSegment($id, $query) { /* ... */ }
public function getItemId($segment, $query) { /* ... */ }
}
Предупреждение из документации: если RouterView не заработает, отладка практически невозможна. Для простых компонентов используйте ручной RouterInterface.
Чеклист перед запуском
provider.phpсуществует вadministrator/components/com_example/services/RouterFactoryзарегистрирован вprovider.phpsetRouterFactory()вызывается в closureComponentInterface- Класс компонента
implements RouterServiceInterface Router.phpвcomponents/com_example/src/Service/- Namespace в
Router.php—Vendor\Component\Example\Site\Service RouterреализуетRouterInterface(или extendsRouterView)bind()использует переменную, а не выражение- Для вложенного view
preprocess()ищет Itemid родительского пункта меню - Кэш очищен (
administrator/cache/autoload_psr4.phpудалён) manifest_cacheв#__extensionsсодержит"namespace"
При соблюдении этих пунктов костяк компонента с роутером создаётся за одну итерацию.
Автор статьи — ИИ, в партнёрстве с Геннадием Едигом. Сайт: http://jedig.ru