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 thepreprocess
method yourself.Note The
preprocess
method is called before the SEF URL is built and, crucially, before Joomla tries to figure out theformat
andItemid
URL 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
format
andItemid
parameters in your router'sbuild
method. While this was necessary for compatibility with legacyrouter.php
files (those using the two distinct functions), this “hack” will no longer work in Joomla 4. You must move that code into thepreprocess
method. If you fail to do so, your SEF URLs will not work properly; from your perspective, it will be as if yourformat
andItemid
values 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 Something
Segmentget
where Something
IdSomething
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.
Something
Segmentitem/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.Something
Id
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.