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.

Enhancers and aspects are an important concept in TYPO3 and they are used to map GET parameters to routes.

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

Aspects can be registered for a specific enhancer to modify placeholders, adding static, human readable names within the route path or dynamically generated values.

To give you an overview of what the distinction is, imagine a web page which is available at

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

(the path mirrors the page structure in the backend) and has page ID 13.

Enhancers can transform this route to:

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

An enhancer adds the suffix /products/<product-name> to the base route of the page. The enhancer uses a placeholder variable which is resolved statically, dynamically or built by an aspect or "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 for 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 generating and resolving the route.

Enhancers

There are two types of enhancers: route decorators and route enhancers. A route enhancer replaces a set of placeholders, inserts URL parameters during URL generation and then resolves them properly later. The substitution of values with aliases can be done by aspects. To simplify, a route enhancer specifies what the full route path looks like and which variables are available, whereas an aspect maps a single variable to a value.

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

TYPO3 provides the following route decorator out of the box:

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

EXT:my_extension/ext_localconf.php
<?php

declare(strict_types=1);

use MyVendor\MyPackage\Routing\CustomEnhancer;

defined('TYPO3') or die();

$GLOBALS['TYPO3_CONF_VARS']['SYS']['routing']['enhancers']['CustomEnhancer']
    = 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 expressions. This way it is configurable to allow only integer values, for example 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 route arguments. It maps them to an argument to make a URL that can 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!
routePath
defines the static keyword and the placeholders.
requirements
defines parts that should be replaced in the routePath. Regular expressions limit the allowed chars to be used in those parts.
_arguments
defines the mapping from the placeholder in the routePath to the name of the parameter in the URL as it would appear without enhancement. Note that it is also possible to map to nested parameters by providing a path-like parameter name. For example, specifying my_array/my_key as the parameter name would set the GET parameter my_array[my_key] to the value of the specified placeholder.

Plugin enhancer

The plugin enhancer works with plugins based on Core functionality.

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 variation for a specific use case. For the frontend login, we would need to set up two 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 above parameters the resulting link will be 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 a 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.

Instead of using the combination of extension and plugin one can also provide the namespace property as in the regular plugin enhancer:

routeEnhancers:
  NewsPlugin:
    type: Extbase
    limitToPages: [13]
    namespace: tx_news_pi1
    # ... further configuration
Copied!

Changed in version 12.2

Prior to version 12.2 the combination of extension and plugin was preferred when all three properties are given. Since v12.2 the namespace property has precedence.

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

PageType decorator

The PageType enhancer (route 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 transform a route to a page by using arguments inserted into a URL, we will look at aspects. An aspect handles the detailed logic within placeholders. The most common part of an aspect is called a mapper. For example, parameter {news}, is a UID within TYPO3, and is mapped to the current 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 is a way to modify, beautify or map an argument into a placeholder. That's why the terms "mapper" and "modifier" will pop up, depending on the different cases.

Aspects are registered within a 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

If we have an enhanced route path such as /archive/{year}/{month} it should be possible in multi-language setups to change /archive/ depending on the language of the page. This modifier is a good example where a route path is modified, but 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!

This aspect replaces the placeholder localized_archive depending 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.

Aspect fallback value handling

New in version 12.1

Imagine a route like /news/{news_title} that has been filled with an "invalid" value for the news_title part. Often these are outdated, deleted or hidden records. Usually TYPO3 reacts to these "invalid" URL sections at a very early stage with an HTTP status code "404" (resource not found).

The property fallbackValue = [string|null] can prevent the above scenario in several ways. By specifying an alternative value, a different record, language or other detail can be represented. Specifying null removes the corresponding parameter from the route result. In this way, it is up to the developer to react accordingly.

In the case of Extbase extensions, the developer can define the parameters in his calling controller action as nullable and deliver corresponding flash messages that explain the current scenario better than a "404" HTTP status code.

Examples

routeEnhancers:
  NewsPlugin:
    type: Extbase
    extension: News
    plugin: Pi1
    routes:
      - routePath: '/detail/{news_title}'
        _controller: 'News::detail'
        _arguments:
          news_title: 'news'
    aspects:
      news_title:
        type: PersistedAliasMapper
        tableName: tx_news_domain_model_news
        routeFieldName: path_segment

        # A string value leads to parameter `&tx_news_pi1[news]=0`
        fallbackValue: '0'

        # A null value leads to parameter `&tx_news_pi1[news]` being removed
        # fallbackValue: null
Copied!

Custom mapper implementations can incorporate this behavior by implementing the \TYPO3\CMS\Core\Routing\Aspect\UnresolvedValueInterface which is provided by \TYPO3\CMS\Core\Routing\Aspect\UnresolvedValueTrait :

EXT:my_extension/Classes/Routing/Enhancer/MyCustomEnhancer.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Routing;

use TYPO3\CMS\Core\Routing\Aspect\MappableAspectInterface;
use TYPO3\CMS\Core\Routing\Aspect\UnresolvedValueInterface;
use TYPO3\CMS\Core\Routing\Aspect\UnresolvedValueTrait;

final class MyCustomEnhancer implements MappableAspectInterface, UnresolvedValueInterface
{
    use UnresolvedValueTrait;

    public function generate(string $value): ?string
    {
        // TODO: Implement generate() method.
    }

    public function resolve(string $value): ?string
    {
        // TODO: Implement resolve() method.
    }
}
Copied!

In another example we handle the null value in an Extbase show action separately, for instance, to redirect to the list page:

EXT:my_extension/Classes/Controller/MyController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Domain\Model\MyModel;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

class MyController extends ActionController
{
    public function showAction(?MyModel $myModel = null): ResponseInterface
    {
        if ($myModel === null) {
            return $this->redirect('somethingElse');
        }

        return $this->htmlResponse();
    }
}
Copied!

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.