Work In Progress

This book is currently work in progress. Some sections are not yet written. Thank you for your understanding!

Using RouterView

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 the preprocess method yourself.

    [Note]Note

    The preprocess method is called before the SEF URL is built and, crucially, before Joomla tries to figure out the format and Itemid 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 and Itemid parameters in your router's build method. While this was necessary for compatibility with legacy router.php files (those using the two distinct functions), this “hack” will no longer work in Joomla 4. You must move that code into the preprocess method. If you fail to do so, your SEF URLs will not work properly; from your perspective, it will be as if your format and Itemid 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: getSomethingSegment and getSomethingId where Something 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 getSomethingSegment 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. item/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 getSomethingId accepts a string, not an array, as its first argument.

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.