Advanced routing configuration (for extensions)

Introduction

While page-based routing works out of the box, routing for extensions has to be configured explicitly in your site configuration.

To map GET parameters to routes, a concept called "enhancers and aspects" has been introduced.

An enhancer creates variants of a specific page-based route for a specific purpose (e.g. an Extbase plugin) and enhances the existing route path, which can then contain flexible values, so-called "placeholders".

On top, aspects can be registered for a specific enhancer to modify a specific placeholder, like static human readable names within the route path or dynamically generated values.

To give you an overview of what the distinction is, we take a regular page which is available at

https://example.org/path-to/my-page

to access the page with ID 13.

Enhancers are a way to extend this route with placeholders on top of this specific route to a page:

https://example.org/path-to/my-page/products/<product-name>

The suffix /products/<product-name> to the base route of the page is added by an enhancer. The placeholder variable added by the curly braces can then be resolved statically or dynamically, or built by an aspect (more commonly known as a "mapper").

It is possible to use the same enhancer multiple times with different configurations. Be aware that it is not possible to combine multiple variants / enhancers matching multiple configurations.

However, custom enhancers can be created to overcome special use cases, for example, when two plugins with multiple parameters each could be configured. Otherwise, the first variant matching the URL parameters is used for generation and resolving.

Enhancers

There are two types of enhancers: decorators and route enhancers. A route enhancer is there to replace a set of placeholders and fill in URL parameters during URL generation and resolve them properly later. The substitution of values with aliases can be achieved by aspects. To simplify, a route enhancer specifies how the full route path looks like and which variables are available, whereas an aspect takes care of mapping a single variable to a value.

TYPO3 comes with the following route enhancers out of the box:

TYPO3 provides the following decorator out of the box:

Custom enhancers can be registered by adding an entry to an extension's ext_localconf.php file:

$GLOBALS['TYPO3_CONF_VARS']['SYS']['routing']['enhancers']['CustomEnhancer']
    = \MyVendor\MyPackage\Routing\CustomEnhancer::class;
Copied!

Within a configuration, an enhancer always evaluates the following properties:

type
The short name of the enhancer as registered within $GLOBALS['TYPO3_CONF_VARS']. This is mandatory.
limitToPages
An array of page IDs where this enhancer should be called. This is optional. This property (array) triggers an enhancer only for specific pages. In case of special plugin pages, it is recommended to enhance only those pages with the plugin to speed up performance of building page routes of all other pages.

All enhancers allow to configure at least one route with the following configuration:

defaults
Defines which URL parameters are optional. If the parameters are omitted during generation, they can receive a default value and do not need a placeholder - it is possible to add them at the very end of the routePath.
requirements

Specifies exactly what kind of parameter should be added to that route as a regular expression. This way it is configurable to allow only integer values, e.g. for pagination.

Make sure you define your requirements as strict as possible. This is necessary so that performance is not reduced and to allow TYPO3 to match the expected route.

_arguments
Defines what route parameters should be available to the system. In the following example, the placeholder is called category_id, but the URL generation receives the argument category. It is mapped to that name (so you can access/use it as category in your custom code).

TYPO3 will add the parameter cHash to URLs when necessary, see Caching variants - or: What is a "cache hash"?. The cHash can be removed by converting dynamic arguments into static arguments. All captured arguments are dynamic by default. They can be converted to static arguments by defining the possible expected values for these arguments. This is done by adding aspects for those arguments to provide a static list of expected values.

Simple enhancer

The simple enhancer works with various route arguments to map them to an argument to be used later.

index.php?id=13&category=241&tag=Benni
Copied!

results in

https://example.org/path-to/my-page/show-by-category/241/Benni
Copied!

The configuration looks like this:

routeEnhancers:
  # Unique name for the enhancers, used internally for referencing
  CategoryListing:
    type: Simple
    limitToPages: [13]
    routePath: '/show-by-category/{category_id}/{tag}'
    defaults:
      tag: ''
    requirements:
      category_id: '[0-9]{1,3}'
      tag: '[a-zA-Z0-9]+'
    _arguments:
      category_id: 'category'
Copied!

The configuration option routePath defines the static keyword and the available placeholders.

Plugin enhancer

The plugin enhancer works with plugins based on the class AbstractPlugin, also known as "pi-based plugins".

In this example we will map the raw parameters of an URL like this:

https://example.org/path-to/my-page?id=13&tx_felogin_pi1[forgot]=1&tx_felogin_pi1[user]=82&tx_felogin_pi1[hash]=ABCDEFGHIJKLMNOPQRSTUVWXYZ012345
Copied!

The result will be an URL like this:

https://example.org/path-to/my-page/forgot-password/82/ABCDEFGHIJKLMNOPQRSTUVWXYZ012345
Copied!

The base for the plugin enhancer is the configuration of a so-called "namespace", in this case tx_felogin_pi1 - the plugin's namespace.

The plugin enhancer explicitly sets exactly one additional variant for a specific use case. In case of the frontend login, we would need to set up multiple configurations of the plugin enhancer for "forgot password" and "recover password".

routeEnhancers:
  ForgotPassword:
    type: Plugin
    limitToPages: [13]
    routePath: '/forgot-password/{user}/{hash}'
    namespace: 'tx_felogin_pi1'
    defaults:
      forgot: '1'
    requirements:
      user: '[0-9]{1,3}'
      hash: '^[a-zA-Z0-9]{32}$'
Copied!

If a URL is generated with the given parameters to link to a page, the result will look like this:

https://example.org/path-to/my-page/forgot-password/82/ABCDEFGHIJKLMNOPQRSTUVWXYZ012345
Copied!

As you see, the plugin enhancer is used to specify placeholders and requirements with a given namespace.

If you want to replace the user ID (in this example "82") with the username, you would need an aspect that can be registered within any enhancer, see below for details.

Extbase plugin enhancer

When creating Extbase plugins, it is very common to have multiple controller/action combinations. Therefore, the Extbase plugin enhancer is an extension to the regular plugin enhancer and provides the functionality to generate multiple variants, typically based on the available controller/action pairs.

The Extbase plugin enhancer with the configuration below would now apply to the following URLs:

index.php?id=13&tx_news_pi1[controller]=News&tx_news_pi1[action]=list
index.php?id=13&tx_news_pi1[controller]=News&tx_news_pi1[action]=list&tx_news_pi1[page]=5
index.php?id=13&tx_news_pi1[controller]=News&tx_news_pi1[action]=list&tx_news_pi1[year]=2018&tx_news_pi1[month]=8
index.php?id=13&tx_news_pi1[controller]=News&tx_news_pi1[action]=detail&tx_news_pi1[news]=13
index.php?id=13&tx_news_pi1[controller]=News&tx_news_pi1[action]=tag&tx_news_pi1[tag]=11
Copied!

And generate the following URLs:

https://example.org/path-to/my-page/list/
https://example.org/path-to/my-page/list/5
https://example.org/path-to/my-page/list/2018/8
https://example.org/path-to/my-page/detail/in-the-year-2525
https://example.org/path-to/my-page/tag/future
Copied!
routeEnhancers:
  NewsPlugin:
    type: Extbase
    limitToPages: [13]
    extension: News
    plugin: Pi1
    routes:
      - routePath: '/list/'
        _controller: 'News::list'
      - routePath: '/list/{page}'
        _controller: 'News::list'
        _arguments:
          page: '@widget_0/currentPage'
      - routePath: '/detail/{news_title}'
        _controller: 'News::detail'
        _arguments:
          news_title: 'news'
      - routePath: '/tag/{tag_name}'
        _controller: 'News::list'
        _arguments:
          tag_name: 'overwriteDemand/tags'
      - routePath: '/list/{year}/{month}'
        _controller: 'News::list'
        _arguments:
          year: 'overwriteDemand/year'
          month: 'overwriteDemand/month'
        requirements:
          year: '\d+'
          month: '\d+'
    defaultController: 'News::list'
    defaults:
      page: '0'
    aspects:
      news_title:
        type: PersistedAliasMapper
        tableName: tx_news_domain_model_news
        routeFieldName: path_segment
      page:
        type: StaticRangeMapper
        start: '1'
        end: '100'
      month:
        type: StaticRangeMapper
        start: '1'
        end: '12'
      year:
        type: StaticRangeMapper
        start: '1984'
        end: '2525'
      tag_name:
        type: PersistedAliasMapper
        tableName: tx_news_domain_model_tag
        routeFieldName: slug
Copied!

In this example, the _arguments parameter is used to set sub-properties of an array, which is typically used within demand objects for filtering functionality. Additionally, it is using both the short and the long form of writing route configurations.

To understand what's happening in the aspects part, read on.

PageType decorator

The PageType enhancer (decorator) allows to add a suffix to the existing route (including existing other enhancers) to map a page type (GET parameter &type=) to a suffix.

It is possible to map various page types to endings:

Example in TypoScript:

page = PAGE
page.typeNum = 0
page.10 = TEXT
page.10.value = Default page

rssfeed = PAGE
rssfeed.typeNum = 13
rssfeed.10 < plugin.tx_myplugin
rssfeed.config.disableAllHeaderCode = 1
rssfeed.config.additionalHeaders.10.header = Content-Type: xml/rss

jsonview = PAGE
jsonview.typeNum = 26
jsonview.config.disableAllHeaderCode = 1
jsonview.config.additionalHeaders.10.header = Content-Type: application/json
jsonview.10 = USER
jsonview.10.userFunc = MyVendor\MyExtension\Controller\JsonPageController->renderAction
Copied!

Now we configure the enhancer in your site's config.yaml file like this:

routeEnhancers:
  PageTypeSuffix:
    type: PageType
    default: ''
    map:
      'rss.feed': 13
      '.json': 26
Copied!

The map allows to add a filename or a file ending and map this to a page.typeNum value.

It is also possible to set default, for example to .html to add a ".html" suffix to all default pages:

routeEnhancers:
  PageTypeSuffix:
    type: PageType
    default: '.html'
    index: 'index'
    map:
      'rss.feed': 13
      '.json': 26
Copied!

The index property is used when generating links on root-level page, so instead of having /en/.html it would then result in /en/index.html.

Aspects

Now that we have looked at how to extend a route to a page with arguments and insert them as segments in the URL, the detailed logic within a placeholder is in an aspect. The most common practice of an aspect is called a mapper. For example, a parameter {news}, which is a UID within TYPO3, is mapped to the actual news slug, which is a field within the database table containing the cleaned/sanitized title of the news (for example, "software-updates-2022" maps to news ID 10).

An aspect can be a way to modify, beautify or map an argument from the URL generation into a placeholder. That's why the terms "mapper" and "modifier" will pop up, depending on the different cases.

Aspects are registered within one single enhancer configuration with the option aspects and can be used with any enhancer.

Let us start with some examples first:

StaticValueMapper

The static value mapper replaces values on a 1:1 mapping list of an argument into a speaking segment, useful for a checkout process to define the steps into "cart", "shipping", "billing", "overview" and "finish", or in another example to create human-readable segments for all available months.

The configuration could look like this:

routeEnhancers:
  NewsArchive:
    type: Extbase
    limitToPages: [13]
    extension: News
    plugin: Pi1
    routes:
      - { routePath: '/{year}/{month}', _controller: 'News::archive' }
    defaultController: 'News::list'
    defaults:
      month: ''
    aspects:
      month:
        type: StaticValueMapper
        map:
          january: 1
          february: 2
          march: 3
          april: 4
          may: 5
          june: 6
          july: 7
          august: 8
          september: 9
          october: 10
          november: 11
          december: 12
Copied!

You see the placeholder month where the aspect replaces the value to a human-readable URL path segment.

It is possible to add an optional localeMap to that aspect to use the locale of a value to use in multi-language setups:

routeEnhancers:
  NewsArchive:
    type: Extbase
    limitToPages: [13]
    extension: News
    plugin: Pi1
    routes:
      - { routePath: '/{year}/{month}', _controller: 'News::archive' }
    defaultController: 'News::list'
    defaults:
      month: ''
    aspects:
      month:
        type: StaticValueMapper
        map:
          january: 1
          february: 2
          march: 3
          april: 4
          may: 5
          june: 6
          july: 7
          august: 8
          september: 9
          october: 10
          november: 11
          december: 12
        localeMap:
          - locale: 'de_.*'
            map:
              januar: 1
              februar: 2
              maerz: 3
              april: 4
              mai: 5
              juni: 6
              juli: 7
              august: 8
              september: 9
              oktober: 10
              november: 11
              dezember: 12
Copied!

LocaleModifier

The enhanced part of a route path could be /archive/{year}/{month} - however, in multi-language setups, it should be possible to rename /archive/ depending on the language that is given for this page translation. This modifier is a good example where a route path is modified, but is not affected by arguments.

The configuration could look like this:

routeEnhancers:
  NewsArchive:
    type: Extbase
    limitToPages: [13]
    extension: News
    plugin: Pi1
    routes:
      - { routePath: '/{localized_archive}/{year}/{month}', _controller: 'News::archive' }
    defaultController: 'News::list'
    aspects:
      localized_archive:
        type: LocaleModifier
        default: 'archive'
        localeMap:
          - locale: 'fr_FR.*|fr_CA.*'
            value: 'archives'
          - locale: 'de_DE.*'
            value: 'archiv'
Copied!

You will see the placeholder localized_archive where the aspect replaces the localized archive based on the locale of the language of that page.

StaticRangeMapper

A static range mapper allows to avoid the cHash and narrow down the available possibilities for a placeholder. It explicitly defines a range for a value, which is recommended for all kinds of pagination functionality.

routeEnhancers:
  NewsPlugin:
    type: Extbase
    limitToPages: [13]
    extension: News
    plugin: Pi1
    routes:
      - { routePath: '/list/{page}', _controller: 'News::list', _arguments: {'page': '@widget_0/currentPage'} }
    defaultController: 'News::list'
    defaults:
      page: '0'
    aspects:
      page:
        type: StaticRangeMapper
        start: '1'
        end: '100'
Copied!

This limits down the pagination to a maximum of 100 pages. If a user calls the news list with page 101, the route enhancer does not match and would not apply the placeholder.

PersistedAliasMapper

If an extension ships with a slug field or a different field used for the speaking URL path, this database field can be used to build the URL:

routeEnhancers:
  NewsPlugin:
    type: Extbase
    limitToPages: [13]
    extension: News
    plugin: Pi1
    routes:
      - { routePath: '/detail/{news_title}', _controller: 'News::detail', _arguments: {'news_title': 'news'} }
    defaultController: 'News::detail'
    aspects:
      news_title:
        type: PersistedAliasMapper
        tableName: 'tx_news_domain_model_news'
        routeFieldName: 'path_segment'
        routeValuePrefix: '/'
Copied!

The persisted alias mapper looks up the table and the field to map the given value to a URL. The property tableName points to the database table, the property routeFieldName is the field which will be used within the route path, in this example path_segment.

The special routeValuePrefix is used for TCA type slug fields where the prefix / is within all fields of the field names, which should be removed in the case above.

If a field is used for routeFieldName that is not prepared to be put into the route path, e.g. the news title field, you must ensure that this is unique and suitable for the use in an URL. On top, special characters like spaces will not be converted automatically. Therefore, usage of a slug TCA field is recommended.

PersistedPatternMapper

When a placeholder should be fetched from multiple fields of the database, the persisted pattern mapper is for you. It allows to combine various fields into one variable, ensuring a unique value, for example by adding the UID to the field without having the need of adding a custom slug field to the system.

routeEnhancers:
  Blog:
    type: Extbase
    limitToPages: [13]
    extension: BlogExample
    plugin: Pi1
    routes:
      - { routePath: '/blog/{blogpost}', _controller: 'Blog::detail', _arguments: {'blogpost': 'post'} }
    defaultController: 'Blog::detail'
    aspects:
      blogpost:
        type: PersistedPatternMapper
        tableName: 'tx_blogexample_domain_model_post'
        routeFieldPattern: '^(?P<title>.+)-(?P<uid>\d+)$'
        routeFieldResult: '{title}-{uid}'
Copied!

The routeFieldPattern option builds the title and uid fields from the database, the routeFieldResult shows how the placeholder will be output. However, as mentioned above special characters in the title might still be a problem. The persisted pattern mapper might be a good choice if you are upgrading from a previous version and had URLs with an appended UID for uniqueness.

Aspect precedence

Route requirements are ignored for route variables having a corresponding setting in aspects. Imagine an aspect that is mapping an internal value 1 to route value one and vice versa - it is not possible to explicitly define the requirements for this case - which is why aspects take precedence.

The following example illustrates the mentioned dilemma between route generation and resolving:

routeEnhancers:
  MyPlugin:
    type: 'Plugin'
    namespace: 'my'
    routePath: 'overview/{month}'
    requirements:
      # note: it does not make any sense to declare all values here again
      month: '^(\d+|january|february|march|april|...|december)$'
    aspects:
      month:
        type: 'StaticValueMapper'
        map:
          january: '1'
          february: '2'
          march: '3'
          april: '4'
          may: '5'
          june: '6'
          july: '7'
          august: '8'
          september: '9'
          october: '10'
          november: '11'
          december: '12'
Copied!

The map in the previous example is already defining all valid values. That is why aspects take precedence over requirements for a specific routePath definition.

Behind the Scenes

While accessing a page in TYPO3 in the frontend, all arguments are currently built back into the global GET parameters, but are also available as so-called \TYPO3\CMS\Core\Routing\PageArguments object. The PageArguments object is then used to sign and verify the parameters, to ensure that they are valid, when handing them further down the frontend request chain.

If there are dynamic parameters (= parameters which are not strictly limited), a verification GET parameter cHash is added, which can and should not be removed from the URL. The concept of manually activating or deactivating the generation of a cHash is not optional anymore, but strictly built-in to ensure proper URL handling. If you really have the requirement to not have a cHash argument, ensure that all placeholders are having strict definitions on what could be the result of the page segment (e.g. pagination), and feel free to build custom mappers.

All existing APIs like typolink or functionality evaluate the page routing API directly.