If you are using a router class extending from
Joomla\CMS\Component\Router\RouterView it is
fairly easy to create a router for most components.
The constructor of your class tells Joomla which views of your component can be routed in the frontend and the relation to each other. For example:
public function __construct(SiteApplication $app = null, AbstractMenu $menu = null)
{
$welcome = new \Joomla\CMS\Component\Router\RouterViewConfiguration('welcome');
$this->registerView($welcome);
$item = (new \Joomla\CMS\Component\Router\RouterViewConfiguration('item'))
->setKey('id')
->addLayout('default')
->addLayout('fancy');
$this->registerView($item);
$detail = (new \Joomla\CMS\Component\Router\RouterViewConfiguration('detail'))
->setKey('id')
->setParent($item, 'itemid');
$this->registerView($detail);
parent::__construct($app, $menu);
$this->attachRule(new \Joomla\CMS\Component\Router\Rules\MenuRules($this));
$this->attachRule(new \Joomla\CMS\Component\Router\Rules\StandardRules($this));
$this->attachRule(new \Joomla\CMS\Component\Router\Rules\NomenuRules($this));
}
This tells Joomla that we have three routable views called
welcome, item and detail. The
detail view is a child of item.
How would Joomla know about which detail is under a specific item?
We told it that the detail's itemid property
must match the key of the item and the key of the
item is called id.
The last three lines tell Joomla which routing rules to register:
-
MenuRules. Tries to detect the correct Itemid for a view if none was provided in the non-SEF URL. You should keep that unless you want to implement thepreprocessmethod yourself.![[Note]](/media/com_docimport/admonition/note.png)
Note The
preprocessmethod is called before the SEF URL is built and, crucially, before Joomla tries to figure out theformatandItemidURL parameters to send to your component's Router's build method.In Joomla 3 you could get away with trying to figure out and change the
formatandItemidparameters in your router'sbuildmethod. While this was necessary for compatibility with legacyrouter.phpfiles (those using the two distinct functions), this “hack” will no longer work in Joomla 4. You must move that code into thepreprocessmethod. If you fail to do so, your SEF URLs will not work properly; from your perspective, it will be as if yourformatandItemidvalues overridden in the built method were never taken into account. That's exactly what happens.This is actually a good change. It makes the core routing code more efficient and keeps us third party developers in the habit of applying separation of concerns in our code.
-
StandardRules. Standard non-SEF to SEF URL (and vice-versa) routing. If you omit this you will not get any SEF URL routing which beats the purpose of having a router. -
NomenuRules. Process URLs when no Itemid exists for the component. This is necessary to route URLs when no published menu items exist for your component and Joomla needs to create or parse URLs in the format/component/example/foo/bar.html.
At this point Joomla knows the logical hierarchy of our component's views but it does not know how to convert an id in the non-SEF URL to a SEF URL segment when building the SEF URL (e.g. convert an item ID to its alias) or how to convert a SEF URL segment back to a non-SEF URL's numeric ID when it's parsing the SEF URL (e.g. convert an item alias to its ID). This is up to us.
We need to provide two methods for each
RouterViewConfiguration objects we created:
get
and SomethingSegmentget
where SomethingIdSomething is the name of the view with
its first letter capitalised. Here's what the code for the Item view
would look like in our example:
public function getItemId(string $segment, array $query): bool|int
{
$db = \Joomla\CMS\Factory::getContainer()->get('DatabaseDriver');
$dbQuery = $db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__example_items'))
->where($db->quoteName('alias') . ' = :alias')
->bind(':alias', $segment);
return $db->setQuery($dbQuery)->loadResult() ?: false;
}
public function getItemSegment(int $id, array $query): array
{
$db = \Joomla\CMS\Factory::getContainer()->get('DatabaseDriver');
$dbQuery = $db->getQuery(true)
->select($db->quoteName('alias'))
->from($db->quoteName('#__example_items'))
->where($db->quoteName('id') . ' = :id')
->bind(':id', $id);
$segment = $db->setQuery($dbQuery)->loadResult() ?: null;
if ($segment === null) {
return [];
}
return [$segment];
}
There is something worth noting here. The
get
method can return an array with more than one segments. This is useful
if you want to somehow return a more complex structure e.g.
SomethingSegmentitem/foo/detail/bar where item and
detail are fixed strings. However, if you do that, you will
need to override the parse method to handle
multi-segment views. The default implementation in
StandardRules assumes that you are using exactly
one segment per view and that's why
get
accepts a string, not an array, as its first argument.SomethingId
You may wonder, what about our welcome view? Don't we
need to create methods for it? No, we don't. Views which do not have a
key set for them use the name of the view as the (only) segment for SEF
URLs. If there is a naming clash between such a view and an alias of a
top-level view (or category, as we will see below) the first
RouterViewConfiguration object registered “wins”
in determining how that segment should be parsed. As a result, you
should keep this kind of top-level views to a minimum and either inform
users that they cannot use these aliases or actively prevent them with
validation rules whenever possible.