.. include:: /Includes.rst.txt
.. highlight:: yaml

.. _feature-86365:

===============================================
Feature: #86365 - Routing Enhancers and Aspects
===============================================

See :issue:`86365`

Description
===========

Page-based routing is now flexible by adding enhancers to routes that are generated or resolved with parameters, which
were previously appended as GET parameters.

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

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

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

`https://www.example.com/path-to/my-page`

to access the Page with ID 13.

Enhancers are ways to extend this route with placeholders on top of this specific route to a page.

`https://www.example.com/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
which is added by the curly braces can then be statically or dynamically resolved or built by an Aspect or more
commonly known a Mapper.

Enhancers and aspects are activated and configured in a site configuration, currently possible by modifying the
site's :file:`config.yaml` and adding the :yaml:`routeEnhancers` section manually, as there is no UI available for
this configuration. See examples below.

It is possible to use the same enhancers multiple times with different configurations, however, be aware that
it is not possible to combine multiple variants / enhancers that match multiple configurations.

However, custom enhancers can be built to overcome special use cases where e.g. two plugins with multiple parameters
each could be configured. Otherwise, the first variant that matches the URL parameters is used for generation and
resolving.

Enhancers
^^^^^^^^^

TYPO3 comes with the following enhancers out of the box:

- Simple Enhancer (enhancer type "Simple")
- Plugin Enhancer (enhancer type "Plugin")
- Extbase Plugin Enhancer (enhancer type "Extbase")

Custom enhancers can be registered by adding an entry to an extensions :file:`ext_localconf.php`.

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

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

* `type` - the short name of the enhancer as registered within :php:`$TYPO3_CONF_VARS`. This is mandatory.
* `limitToPages` - an array of page IDs where this enhancer should be called. This is **optional**. This property (array)
  evaluates to only trigger an enhancer for specific pages. In case of special plugin pages it is
  useful to only enhance pages with IDs, to speed up performance for building page routes of all other pages.

Simple Enhancer
---------------

The Simple Enhancer works with various route arguments to map them to an argument to be used later-on.

`index.php?id=13&category=241&tag=Benni`
results in
`https://www.example.com/path-to/my-page/show-by-category/241/Benni`

The configuration looks like this:

.. code-block:: yaml

   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'

The configuration option `routePath` defines the static keyword (previously known to some as "postVarSets" keyword for
some TYPO3 folks), and the available placeholders.

The `defaults` section defines which URL parameters are optional. If the parameters are omitted on generation, they
can receive a default value, and do not need a placeholder - it is also possible to add them at the very end of the
`routePath`.

The `requirements` section exactly specifies what kind of parameter should be added to that route as regular expression.
This way, it is configurable to only allow integer values for e.g. pagination. If the requirements are too loose, a
URL signature parameter ("cHash") is added to the end of the URL which cannot be removed.

The `_arguments` section defines what Route Parameters should be available to the system. In this example, the
placeholder is called `category_id` but the URL generation receives the argument `category`, so this is mapped to
this very name.

An enhancer is only there to replace a set of placeholders and fill in URL parameters or resolve them properly
later-on, but not to substitute the values with aliases, this can be achieved by Aspects.


Plugin Enhancer
---------------

The Plugin Enhancer works with plugins on a page that are commonly known as `Pi-Based Plugins`, where previously
the following GET/POST variables were used:

* `index.php?id=13&tx_felogin_pi1[forgot]=1` (form to request token)
* `index.php?id=13&tx_felogin_pi1[user]=82&tx_felogin_pi1[hash]=12345679%7CABCDEFGHIJKLMNOPQRSTUVWXYZ012345`
  (form to actually recover password, `%7C` is URL-encoded pipe character `|`)

The base for the plugin enhancer is to configure 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 Frontend Login,
we would need to set up multiple configurations of Plugin Enhancer for forgot and recover passwords.

.. code-block:: yaml

   routeEnhancers:
     ForgotPasswordForm:
       type: Plugin
       limitToPages: [13]
       routePath: '/forgot-password/{forgot}'
       namespace: 'tx_felogin_pi1'
       defaults:
         forgot: '1'
       requirements:
         forgot: '1'
     ForgotPasswordRecover:
       type: Plugin
       limitToPages: [13]
       routePath: '/forgot-password/{user}/{forgothash}'
       namespace: 'tx_felogin_pi1'
       requirements:
         user: '[0-9]{1,3}'
         forgothash: '\d+\|[[:xdigit:]]{32}'

If URLs are generated with the given parameters to link to a page, the results will look like this:

* `https://www.example.com/path-to/my-page/forgot-password`
  (for `index.php?id=13&tx_felogin_pi1[forgot]=1`)
* `https://www.example.com/path-to/my-page/forgot-password/82/12345679%7CABCDEFGHIJKLMNOPQRSTUVWXYZ012345`
  (for `index.php?id=13&tx_felogin_pi1[user]=82&tx_felogin_pi1[hash]=12345679%7CABCDEFGHIJKLMNOPQRSTUVWXYZ012345`)

If the input given to generate the URL does not meet the requirements, the route enhancer does not offer the
variant and the parameters are added to the URL as regular query parameters. If e.g. the user parameter would be more
than three characters, or non-numeric, this enhancer would not match anymore.

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, but see below for details on Aspects.


Extbase Plugin Enhancer
-----------------------

When creating extbase plugins, it is very common to have multiple controller/action combinations. The Extbase Plugin
Enhancer is therefore an extension to the regular Plugin Enhancer, providing the functionality that multiple variants
are generated, typically built on the amount of controller/action pairs.

The `namespace` option is omitted, as this is built with `extension` and `plugin` name.

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]=detail&tx_news_pi1[news]=13`
* `index.php?id=13&tx_news_pi1[controller]=News&tx_news_pi1[action]=archive&tx_news_pi1[year]=2018&&tx_news_pi1[month]=8`

And generate the following URLs

* `https://www.example.com/path-to/my-page/list/`
* `https://www.example.com/path-to/my-page/list/5`
* `https://www.example.com/path-to/my-page/detail/13`
* `https://www.example.com/path-to/my-page/archive/2018/8`

.. code-block:: yaml

   routeEnhancers:
     NewsPlugin:
       type: Extbase
       limitToPages: [13]
       extension: News
       plugin: Pi1
       routes:
         - { routePath: '/list/{page}', _controller: 'News::list', _arguments: {'page': '@widget_0/currentPage'} }
         - { routePath: '/tag/{tag_name}', _controller: 'News::list', _arguments: {'tag_name': 'overwriteDemand/tags'}}
         - { routePath: '/detail/{news_title}', _controller: 'News::detail', _arguments: {'news_title': 'news'} }
         - { routePath: '/archive/{year}/{month}', _controller: 'News::archive' }
       defaultController: 'News::list'
       defaults:
         page: '1'
       requirements:
         page: '\d+'

In this example, you also see that the `_arguments` parameter can be used to bring them into sub properties of an array,
which is typically the case within demand objects for filtering functionality.

For the Extbase Plugin Enhancer, it is also possible to configure the namespace directly by skipping `extension`
and `plugin` properties and just using the `namespace` property as in the regular Plugin Enhancer.

Aspects
^^^^^^^

Now that we've looked into ways on how to extend a route to a page with arguments, and to put them into the URL
path as segments, the detailed logic within one placeholder is in an aspect. The most common practice of an aspect
is a so-called mapper. Map `{news_title}` which is a UID within TYPO3 to the actual news title, which is a field
within the database table.

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.

.. note::
   Values in `defaults` and `requirements` always focus on corresponding internal raw values and not on
   generated route values.

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

Let's start with some simpler examples first:


StaticValueMapper
-----------------

The StaticValueMapper replaces values simply 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 a
simpler example to create human readable segments for all available months.

The configuration could look like this:

.. code-block:: yaml

   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'll 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.

.. code-block:: yaml

    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
--------------

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:

.. code-block:: yaml

   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'

You'll 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,
and to explicitly define a range for a value, which is recommended for all kinds of pagination functionality.

.. code-block:: yaml

   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'
       requirements:
         page: '\d+'
       aspects:
         page:
           type: StaticRangeMapper
           start: '1'
           end: '100'

This limits down the pagination to max. 100 pages, if a user calls the news list with page 101, then 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:

.. code-block:: yaml

    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 PersistedAliasMapper looks up (via a so-called delegate pattern under the hood) to map the given value to a
a URL. The property `tableName` points to the database table, property `routeFieldName` is the field which will be
used within the route path for example.

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,
it still must be ensured that this is unique. On top, if there are special characters like spaces they will be
URL-encoded, to ensure a definitive value, a slug TCA field is recommended.

PersistedPatternMapper
----------------------

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

.. code-block:: yaml

   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 `routeFieldPattern` option builds the title and uid fields from the database, the `routeFieldResult` shows
how the placeholder will be output.

Impact
======

Some notes to the implementation:

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 `PageArguments` object, which is then used to be signed and verified
that they are valid, when handing them to process a frontend request further.

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 never 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.

Setting the TypoScript option `typolink.useCacheHash` is not necessary anymore when running with a site configuration.

Please note that Enhancers and Page-based routing is only available for pages that are built with a site configuration.

All existing APIs like `typolink` or functionality evaluate the new Page Routing API directly and come with route
enhancers.

Please note that if you update the Site configuration with enhancers that you need to clear all caches.

.. index:: Frontend, PHP-API
