Introduced in Joomla 3.4, the new component router tries to solve in a modern way the issue of building nice sef urls with Joomla.
You can find a basic explanation of how to implement on the official Joomla Documention. However, as I was working on implementing it for Tracks, I ran into some issues with the way to build urls in a nice hierarchical way.
Tracks use case
Tracks component aims at displaying the results of racing type competition, so typically, I want to be able to display the ranking, teamn rankings, and results of a same project.
the non sef url will be something like:
- index.php?option=com_tracks&view=project&p=1 (project 1 landing page, with each event winners)
- index.php?option=com_tracks&view=ranking&p=1 (project 1 ranking)
- index.php?option=com_tracks&view=teamranking&p=1 (project 1 team ranking)
- index.php?option=com_tracks&view=roundresult&id=43&p=1 (results of round 43 for project 1)
Note that for the latter entry, we can do wthout the 'p=1', as 'id' is enough to get the results, and associated project, but I added it to work with the router 'parent' hierarchy...
Adding the projects and project view
so first, lets add the routing for the project view in the constructor:
$projects = new JComponentRouterViewconfiguration('projects'); $this->registerView($projects); $project = new JComponentRouterViewconfiguration('project'); $project->setKey('p')->setParent($projects); $this->registerView($project);
We make 'project' inherit from 'projects', so that if we don't want to create a menu item for every project, we can just just create one for the 'projects' view (and select to 'not display' it in menu item if that is not what you want) to get a base for our sef urls (and an Itemid). Let's say i call this view 'all projects', i will now get for example '/all-projects/formula1-2019' for my formula1 2019 project.
If you later create a specific menu item for a project, the menu item alias will be used instead, for exemple '/f1-2019'
For this to work, we need to implement the function to add a segment to the sef url when building it, and another function to restore the variables when parsing it (here the 'entity' object is a kind of internal model, adapt to your own code to get the alias of your project):
public function getProjectSegment($id, $query) { $entity = TrackslibEntityProject::load($id); if ($entity->isValid()) { return [(int) $id => $entity->alias]; } return []; } public function getProjectId($segment, $query) { $table = Table::getInstance('Project', 'TracksTable'); $table->load(['alias' => $segment]); return $table->id; }
The first function replaces the $id with the alias of the project, the second function does the opposite, looks up the id associated for the alias, and returns it.
Adding the ranking view as a child of the project view
All good for now, so let's add the ranking view. What I want, is to build on the previous view hierarchy, and get an url like '/all-projects/formula1-2019/ranking'. Let's start by adding the route to the constructor
$ranking = new JComponentRouterViewconfiguration('ranking'); $ranking->setKey('p')->setParent($project, 'p'); $this->registerView($ranking);
This assumes we will be using the url as I wrote earlier. We create the ranking view with a key (i.e. url get variable) 'p', and we use the same key to link to the parent 'project' view.
Now, it's time to implement the segment setter and parser. We don't need to use an alias in the setter, as the parent view 'project' is already taking care of that part. However, we still need to add something to the url, or the router won't know for sure how to associate the segments back to the view
const SEGMENT_RANKING = 'ranking'; ..... public function getRankingSegment($id, $query) { return [(int) $id => self::SEGMENT_RANKING]; }
For parsing, we need to make sure the segment is correct, and return the project id so that it gets associated to the parsed query variables. It is important to check the segment, because of the way the router works: when parsing each segment, it checks for all possible 'children' of the current view if they are a match, and to do so, it asks the get<View>Id to either return the id, or false if the view is not the right one. In our case, we currently only have the 'ranking' view as a child of the project view, but we will add others later, so it's important that they don't conflict.
public function getRankingId($segment, $query) { if ($segment == self::SEGMENT_RANKING) { return $query['p']; } return false; }
Adding the team ranking view as another child of the project view
The method is exactly the same as for the ranking view, and it work as well for other views not adding another id. Note that you can't 'not' add the setKey() to the view, as in that case the id would be stripped out of the parse query.
$teamranking = new JComponentRouterViewconfiguration('teamranking'); $teamranking->setKey('p')->setParent($project, 'p'); $this->registerView($teamranking);
public function getTeamrankingSegment($id, $query) { return [(int) $id => self::SEGMENT_TEAMRANKING]; }
public function getTeamrankingId($segment, $query) { if ($segment == self::SEGMENT_TEAMRANKING) { return $query['p']; } return false; }
Adding the round result view as child of the project view, but using it's own id
As we added the 'p' variable in the url to identify the project, adding the view in the constructor is similar to the ranking view
$roundresult = new JComponentRouterViewconfiguration('roundresult'); $roundresult->setKey('id')->setParent($project, 'p'); $this->registerView($roundresult);
But this time, when building the segment part, we need a way to identify the view and the id at the same time. So, what we do is that we add a constant part like to the ranking and teamranking part, plus a slug for the id of the round. in the hierarchy of tracks, round result don't have their own alias, so i have to keep the id in the final sef url, but i add the round name slug to make it niced (there could be 2 times the same 'round' in the same project, so i can't use the alias only).
const SEGMENT_ROUNDRESULT = 'result';
public function getRoundResultSegment($id, $query) { $entity = TrackslibEntityProjectround::load((int) $id); if ($entity->isValid()) { return [(int) $id => self::SEGMENT_ROUNDRESULT . '-' . (int) $id . '-' . $entity->getRound()->alias]; } return []; }
public function getRoundresultId($segment, $query) { if (strpos($segment, self::SEGMENT_ROUNDRESULT . '-') !== 0) { return false; } return (int) substr($segment, strlen(self::SEGMENT_ROUNDRESULT) + 1); }
And that's it, this time we will get urls of the type '/all-projects/formula1-2019/result-456-australian-grand-prix'
You can see it in action here
I hope this can help other developers trying to add the new router to their extension. It turns out to be working pretty well after all.