TYPO3 Routing: speaking URLs for Extbase extensions

Routing in TYPO3 v9: the Extbase Plugin Enhancer

The Routing Enhancer for Extbase extensions, reviewed line per line and using EXT:news as an example.

Introduction

The first part of this series dealt with setting up a Site Configuration. Now we will take a closer look at the Extbase Plugin Enhancer. The popular News extension will serve us as an example. But before we dive right in, let's clarify what Routing Enhancers do in general and which types exist in TYPO3 v9.

About Routing Enhancers

As soon as we configured a Site Configuration, TYPO3 automatically rewrites all pages to 'speaking URLs'. Yay!

But if our URLs contain any Query string parts (GET parameters), like tx_news_pi1[news] or tx_felogin_pi1, we'll have to take care of this ourselves. We need a configuration to rewrite these parameters, kinda comparable to RealURL. That's where the Routing enhancers come into play, and the associated Aspects which I'll explain, too.

Which Routing enhancers are available in TYPO3 v9?

  1. Simple Enhancer (type: Simple):
    Works with various route arguments to map them to a argument to be used in the URL later-on.
  2. Plugin Enhancer (type: Plugin):
    This enhancer can be used for pibase plugins.
  3. Extbase Plugin Enhancer (type: Extbase):
    As the name suggests, we can use this enhancer for Extbase extensions. It is an extension of the regular Plugin Enhancers.
  4. PageTypeEnhancer (type: PageType):
    This special enhancer is solely responsible for rewriting the typeNum of a page (&type=123). It allows to append a suffix to the end of an URL. You can use it to add a slash / or .html to all URLs or e.g. rewrite a type as rss.xml.

For each task a new routing enhancer is configured. So, for example

  • an Extbase Plugin Enhancer for EXT:news,
  • another Extbase Plugin Enhancer for EXT:tt_address and
  • a Plugin Enhancer for EXT:felogin.

The Extbase Plugin Enhancer for the News extension

Now to the practical implementation. First I'll show you the final and working configuration. Then we look at the individual components more closely.

The following, minimal page tree exists as my example:

UIDTitleTypeURL SegmentPage content
7routingroot/Empty, irrelevant for this tutorial
8news: listStandard/news-listBasic news list view
9news: recordsFolder/news-list/news-recordsNews records and system categories
10news: detailStandard/news-list/news-detailBasic news detail view
11news: category listStandard/categoryNews list view with category mode enabled (OR), news category menu (filter)

In addition to the normal news list view, the example includes a second list view with the ability to filter news records by their assigned system categories. I intentionally didn't change the URL segments of the pages (the category page beeing an exception).

The complete Site Configuration

The finished config.yaml contains the mandatory details about domain, language etc. We will focus on the routeEnhancers configuration.

rootPageId: 7
base: 'https://mydomain.de/'
baseVariants:
  -
    base: 'http://mydomain.test/'
    condition: 'applicationContext == "Development"'
languages:
  -
    title: English
    enabled: true
    languageId: '0'
    base: /
    typo3Language: default
    locale: en_US.UTF-8
    iso-639-1: en
    navigationTitle: English
    hreflang: en-US
    direction: ''
    flag: en-us-gb
errorHandling: {  }
routes: {  }
routeEnhancers:
  News:
    type: Extbase
    limitToPages:
      # News list view - needed for pagination:
      - 8
      # News detail views - needed for general routing:
      - 10
      # News category view:
      - 11
    extension: News
    plugin: Pi1
    routes:
      - routePath: '/page/{page}'
        _controller: 'News::list'
        _arguments:
          page: '@widget_0/currentPage'
      - routePath: '/{news_title}'
        _controller: 'News::detail'
        _arguments:
          news_title: news
      - routePath: '/{category_name}'
        _controller: 'News::list'
        _arguments:
          category_name: overwriteDemand/categories
    defaultController: 'News::list'
    defaults:
      page: '0'
    requirements:
      news_title: '^[a-zA-Z0-9].*$'
      page: \d+
    aspects:
      news_title:
        type: PersistedAliasMapper
        tableName: tx_news_domain_model_news
        routeFieldName: path_segment
      page:
        type: StaticRangeMapper
        start: '1'
        end: '100'
      category_name:
        type: PersistedAliasMapper
        tableName: sys_category
        routeFieldName: title

The separate Routing options, identified by line number

In this tutorial I will restrict myself to the options used in this basic example. The comprehensive Changelog entry about Routing Enhancers offers more configurations and explanations. For example, the extension und plugin options could be omitted and replaced with a namespace.

22) routeEnhancers:

In this section all Routing Enhancers are configured. The section itself may exist only once.

23) News:

For each Enhancer that we configure, an arbitrary but unique name has to be set. You don't have to use the extension key, "Blog" or VanillaIceCream" would also work.

24) type: Extbase

For the news extension, the Extbase Plugin Enhancer is used.

25) limitToPages:

Calling the enhancer can be limited to an array of pages. This setting is not mandatory, but recommended for performance reasons.

For the news extension we primarily need routing for the detail view. However, we'll also need routing for the list view's pagination and the filtering of categories. This will be explained below.

32) extension: News

The extension name. Attention: the name must be converted to UpperCamelCase! For example, the extension key blog_example becomes BlogExample.

33) plugin: Pi1

The pluginName of the extension. You don't know the exact name of a plugin? Check the extension's source code how it was registered in TYPO3 with registerPlugin. In case of EXT:news it is simply "Pi1".

34) routes:

Now were coming to the root of the matter! Here we create a routePath for each view that we want to rewrite into "speaking URLs".

In my example a routePath is needed for each:

  1. the list view, once the widget is used for pagination (Page 1 of x);
  2. the detail view of all news records;
  3. a (separate) list view where I want to filter by categories.

Instead of the cryptic URLs

  1. mydomain.test/news-list?tx_news_pi1[@widget_0][currentPage]=2&cHash=
  2. mydomain.test/news-list/news-detail?tx_news_pi1[action]=detail&tx_news_pi1[controller]=News&tx_news_pi1[news]=3&cHash=
  3. mydomain.test/category?tx_news_pi1[overwriteDemand][categories]=3&cHash=

I want to create URLs like

  1. mydomain.test/news-list/page/2
  2. mydomain.test/news-list/news-detail/my-news-article
  3. mydomain.test/category/my-category

Once you compare these requirements with the configured routePaths, the routing process will hopefully be clearer. That's because the GET parameters of the above URLs can be found in the arguments of the routePaths:

      - routePath: '/page/{page}'
        _controller: 'News::list'
        _arguments:
          page: '@widget_0/currentPage'
      - routePath: '/{news_title}'
        _controller: 'News::detail'
        _arguments:
          news_title: news
      - routePath: '/{category_name}'
        _controller: 'News::list'
        _arguments:
          category_name: overwriteDemand/categories

First of all we define the desired path. You are wondering where the placeholders {page}, {news_title} and {category_name} come from? These are passed in _arguments. But this alone would result in URLs like "mydomain.test/news-list/news-detail/3?cHash=". Therefore, the arguments will be configured by aspects to our wishes.

We also need a combination of the current controller and action, e.g. News::detail. The possible combinations of an extension can be found in the ext_localconf.php file within the section configurePlugin.

For now we'll skip the next couple of lines and take a closer look at the Aspects.

53) aspects:

Here, a routePath's arguments can be modified or mapped, e.g. to database fields.

There are several types of Aspects. Each one can be used used with any Routing Enhancer. When we configure an Aspect, it only applies to the current Routing Enhancer (never globally).

Let's take a look at the argument {news_title}: by default, it contains the UID of the current news record. By using the aspect type PersistedAliasMapper we can access the database table with the news records and replace the UID with the current value of the field path_segment.
The field path_segment is filled by the news extension with a copy of the news title, optimized for use in URLs: spaces are replaced by hyphens, special characters are removed, all characters are converted to lower case.

That brings us to the Aspect of {category_name}. Here, the normal title field is used as routeFieldName. This will result in unpleasant URLs if a category title consists of more than one word:

mydomain.test/category/My%20Category

If you use the news extension in version 7.1.0 or newer, you can avoid this issue: both sys categories and news tags were extended with a dedicated slug field. So if you use these slug fields as routeFieldName, you'll get equally optimized URL paths.

Finally, there's the StaticRangeMapper for the argument {page} (the pagination in list views). Here we limit the number of pages to 100. Without this limit, the requirements for routing would be too loose and the cHash parameter would remain in the URL.
Note: The StaticRangeMapper is internally limited to a maximum of 1000 elements to prevent brute force attacks.

In this routing example we only use two of the Aspect types. More Aspect examples can be found in the Changelog.

Let us now deal with the remaining options of our example.

47) defaultController

The defaultController is used if none of the configured routes match.

48) defaults:

In this section we define which GET parameters are optional. If these parameters are omitted during the generation of the link, they receive the default value instead and a placeholder isn't necessary.

50) requirements:

Here, regular expressions are used to specify which types of data should be added to the route. If the generated URL still contains a cHash parameter, your requirements are too loose.

We use \d+ to restrict the pagination {page} of the list views to one or more digits. The {news_title} may contain uppercase and lowercase letters and numbers.

Conclusion

I hope this short introduction was useful to you. The configuration could be extended as desired. You could for example add settings for the news tags or the date menu.

It would go beyond the scope of a tutorial to list every possible routing options. Be sure to read through the changelog entries about Routing Enhancers and Aspects and PageTypeEnhancer.

More tutorials on this topic could follow. In the meantime, keep experimenting!

Back