Note many examples online use namespaced classes. This was introduced in Joomla 3.8. There are non-namespaced versions of all these classes which will work in Joomla 3.4+.
Search engine friendly (SEF), human-readable or clean URLs are URLs that make sense to both humans and search engines because they explain the path to the particular page they point to. Since version 1.5, Joomla! is capable of creating and parsing URLs in any format, including SEF URLs. This does not depend on URL rewriting executed by the web server, so it works even if Joomla! runs a server other than Apache with the mod_rewrite module. The SEF URLs follow a certain fixed pattern, but the user can define a short descriptive text (alias) for each segment of the URL.
Internally, the local part of a SEF URL (the part after the domain name) is called a route. Creating and processing SEF URLs is therefore referred to as routing, and the relevant code is called a router.
In Joomla!, each component is responsible for handling its own SEF URLs. Therefore, as the developer of a component, you will have to create your own router to allow your component to use SEF URLs.
The Concept[edit]
Assuming you are following standard development practices, your component is probably using «system URLs» that look a lot like http://www.example.com/index.php?option=com_yourcomponent&view=article&id=1&catid=20&Itemid=50, and your goal is to transform this into http://www.example.com/example-menu-item/example-category/example-article. As the developer, you have two tasks: signalling the system that certain pieces of text are URLs and need to be transformed, and explaining the system how to transform URLs.
Applying Route::_
[edit]
It is difficult and inefficient for Joomla! to figure out which parts of your component’s output are URLs. To support SEF URLs, you will need to change URL-generating code so that it applies \Joomla\CMS\Router\Route::_
before outputting the URL:
echo \Joomla\CMS\Router\Route::_('index.php?view=article&id=1&catid=20');
Notice that it is possible to leave out the parameters option
and Itemid
. option
defaults to the name of the component currently being executed, and Itemid
defaults to the current menu item’s ID.
In general, you should only apply this to URLs that users and/or search engines are able to see. For example, there is no need to transform URLs used in redirects that immediately result in other redirects.
If the user turns off SEF URLs in the site’s settings, \Joomla\CMS\Router\Route::_
will produce working non-SEF URLs without any changes to the code.
Writing a router[edit]
You’ll also need to write a router, which is a single file containing a class with three functions that convert system URLs to and from SEF URLs. This file needs to be placed at /components/com_yourcomponent/router.php.
The class should be called [componentname]Router
(e.g. for com_content ContentRouter
), and must implement Joomla\CMS\Component\Router\RouterInterface
The first function, build(&$query)
, must transform an array of URL parameters into an array of segments that will form the SEF URL. Schematically, the transformation works as follows:
- http://www.example.com/index.php?option=com_yourcomponent&view=article&id=1&catid=20&Itemid=50
-
- ↓
\Joomla\CMS\Router\Route::_
, called by your component or any other extension
- ↓
$query = array('view' => 'article', 'id' => 1, 'catid' => 20)
-
- ↓ Your router’s
[componentname]Router::build
- ↓ Your router’s
$segments = array(20, 1);
-
- ↓ Joomla’s internal route building (for display)
- http://www.example.com/example-menu-item/20/1
The second function, parse($segments)
, must transform an array of segments back into an array of URL parameters. Schematically:
- http://www.example.com/example-menu-item/20/1
-
- ↓ Joomla’s internal route parsing
$segments = array(20, 1);
-
- ↓ Your router’s
[componentname]Router::parse
- ↓ Your router’s
$query = array('view' => 'article', 'id' => 1, 'catid' => 20)
The two functions must cooperate in such a way that the original URL can be reconstructed. You can think of build
as a form of encoding and parse
as the corresponding decoding. When the original URL isn’t properly reproduced, your component will stop working.
The final function, preprocess($query)
, is a preparation method for URLs. This method is executed on each URL, regardless of SEF mode switched on or not. We will come back to this method later in the tutorial.
Preparing Your Data for Routing[edit]
Clearly, any URL format needs to contain some kind of information that identifies the data you want to show. If your system URLs look like http://www.example.com/index.php?option=com_yourcomponent&view=article&id=1&catid=20&Itemid=50, that information is currently the id URL parameter (id=1). You probably want your SEF URLs to contain a textual description of the data they point to. In Joomla!, this is usually done by giving your users a way to enter an alias to be used in the URL.
The Alias[edit]
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.
The Slug[edit]
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.
Routing URLs[edit]
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);
Where:
$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[edit]
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.
The Component Router[edit]
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.
A Simple View-based Example[edit]
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!
A more complex Example[edit]
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[edit]
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[edit]
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::_
!
See Also[edit]
For details on the internals of routing, see Routing implementation details.
19 сентября 2017 г. состоялся релиз Joomla 3.8. Если вы не являетесь разработчиком расширений, то в этой версии вас может заинтересовать всего две особенности: отложенная установка демо-данных и обновленный роутер Joomla, позволяющий строить более красивые URL’ы страниц и избавляющий от части дублей. В этой статье я расскажу подробнее про этот самый роутер: как его включить, кому и когда можно использовать.
Что такое роутер и зачем он мне нужен?
Если слово «роутер» не вызывает у вас никаких ассоциаций, то этот блок специально для вас.
Говоря простыми словами, роутер Joomla – это та штука, что превращает ссылки вида:
index.php?option=com_content&view=article&id=2:art&catid=8&Itemid=154
в:
cat/art
Поисковые системы любят последние гораздо больше первых.
Роутер начинает работать, когда вы включаете в админке, в общих настройках, использование SEF (ЧПУ):
Не хочу вдаваться в особенности работы роутера, но могу сказать, что SEF-ссылки должны быть включены на каждом сайте. Это не только плюс для SEO-оптимизации сайта, но и вопрос безопасности.
Видимые обновления роутера коснулись трех параметров:
- Из URL убраны ID материалов.
- Практически сведены к нулю дубли страниц
- Корректная обработка несуществующих страниц (404).
Теперь подробнее.
Используя новый роутер, вы можете получить красивые ссылки. Давайте сравним ссылки, генерируемые старым и новым роутером:
Старый:
/menu-item-blog-category-alias/2-article-alias
новый:
/menu-item-blog-category-alias/article-alias
Т.е., к примеру, старый:
/cat/2-art
новый:
/cat/art
Из URL убрали ID материала, наличие которого было обязательным в старом роутере. Заодно решили массу смежных проблем с дублями.
Раньше, если оригинальный материал был доступен по ссылке:
/cat/2-art
то он также был доступен и по следующим ссылкам:
- /cat/2
- /cat/2/
- /2
- /2-blablabla
Вместо «blablabla» можно подставить любой, совершенно любой текст. И каждая такая страница загрузится и будет дублем основной страницы. Такие дела…
Новый роутер, к счастью, решает эту проблему. Теперь, если он активирован, никакие другие, альтернативные варианты, не сработают. Везде вернется ошибка 404.
Да, насчет ошибки 404. Теперь она реально отдает код 404, а не 200, с заголовком «404», как это было раньше. В общем, детские болячки Joomla по SEO немного подлатали.
Как я могу использовать новый роутер Joomla?
Готов поспорить, что вы сами, без гуглинга, не найдете как включается новый роутер в Joomla 3.8. Я не нашел =). В этой версии роутер пока экспериментальный, и разработчики запрятали его куда подальше. Пока новый роутер доступен для контента, контактов, пользователей и новостных лент. Для каждого из этих типов контента он включается отдельно. Рассказываю на примере контента, остальное по аналогии.
Идем в менеджер материалов, нажимаем кнопку «Настройки» в правом верхнем углу, в открывшемся окне настроек переходим на вкладку «Интеграция» и находим на ней опцию «URL Routing». Для активации нового роутера она должна быть установлена в «Experimental»:
Для других типов контента активация аналогична.
Зачем такие сложности? Думаю, все дело в том, что новый роутер пока «Experimental», и этим все сказано.
Когда я могу использовать новый роутер Joomla?
Вот мы и добрались до главного вопроса: «Быть или не быть?». Когда оправдано применение нового роутера?
- Если вы делаете новый сайт – 100% ДА.
- Если у вас существующий сайт с небольшим количеством страниц – ДА, и настраиваем вручную перенаправления 301 со старых URL на новые.
- Если у вас существующий сайт с большим количеством страниц – нужно подумать и взвесить все «за» и «против». Если вы сможете создать автоматически перенаправления, то может быть игра и стоит свеч. Если все и так неплохо, дубли вас не беспокоят, посещаемость растет, используете и дальше старый роутер.
- Если у вас существующий сайт со сторонним SEF-компонентом – дождитесь выхода Joomla 4, в которой роутер должны окончательно довести до ума, а затем избавляйтесь от SEF-компонента в пользу родного нового роутера.
Таково мое мнение.
Кстати, ID в URL отключать необязательно. Новый роутер может работать и с ними. Это особенно актуально, если вы имеете уже проиндексированный сайт. Только вот дубли при этом также останутся с вами.
Что можно сказать в заключение? С выходом Joomla 3.8, с SEO дела стали обстоять получше. Хочется, конечно, большего, но думаю, мы увидим это в Joomla 4, а пока будем понемногу осваивать новый роутер.
Об авторе
Wedal (Виталий). Веб-разработчик полного цикла (Full Stack). Создатель и автор сайта Wedal.ru.
Основной профиль – создание сайтов и расширений на CMS Joomla.
- Об авторе
- Портфолио
- Услуги
- Контакты
Да, да Мы снова о нём — о роутере Joomla. Его ругают, его не любят, но с ним приходится жить. Но похоже на то, что наши «мучения» скоро закончатся. 20 февраля 2016 года произошло знаменательное событие — в ветку Joomla 3.6.x был влит Pull Request базовых классов для нового роутера Joomla.
Немного истории
Всё началось почти два года назад — в апреле 2014 года Hannes Papenberg открыл компанию по сбору средств на разработку нового роутера Joomla, которая завершилась успехом — средства были собраны. Hannes приступил к работе и первый Pull Request был готов уже в июне 2014 года. После этого была ещё серия Pull Request, которые постепенно подготавливали основу, но так и не были влиты по тем или иным причинам.
В итоге Hannes объединил их в один большой Pull Request, который был влит в ветку Joomla 3.6.x (к слову, был влит не полностью, о чём я уже сообщил виновнику кривого коммита).
Но на этом история не заканчивается — Hannes в своей компании обещал плагины, которые поддерживали бы различные варианты роутинга. Вот что он пишет по этому поводу:
I know that you are waiting for the new features that I described in this campaign. Please don’t be alarmed that these are not present in that branch yet. What has been done so far was the ground work to get these new features in and I will soon provide the necessary code for this. Compare it with a car: What I’ve been working on is to get the engine running and the breaks and steering working. Now we can add the new features, like the color of the car, heated seats, navigation and the stereo.
Я знаю, что вы ждёте новые возможности, которые я описал в этой компании. Пожалуйста, не волнуйтесь о том, что их пока что нет в текущей ветке. Что было сделано на данный момент — это основа для внедрения новых возможностей, и я скоро предоставлю необходимый для этого код. Сравните это с машиной: то, над чем я работал, это возможность завести двигатель, заставить работать тормоза и рулевое управление. Теперь мы можем добавлять новые фишки, такие как цвет машины, сиденья с подогревом, навигация и стерео.
Каким будет роутер?
Вот это вопрос на миллион. Что там внутри? Я решил провести небольшой первичный анализ кода Pull Request, чтобы понять, каким будет новый роутер Joomla. Хочу сразу предупредить, что я могу ошибаться, так как никакой документации пока ещё нет.
Основные моменты
Хочу выделить несколько основных моментов:
- новый роутер будет включаться на уровне компонентов. В настройки каждого стандартного компонента добавлена соответствующая опция
sef_advanced
; - роутинг будет строится на основе правил, и на данный момент их несколько: Standard (если
sef_advanced
включен), Legacy (еслиsef_advanced
выключен), Menu и Nomenu; - регистрирация конфигурации представлений: каждое представление (view) компонента должно быть зарегистрировано в роутере как объект класса
JComponentRouterViewconfiguration
.
Базовые классы
Все базовые классы роутера компонента находятся в /libraries/cms/component/router.
JComponentRouterInterface
Интерфейс роутера компонента, который содержит три метода:
preprocess($query)
— используется для валидации и завершения URL параметров. Например, может быть использован для добавленияItemid
илиlanguage
параметров. Метод вызывается для каждого URL независимо от того, включен ли SEF или нет;build(&$query)
— используется для трансформации URL из query параметров в человекочитаемый формат. Метод вызывается только при включенном SEF;parse(&$segments)
— используется для трансформации URL из человекочитаемого формата обратно в query параметры. Метод вызывается только при включенном SEF.
JComponentRouterBase
Базовый абстрактный класс роутера компонента. Реализует JComponentRouterInterface
. Его основаная задача — это установка объектов приложения и меню.
JComponentRouterLegacy
Базовый класс для устаревшей версии роутера. Реализует JComponentRouterInterface
. Это класс по умолчанию для компонентов с устаревшим роутером (который включал методы *BuildRoute
и *ParseRoute
) или у которых нет своего роутера.
JComponentRouterView
Класс роутера компонента в котором за основу взято представление. Расширяет JComponentRouterBase.
Это по сути реализация нового роутера стандартных компонентов Joomla. Его основные задачи:
- регистрация представлений в виде объекта класса
JComponentRouterViewconfiguration
(методregisterView
); - добавление/удаление одного или сразу нескольких правил, реализующих интерфейс
JComponentRouterRulesInterface
(методыattachRule
,detachRule
,attachRules
,detachRules
); - получение полного пути — массива списка всех представлений, включая ID элементов контента, через обращение к методам объекта
JComponentRouterViewconfiguration
и своим методамget<Viewname>Segment
(методgetPath
); - обход правил в методах
preprocess
,build
иparse
.
JComponentRouterViewconfiguration
Конфигурационный класс представления для роутинга компонента, в котором за основу взято представление.
Содержит в себе имя представления (например article, category) , ключ (например id, catid), родительский объект JComponentRouterViewconfiguration
(если есть, например для материала это может быть объект категории), признак вложенности, список поддерживаемых макетов в виде массива (default, blog, form и т.п.), а также полный путь в виде массива от этого представления до корневого.
JComponentRouterRulesInterface
Интерфейс для правил находится в подпапке /rules и содержит в себе такие же методы, как и интерфейс роутера компонента JComponentRouterInterface
, но с другой сигнатурой. Эти методы вызывает класс роутера компонента, когда делает обход всех добавленных в него правил.
Правила
Также в подпапке /rules находятся различные правила роутинга, которые реализуют JComponentRouterRulesInterface
.
JComponentRouterRulesMenu
— это правило на основе меню, строит массив меню и выполняет поиск Itemid. Это то, что обычно разработчики делали в хелпере компонентов.
JComponentRouterRulesNomenu
— это правило предполагает отсутствие активного пункта меню.
JComponentRouterRulesStandard
— это правило стандартной обработки роутинга. Использует методы get<Viewname>Id
ротуера компонента для получения ID элементов контента.
Изменения в компонентах
Понятно, что простое изучение классов особо ничего не даст без реальных примеров. Так как документации не существует, будем ориентироваться на стандартные компоненты Joomla. Благо Pull Request включает в себя изменённые роутеры компонентов com_contact, com_content, com_newsfeed и com_users.
Рассмотрим в качестве примера роутер комопнента com_content.
Главное, что отличает новый класс роутера — он расширяет класс JComponentRouterView
. В конструкторе регистрируются все представления компонента и добавляются правила: всегда правило Menu, а также, если в настройках компонента включена опция sef_advanced
, добавляется правило Standard. Если опция выключена, то добавляется правило Legacy. Все текущие роутеры были переделаны в Legacy правила и перемещены в /helpers/legacyrouter.php компонентов.
/**
* Content Component router constructor
*
* @param JApplicationCms $app The application object
* @param JMenu $menu The menu object to work with
*/
public function __construct($app = null, $menu = null)
{
$categories = new JComponentRouterViewconfiguration('categories');
$categories->setKey('id');
$this->registerView($categories);
$category = new JComponentRouterViewconfiguration('category');
$category->setKey('id')->setParent($categories, 'catid')->setNestable()->addLayout('blog');
$this->registerView($category);
$article = new JComponentRouterViewconfiguration('article');
$article->setKey('id')->setParent($category, 'catid');
$this->registerView($article);
$this->registerView(new JComponentRouterViewconfiguration('archive'));
$this->registerView(new JComponentRouterViewconfiguration('featured'));
$this->registerView(new JComponentRouterViewconfiguration('form'));
parent::__construct($app, $menu);
$this->attachRule(new JComponentRouterRulesMenu($this));
$params = JComponentHelper::getParams('com_content');
if ($params->get('sef_advanced', 0))
{
$this->attachRule(new JComponentRouterRulesStandard($this));
}
else
{
require_once JPATH_SITE . '/components/com_content/helpers/legacyrouter.php';
$this->attachRule(new ContentRouterRulesLegacy($this));
}
}
Реализуются методы get<Viewname>Segment
для получения сегментов:
/**
* Method to get the segment(s) for a category
*
* @param string $id ID of the category to retrieve the segments for
* @param array $query The request that is build right now
*
* @return array|string The segments of this item
*/
public function getCategorySegment($id, $query)
{
$category = JCategories::getInstance($this->getName())->get($id);
if ($category)
{
return array_reverse($category->getPath());
}
return array();
}
/**
* Method to get the segment(s) for an article
*
* @param string $id ID of the article to retrieve the segments for
* @param array $query The request that is build right now
*
* @return array|string The segments of this item
*/
public function getArticleSegment($id, $query)
{
return array($id);
}
Также реализуются методы get<Viename>Id
для получения ID:
/**
* Method to get the id for a category
*
* @param string $segment Segment to retrieve the ID for
* @param array $query The request that is parsed right now
*
* @return mixed The id of this item or false
*/
public function getCategoryId($segment, $query)
{
if (isset($query['id']))
{
$category = JCategories::getInstance($this->getName())->get($query['id']);
foreach ($category->getChildren() as $child)
{
if ($child->id == (int) $segment)
{
return $child->id;
}
}
}
return false;
}
/**
* Method to get the id for an article
*
* @param string $segment Segment of the article to retrieve the ID for
* @param array $query The request that is parsed right now
*
* @return mixed The id of this item or false
*/
public function getArticleId($segment, $query)
{
return (int) $segment;
}
И больше ничего. Всю остальную работу на себя берут родительский класс и правила.
Все route хелперы (которые мы обычно создавали в /helpers/route.php) тоже полегчали — поиск Itemid теперь тоже работа правила. По сути все методы — это просто обёртки для возврата ссылки типа index.php?option=com_content&view=article&id=' . $id
.
Что в итоге?
Очень похоже на то, что Hannes удалось создать неплохую инфраструктуру нового роутера. Можно сразу отметить, что разработчикам жить становится намного проще, если они будут придерживаться стандартного роутинга. В этом случае всю муторную работу будет брать на себя базовый класс JComponentRouterView
и правила.
Непосредственно реализация правил вроде бы тоже неплоха, но я пока не понимаю, как их можно будет реализовывать в виде плагинов. Объект правила создаётся и потом добавляется в роутер через attachRule()
. Но как создать объект плагина — инстанцировать напрямую? В общем не до конца понятен этот момент… А ведь изначально идея состояла именно в этом — правила в виде плагинов.
Ну и пока рано говорить о ликвидации дублей и ненавистных всем циферок (ID) в адресной строке. Но вероятнее всего, это вопрос реализации конкретного правила.
P.S.
Если вы хотите самостоятельно изучить исходный код, то либо используйте ветку Joomla 3.6.x (но там пока нет некоторых файлов), либо репозитроий Hannes (на момент написания статьи он был актуален).
Обзор маршрутизации
Joomla может создавать и синтаксически разбирать URL-адреса в разных форматах, в том числе и в человекопонятном. Одно из преимуществ заключается в том, что преобразование ссылок работает, даже если на сервере нет apache-модуля mod_rewrite.
Хорошим примером этого является статья «Добро пожаловать в Joomla». Первая ссылка создана без mod_rewrite, а вторая с mod_rewrite:
- http://www.example.com/index.php/the-news/1-latest-news/1-welcome-to-joomla
- http://www.example.com/the-news/1-latest-news/1-welcome-to-joomla
Подготовка данных для маршрутизации
Псевдоним
Первым шагом является формирование так называемого псевдонима. Псевдоним используется в URL вместо заголовка «(заголовок это текст, который вы хотите видеь в URL). Псевдоним должен быть URI-безопасным, что означает замену UTF-8 символов их эквивалентами ASCII7, пробельных — дефисами и т.д.
Псевдоним может быть определен самим пользователем, но вы должны обеспечить, чтобы были соблюдены вышеуказанные требования к безопасному URL. Хороший способ сделать это заключается в использовании метода JTable::check() в процесе сохранения. Вы можете посмотреть на этот пример кода:
function check() { jimport('joomla.filter.output'); if(empty($this->alias)) { $this->alias = $this->title; } $this->alias = JFilterOutput::stringURLSafe($this->alias); /* Ваши остальные проверки */ return true; }
Если поле псевдонима будет пусто, то заголовок будет использоваться как псевдоним. Тогда псевдоним будет сделан методом JFilterOutput::stringURLSafe().
Строка
Продолжая тот же самый пример, «строка» — «1-welcome-to-joomla» состоит из двух частей. Первая часть — идентификатор статьи, и вторая — псевдоним. Они отделены дефисом. Эти два элемента были объединены во время запроса к базе данных в модели:
$query = 'SELECT a.*,'. ' CASE WHEN CHAR_LENGTH(a.alias) THEN CONCAT_WS(\':\', a.id, a.alias) ELSE a.id END as slug,'. [...];
После этого шага строка используется вместо иденитификатора.
Маршрутизация URL’s
Метод JRoute::_()
преобразует внутренние ссылки Joomla в SEF-ссылки. JRoute имеет три параметра, вот её прототип:
JRoute::_($url, $xhtml = true, $ssl = 0);
Где:
$url
— абсолютный или относительной внутренний URL Joomla.$xhtml
— булевое значение, которое указывает, должен ли вывод быть в XHTML. Этот параметр не обязательный, и равен true по умолчанию.$ssl
— целое число, которое определяет должен ли URI быть безопасным.
Наиболее важным параметром является $url
.
Пример вызова этого метода:
JRoute::_('index.php?view=article&id='.$row->slug);
$row->slug
это значение, полученное сочетанием id и псевдонима во втором шаге.
Еще одно преимущество использования JRoute состоит в том, что теперь маршрутизатор обрабатывает $option (имя компонента) и $Itemid (id пункта меню). Компонент сам по себе не должен знать своё имя ($option) или id активного пункта меню ($Itemid), как это было в предыдущей версии Joomla.
Очень важно, что вы думаете по поводу последовательности в URL параметра в этой стадии. Это будет более понятно, когда мы глубже взглянем на router.php в следующем разделе.
Процесс создания JRouter делится на два этапа:
- Создание маршрута приложения. Маршрутизация полностью обрабатываются JRouter и разработчику компонента не придется ничего делать, чтобы она работала.
- Создание маршрута компонента. Для создания маршрута компонента, JRouter ищет в каталоге компонента, файл router.php который отвечает за создание маршрута для компонента.
Маршрутизатор
У нас будут две функции в router.php. Одна отвечает за создание URL, и другая за его синтаксический разбор. В следующих примерах, очень основном и более продвинутом, мы предполагаем, что у нас есть три представления, на которые могут вести ссылки. Первым представлением является краткий обзор категорий (view=categories), вторым является единственная категория (view=category), и третьей является единственная статья (view=article).
Файл router.php должен быть в области сайта вашего компонента. Это не используется на страницах администрации / внутренних страницах. Не забывайте добавлять router.php к своей инсталляции XML в папке сайта.
Простой пример
Этот простой пример показывает основы работы маршрутизатора вашего компонента.
function [Componentname]BuildRoute(&$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; }
JRouter
передает массив $query в функцию [componentname]BuildRoute
. Эта функция добавит соответствующие части массива массиву $segments в правильном порядке и возвратит должным образом полученный массив.
Элементы массива $query должны быть удалены, иначе JRouter
добавит их в URL в виде строки запроса (т.е. любые переменные, которые не обрабатываются маршрутизатором будут использованы как параметры в строке запроса).
Следующая функция в router.php разбирает URL:
function [Componentname]ParseRoute($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; }
Что происходит здесь? В функции [componentname]BuildRoute
мы упорядочили элементы в массив в определенной последовательности. Это означает, что в этом примере представление является первым, catid является вторым, и идентификатор третьим в массиве. Читая $segments[0]
, мы получаем название представления. Мы устанавливаем правильное представление и/или идентификатор в зависимости от его значения, и мы возвращаем массив $vars
в JRouter. $vars
должен быть ассоциативным массивом, подобным массиву, который передали к методу BuildRoute
.
Вышеупомянутый пример router.php очень простой способ генерировать sef URL, он должен показать принцип работы маршрутизатора.
Сгенерированный URL в этом примере содержит название представления и не отражает иерархию информационного наполнения:
http://www.example.com/[псевдоним_пункта_меню]/[представление]/[slug]
Более сложный пример
В следующем примере мы попытаемся избавиться от потребности в представлении, и попытаемся отразить в URL текущий уровень иерархии.
Цель состоит в том, что URL должен выглядеть следующим образом:
- Когда смотрим статью:
http://www.example.com/[псевдоним пункта меню]/[категория]/[статья]
- Когда смотрим категорию:
http://www.example.com/[псевдоним пункта меню]/[категория]
- Когда смотрим список категорий:
http://www.example.com/[псевдоним пункта меню]
Давайте представим, что мы сделали шаг 1 и 2, также для данной категории.
Ссылка на статьи будет выглядеть следующим образом:
JRoute::_('index.php?view=article&catid='.$row->catslug.'&id='.$row->slug);
И ссылка на категорию будет выглядеть так:
JRoute::_('index.php?view=category&id='.$row->catslug);
Соответствующий router.php
:
function [Componentname]BuildRoute(&$query) { $segments = array(); if(isset($query['catid'])) { $segments[] = $query['catid']; unset($query['catid']); } if(isset($query['id'])) { $segments[] = $query['id']; unset($query['id']); }; unset($query['view']); return $segments; }
Разница заключается в том, что сейчас мы не добавляем имя представления к массиву $segments
. Мы по-прежнему отключаем представление, поскольку в противном случаеJRouter
добавил бы его в качестве части URL в строке запроса. Еще одним нововведением является параметр catid
, который мы добавляем в массив $segments
.
function ['Componentname']ParseRoute($segments) { $vars = array(); $menu =& JMenu::getInstance(); $item =& $menu->getActive(); // Количество сегментов $count = count($segments); // Выбор мредставления и идентификатора switch( $item>query['view']) { case 'categories': if($count == 1) { $vars['view'] = 'category'; } if($count == 2) { $vars['view'] = 'article'; } $id = explode(':', $segments[$count-1]); $vars['id'] = (int)$id[0]; break; case 'category': $id = explode(':', $segments[$count-1]); $vars['id'] = (int) $id[0]; $vars['view'] = 'article'; break; } return $vars; } }
Вы видете, что у этой функции ParseRoute
в коде есть много отличий по сравнению с предыдущей. Причина для этого проста. У нас нет названия представления в массиве $segments, и мы должны определить его другим способом.
Мы должны узнать, в каком уровне иерархии мы находимся, получая корневой элемент. Мы делаем это, обращаясь к названию представления активного пункта меню:
$item->query['view']
Также мы должны знать число элементов в массиве $segments:
$count = count($segments);
Имея эту информацию мы можем правильно установить представление для всех возможных трех случаев:
- Если пункт меню — ссылка на представление категорий, и у массива
$segments
есть два элемента ($catid и $id), мы знаем, что должны анализировать ссылку на статью. - Если пункт меню — ссылка на представление категорий и у массива $segments есть один элемент
$id
, мы знаем что должны анализировать ссылку на категорию. - Если пункт меню — ссылка на категорию, мы знаем, что любой элемент в массиве
$segments
— идентификатор для статьи.
Результат всего этого кода — хороший и человеческий читаемый URL.
[Видео] Joomla 4 router (для разработчиков)
2 видео от Робби Джексона (Robbie Jackson), рассказывающие о внутреннем устройстве роутера Joomla 4 — кода, отвечающего за формирование и распознавание SEF адресов в Joomla.
1 часть — Joomla 4 router part 1 — parsing a URL
Обзор устройства роутинга Joomla 4, а также даны советы по разработке роутера в Вашем компоненте.
Презентация в Google Docs
2 часть — Joomla 4 router part 2 — building a URL
Объясняет как происходит процесс построения SEF url в Вашем компоненте. Обзор подхода RouterView.
Видео на Youtube (в постах Хабра можно вставлять только одно видео). Презентация в Google Docs
Материалы на английском языке.
Также welcome в чат русскоязычного Joomla-сообщества