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.
Note
There is no graphical user interface for configuring extended
routing. All adjustments need to be made by manually editing your website's
config.
site configuration file (located in
config/
).
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://
(the path mirrors the page structure in the backend) and has page ID 13.
Enhancers can transform this route to:
https://
An enhancer adds the suffix /products/<product-
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
Tip
See Key Terminology for an introduction to the used terminology here.
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:
- Simple enhancer
(enhancer type
Simple
) - Plugin enhancer
(enhancer type
Plugin
) - Extbase plugin enhancer
(enhancer type
Extbase
)
TYPO3 provides the following route decorator out of the box:
- Page type decorator
(enhancer type
Page
)Type
Custom enhancers can be registered by adding an entry to an extension's
ext_
file:
<?php
declare(strict_types=1);
use MyVendor\MyPackage\Routing\CustomEnhancer;
defined('TYPO3') or die();
$GLOBALS['TYPO3_CONF_VARS']['SYS']['routing']['enhancers']['CustomEnhancer']
= CustomEnhancer::class;
Within a configuration, an enhancer always evaluates the following properties:
type
- The short name of the enhancer as registered within
$GLOBALS
. This is mandatory.['TYPO3_ CONF_ VARS'] limit
To Pages - 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
route
.Path 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_
, but the URL generation receives the argumentid category
. It is mapped to that name (so you can access/use it ascategory
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
results in
https://example.org/path-to/my-page/show-by-category/241/Benni
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'
route
Path - defines the static keyword and the placeholders.
requirements
- defines parts that should be replaced in the
route
. Regular expressions limit the allowed chars to be used in those parts.Path _arguments
- defines the mapping from the placeholder in the
route
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, specifyingPath my_
as the parameter name would set the GET parameterarray/ my_ key my_
to the value of the specified placeholder.array [my_ key]
Note
For people coming from
dmitryd/typo3-realurl
in previous TYPO3 versions: The
route
can be loosely compared to some as "postVarSets".
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
The result will be an URL like this:
https://example.org/path-to/my-page/forgot-password/82/ABCDEFGHIJKLMNOPQRSTUVWXYZ012345
The base for the plugin enhancer is the configuration of a so-called
"namespace", in this case tx_
- 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}$'
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
Note
If the input given to generate the URL does not match, the route enhancer is not triggered, and the parameters are added to the URL as normal query parameters. For example, if the user parameter is more than three characters or non-numeric, this enhancer would not match.
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
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
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
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
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.
Attention
Please ensure not to register the same route
more than once, for
example through multiple extensions. In that case, the enhancer imported
last will override any duplicate routes that are in place.
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
Now we configure the enhancer in your site's config.
file like this:
routeEnhancers:
PageTypeSuffix:
type: PageType
default: ''
map:
'rss.feed': 13
'.json': 26
The map
allows to add a filename or a file ending and map this to
a page.
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
The index
property is used when generating links on root-level page,
so instead of having /en/.
it would then result in
/en/
.
Note
The implementation is a decorator enhancer, which means that the PageType enhancer is only there for adding suffixes to an existing route / variant, but not to substitute something within the middle of a human-readable URL segment.
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
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 locale
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
LocaleModifier
If we have an enhanced route path such as /archive/
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'
This aspect replaces the placeholder localized_
depending on the
locale of the language of that page.
StaticRangeMapper
A static range mapper allows to avoid the c
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'
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.
Note
A range larger than 1000 is not allowed.
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: '/'
The persisted alias mapper looks up the table and the field to map the given
value to a URL. The property table
points to the database table,
the property route
is the field which will be used within the
route path, in this example path_
.
The special route
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 route
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}'
The route
option builds the title and uid fields from the
database, the route
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'
The map
in the previous example is already defining all valid values.
That is why aspects
take precedence over requirements
for a
specific route
definition.
Aspect fallback value handling
New in version 12.1
Imagine a route like /news/
that has been filled with an "invalid"
value for the news_
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 fallback
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
Custom mapper implementations can incorporate this behavior by implementing
the \TYPO3\
which is
provided by \TYPO3\
:
<?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.
}
}
In another example we handle the null value in an Extbase show action separately, for instance, to redirect to the list page:
<?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();
}
}
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\
object. The Page
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 c
is added, which can and should not be
removed from the URL. The concept of manually activating or deactivating
the generation of a c
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.
Note
If you update the site configuration with enhancers you have to to clear all caches, for example via the upper menu bar in the backend.