Support des URL SEF dans votre composant
From Joomla! Documentation
Notez que de nombreux exemples en ligne utilisent des classes avec espace de nom. Cela a été introduit dans Joomla 3.8. Il existe des versions sans espace de nom de toutes ces classes qui fonctionneront dans Joomla 3.4+.
La réécriture d'URL en clair (SEF Search Engine Friendly), lisible par l'utilisateur ou URL propres sont des URLs compréhensibles aussi bien par les utilisateurs que par les moteurs de recherche car elles expliquent le chemin d'accès de la page vers laquelle elles pointent. Depuis la version Joomla! 1.5, Joomla! est capable de créer et d'analyser tout format d'URL, y compris les URL SEF. Cela ne dépend pas de la réécriture d'URL exécutée par le serveur, de sorte que cela fonctionne également si Joomla! n'est pas exécuté sur un serveur Apache utilisant le module mod_rewrite. Les URL SEF répondent à une certaine forme fixe, mais l'utilisateur peut définir un court texte descriptif (alias) pour chaque segment de l'URL.
En interne, la partie d'une URL SEF (la partie située après le nom de domaine) est appelée route. La création et le traitement des URLs SEF sont donc appelés le routage, et le code correspondant, un routeur.
Dans Joomla !, chaque composant est responsable de la gestion de ses propres URL SEF. Par conséquent, en tant que développeur d’un composant, vous devrez créer votre propre routeur pour permettre à votre composant d’utiliser les URL SEF.
Le concept
En supposant que vous suiviez les pratiques de développement standard, votre composant utilise probablement des "URL système" qui ressemblent beaucoup à http://www.example.com/index.php?option=com_yourcomponent&view=article&id=1&catid=20&Itemid=50, et votre objectif est de transformer ceci en http://www.example.com/example-menu-item/example-category/example-article. En tant que développeur, vous avez deux tâches: signaler au système que certains éléments de texte sont des URL et qu’ils doivent être transformés, et expliquer au système comment transformer les URL.
Appliquer Route::_
Il est difficile et inefficace pour Joomla! de savoir quelles parties de votre composant en sortie sont des URL. Pour prendre en charge les URL SEF, vous devez modifier le code générateur d’URL afin qu’il applique \Joomla\CMS\Router\Route::_
avant de générer l’URL:
echo \Joomla\CMS\Router\Route::_('index.php?view=article&id=1&catid=20');
Notez qu'il est possible d'omettre les paramètres option
et Itemid
. option
est le nom par défaut du composant en cours d'exécution et Itemid
par défaut, l'ID de l'élément de menu actuel.
En règle générale, vous ne devriez appliquer cela qu'aux URL que les utilisateurs et/ou les moteurs de recherche sont en mesure de voir. Par exemple, il n'est pas nécessaire de transformer les URL utilisées dans les redirections qui entraînent immédiatement d'autres redirections.
Si l'utilisateur désactive les URL SEF dans les paramètres du site, \Joomla\CMS\Router\Route::_
générera des URL de travail non-SEF sans aucune modification du code.
Ecrire un système de routage
Vous aurez également besoin d'écrire un routeur, qui est un fichier unique contenant une classe avec trois fonctions qui convertissent les URL système en et depuis les URL SEF. Ce fichier doit être placé dans /components/com_yourcomponent/router.php.
La classe doit s'appeler [NomDuComposant]Router
(par exemple, pour com_content ContentRouter
) et doit implémenter l'interface Joomla\CMS\Component\Router\RouterInterface
La première fonction, build(&$query)
, doit transformer un tableau de paramètres d'URL en un tableau de segments qui formeront l'URL SEF. Schématiquement, la transformation fonctionne comme suit:
- http://www.example.com/index.php?option=com_yourcomponent&view=article&id=1&catid=20&Itemid=50
- ↓
\Joomla\CMS\Router\Route::_
, appelé par votre composant ou toute autre extension
- ↓
$query = array('view' => 'article', 'id' => 1, 'catid' => 20)
- ↓ Votre code du routeur
[componentname]Router::build
- ↓ Votre code du routeur
$segments = array(20, 1);
- ↓ Construction du routage interne de Joomla (pour l'affichage)
- http://www.example.com/example-menu-item/20/1
La deuxième fonction, parse($segments)
, doit transformer un tableau de segments en un tableau de paramètres URL.
Schématiquement, la transformation fonctionne comme suit:
- http://www.example.com/example-menu-item/20/1
- ↓ Analyse du routage interne de Joomla
$segments = array(20, 1);
- ↓ Votre code du routeur
[componentname]Router::parse
- ↓ Votre code du routeur
$query = array('view' => 'article', 'id' => 1, 'catid' => 20)
Les deux fonctions doivent coopérer de manière à pouvoir reconstruire l'URL d'origine. Vous pouvez considérer la méthode build
comme une forme d' encodage et la méthode parse
comme la fonction de décodage correspondant. Lorsque l'URL d'origine n'est pas correctement reproduite, votre composant cesse de fonctionner.
La fonction finale, preprocess($query)
, est une méthode de préparation des URL. Cette méthode est exécutée sur chaque URL, que le mode SEF soit activé ou non. Nous reviendrons sur cette méthode plus tard dans le tutoriel.
Préparer vos données pour le routage
Clairement, tout format d'URL doit contenir un type d'informations identifiant les données que vous souhaitez afficher. Si les URL de votre système ressemblent à http://www.example.com/index.php?option=com_yourcomponent&view=article&id=1&catid=20&Itemid=50, cette information correspond actuellement au paramètre d'URL id (id=1). Vous voulez probablement que vos URL SEF contiennent une description textuelle des données auxquelles elles pointent. Dans Joomla !, cela se fait généralement en donnant à vos utilisateurs un moyen de saisir un alias à utiliser dans l'URL.
L'alias
Even if your users can enter an alias, they might not do so, leaving the generation of a sensible alias up to your component. If your data has a title field, you can use that as a candidate for the alias (like the core Content component does).
Considering that the alias will be used in URLs, it has to be URL safe. Joomla! provides a method making arbitrary strings URI safe, which includes replacing accented UTF8 characters by their ASCII7 equivalents, white spaces by hyphens, etc. Whether the user entered the alias or a candidate has been chosen automatically, you should ensure that the above requirements for a URL safe alias are met. A good place for implementing this, if you are using JTable
, is the JTable::check()
method, which is called during the save process. Have a look at this example code:
function check()
{
jimport('joomla.filter.output');
if (empty($this->alias))
{
$this->alias = $this->title;
}
$this->alias = JFilterOutput::stringURLSafe($this->alias);
/* All your other checks */
return true;
}
If the alias field is empty the title is used as alias. Then the alias is made URL safe using the JFilterOutput::stringURLSafe()
method.
Le Jeton
A slug is used to minimise the amount of code you need to support SEF URLs. It consists of the numerical identifier (id) your system URLs used, a colon (:), and the alias you created as described above.
Consider a SEF URL for an Article with id 1 and title "Welcome to Joomla!". The automatically generated alias for this article is welcome-to-joomla, and the slug becomes 1:welcome-to-joomla. In the Content component, the two elements are combined during the database query in the model (a
represents #__content
):
$query = 'SELECT a.*, '.
'CASE WHEN CHAR_LENGTH(a.alias) THEN CONCAT_WS(":", a.id, a.alias) ELSE a.id END as slug,'
/*...*/;
The advantage of this method of creating a slug is that you can simply use the slug as a drop-in replacement for the id in most places. For example, you don't need to check for and remove the colon and the alias from the request data manually: if you use JInput's int
(integer) filter, it will do that automatically.
URL de routage
The \Joomla\CMS\Router\Route::_
method translates the internal Joomla! URL to a custom URL. \Joomla\CMS\Router\Route::_
has three parameters and its prototype is:
\Joomla\CMS\Router\Route::_($url, $xhtml = true, $ssl = null);
où :
$url
is a string containing the absolute or relative internal Joomla! URL.$xhtml
is a boolean value that specifies whether or not the output should be in XHTML. This parameter is optional and if omitted defaults to true.$ssl
is an integer value that specifies whether the URI should be secure. It should be set to 1 to force the URI to be secure using the global secure site URI, 0 to leave it in the same state as when it was passed, and -1 to force the URI to be unsecure using the global unsecure site URI.
The most important parameter is $url
. A call to this method might look like:
\Joomla\CMS\Router\Route::_('index.php?view=article&id=' . $row->slug);
$row->slug
is the value that was generated in step 2 from a combination of id and title alias.
Another advantage of using \Joomla\CMS\Router\Route::_
is that the router now handles $option
(the component name) and the $Itemid
(the menu item ID). The component itself doesn't have to know its name ($option
) or the active menu item ($Itemid
) like it did in previous version of Joomla.
It is important that you think about the sequence of the URL parameter in this stage. This will be more clear when we have a deeper look at the router.php in the next section.
The building process of \Joomla\CMS\Router\Router is divided into two steps:
- Create the application route. The application route is fully handled by \Joomla\CMS\Router\Router and the component developer doesn’t have to do anything to make it work.
- Create the component route. To create the component route, \Joomla\CMS\Router\Router looks for the router.php in the component directory which is responsible for building the route for the component.
Creating Frontend SEF URLs in the Administrator
From you can also create SEF URLs between any application (the most important use case of this will be creating URLs from the administrator/api to the frontend of Joomla). To achieve this a new method has been introduced. The
\Joomla\CMS\Router\Route::link
is very similar to the \Joomla\CMS\Router\Route::_
discussed in the previous section. \Joomla\CMS\Router\Route::link
has four parameters and its prototype is:
\Joomla\CMS\Router\Route::link($client, $url, $xhtml = true, $ssl = null);
The $client parameter in this case is the client name of the application. So for the Joomla Frontend this will be site
. So an example you can run in the administrator section is
$app->enqueueMessage('3. Admin to Site: ' . JRoute::link('site', 'index.php?option=com_content&catid=1&view=article&id=1'));
to get the SEF URL of a Joomla article with ID 1.
Le Routeur du Composant
We will have three functions in our class in router.php. One is responsible for building the URL and the other is responsible for parsing it. In the next examples, a very basic and a more advanced one, we assume that we have three views that links can point to. The first is a categories overview (view=categories
), the second is a single category (view=category
) and the third is a single article (view=article
).
The file router.php should be in the site area of your component. It is not used on admin/backend pages. Don't forget to add it to your XML manifest file in the site folder.
Un exemple simple basé sur une Vue
Often in Joomla's URL structure you will have routing structures based on view hierarchies. In the this example we will try to reflect the current hierarchy level in the URL from an existing menu item. The goal is URL's that look like:
- When viewing an article: http://www.example.com/[menualias]/[category]/[article]
- When viewing a category: http://www.example.com/[menualias]/[category]
- When viewing the categories overview: http://www.example.com/[menualias]
The link to the article would look like this:
\Joomla\CMS\Router\Route::_('index.php?view=article&catid=' . $row->catslug . '&id='.$row->slug);
And the Link to the category would look like this:
\Joomla\CMS\Router\Route::_('index.php?view=category&id=' . $row->catslug);
Since (and implemented in the core components since
as the "new component routers"), there is a new way of working on routers using the
\Joomla\CMS\Component\Router\RouterView
base class. This handles routing by allowing you to register views into your system. So first of all let's build up our component's router constructor:
/**
* Magic Component router constructor
*
* @param CMSApplication $app The application object
* @param AbstractMenu $menu The menu object to work with
*/
public function __construct($app = null, $menu = null)
{
$category = new RouterViewConfiguration('category');
$category->setKey('id')->setNestable();
$this->registerView($category);
$article = new RouterViewConfiguration('article');
$article->setKey('id')->setParent($category, 'catid');
$this->registerView($article);
}
So what have we done here? Well we've registered a category
view that has a routing key of it's id
, which can be nested (i.e. have multiple levels). We've also registered an article view which also has a routing key of id
, which has a parent of the category view.
Now we have registered our component's views into our router. The next step is to register the Joomla rules that use these rules. There are 3 rules provided by Joomla out of the box. The first \Joomla\CMS\Component\Router\Rules\MenuRules
looks to see if the URL matches a known menu item, and ensures in multilingual sites that a language tag is present. The second rule \Joomla\CMS\Component\Router\Rules\StandardRules
uses your view configuration to build up a menu path. Finally the third rule \Joomla\CMS\Component\Router\Rules\NomenuRules
provides a fallback when there is no good match found for building or parsing the URL. After applying these rules our finished router constructor looks like:
public function __construct(CMSApplication $app = null, AbstractMenu $menu = null)
{
$category = new RouterViewConfiguration('category');
$category->setKey('id')->setNestable();
$this->registerView($category);
$article = new RouterViewConfiguration('article');
$article->setKey('id')->setParent($category, 'catid');
$this->registerView($article);
parent::__construct($app, $menu);
$this->attachRule(new MenuRules($this));
$this->attachRule(new StandardRules($this));
$this->attachRule(new NomenuRules($this));
}
The final piece of the puzzle Joomla needs is to convert the ids to and from their alias. So for each view registered you need to provide a getViewnameSegment($id, $query)
and a getViewnameId($segment, $query)
method, for building and parsing URLs. So for our example we need four functions: getCategorySegment
, getCategoryId
, getArticleSegment
and getArticleId
. You can see the implementation of these four functions at https://github.com/joomla/joomla-cms/blob/3.8.0/components/com_content/router.php . For articles we are going to directly validate our slugs in the database and for categories we will use Joomla's JCategories
class to validate the path to our category.
Building a complicated router in Joomla really is that simple!
Un exemple plus complexe
This more complicated example will illustrate the basics of implementing a more custom router for your component.
use Joomla\CMS\Component\Router\RouterBase;
class [componentname]Router extends RouterBase
{
public function build(&$query)
{
$segments = array();
if (isset($query['view']))
{
$segments[] = $query['view'];
unset($query['view']);
}
if (isset($query['id']))
{
$segments[] = $query['id'];
unset($query['id']);
};
return $segments;
}
\Joomla\CMS\Router\Router
passes a $query
array to the [componentname]Router::build
function. This function will add the relevant parts of the array to the $segments array in the right order and will return the properly ordered array. The content of the $query
array needs to be unset, otherwise \Joomla\CMS\Router\Router
will add it to the URL in the form of a query string (i.e. any variables that are not handled by the router will be passed in the query string).
Note in the above we have chosen to extend \Joomla\CMS\Component\Router\RouterBase
as we do not need to do any preprocessing in this simple example.
The prefix componentname is the name for your component, as found in the directory holding the component's files. For instance, a component "Magic" in directory /components/com_magic/... would use a prefix magic
(all lower case).
The next function in the router.php parses the URL:
public function parse(&$segments)
{
$vars = array();
switch($segments[0])
{
case 'categories':
$vars['view'] = 'categories';
break;
case 'category':
$vars['view'] = 'category';
$id = explode(':', $segments[1]);
$vars['id'] = (int) $id[0];
break;
case 'article':
$vars['view'] = 'article';
$id = explode(':', $segments[1]);
$vars['id'] = (int) $id[0];
break;
}
return $vars;
}
What happens here? In the function [componentname]Router::build
we arranged the items in the $query
array in a specific sequence. This means that in this example the view is first and the id is second in the array.
By reading $segments[0]
, we access the name of the view. We set the right view and/or identifier depending on its value and we return the $vars
array to \Joomla\CMS\Router\Router
. $vars
should be an associative array similar to the array that was passed to the BuildRoute method.
The above example of the router.php is a very simple way to generate SEF URLs but should show how this works quite clearly. The generated URL in this example contains the name of the view and doesn't reflect the content hierarchy:
http://www.example.com/[menualias]/[view]/[slug]
Routers and Menu Items
A last important part of creating a router is considering what to do with menu items. As explained on Search Engine Friendly URLs, the output of the component router is used after the first segment of a route, the first segment being the menu item's alias. This creates a difficult question: how is your router and/or other code to know which menu item to route through?
Suppose, for example, that your component is currently producing output for the page /dogs, which lists all dogs in the system. Of course, the items in the list need to be links to pages that display more details about one dog. What should the URL to the dog with ID 21 and name Fido become? Using a router that works according to the principles we've seen so far, the route that is produced is dogs/21-fido, or with some additional work /dogs/fido. But perhaps the user has created a menu item with the alias mydoggy which displays exactly that dog's details. Then it is probably the user's intention to route this URL through that menu item, and the item in the list should link to the page /mydoggy.
More generally, whenever you are building a route, you will need to find the menu item that is most suitable as a starting point for building your route. The term starting point is emphasized because the rest of the route depends on the configuration of the menu item. In our example above, /dogs/21-fido is an acceptable route, /mydoggy is arguably even better, but /mydoggy/21-fido is simply wrong, since /mydoggy is in itself a menu item that is set up to display fido's information.
Several approaches are available to tackle this problem. Joomla!'s core components take a mixed approach, separating responsibilities in two units of code: the router itself and the so-called [componentname]RouteHelper
. The [componentname]RouteHelper
provides methods that find the most suitable menu item for a given piece of data to be displayed, while the router analyzes the menu item and puts any information that is not determined by the menu item's configuration into the route. This does mean that the calling code must explicitly call the helper's method before routing (echo \Joomla\CMS\Router\Route::_(DogsRouteHelper::getDogRoute(21));
).
SEF Plugin
The Joomla System SEF plugin will try it's best to handle any URLs that aren't being passed into \Joomla\CMS\Router\Route::_
in your application code. It hooks in on the onAfterRender system plugin event. In this function the body of the response that will be sent to the browser is retrieved using \Joomla\CMS\Application\CMSApplication::getBody
. The body of the response is then searched for links containing "/index.php..." and replaces them with a correct SEF url by calling \Joomla\CMS\Router\Route::_(url)
. Note that this is not the most reliable approach. You should use \Joomla\CMS\Router\Route::_
!
Voir également
For details on the internals of routing, see Routing implementation details.