Collection of various routing examples

EXT: News

Prerequisites:

The plugins for list view and detail view are on separate pages. If you use the category menu or tag list plugins to filter news records, their titles (slugs) are used.

Result:

  • Detail view: https://example.org/news/detail/the-news-title
  • Pagination: https://example.org/news/page-2
  • Category filter: https://example.org/news/my-category
  • Tag filter: https://example.org/news/my-tag
config/my_site/config.yaml (excerpt)
# ...
routeEnhancers:
  News:
    type: Extbase
    extension: News
    plugin: Pi1
    routes:
      - routePath: '/page-{page}'
        _controller: 'News::list'
        _arguments:
          page: currentPage
      - routePath: '/{news-title}'
        _controller: 'News::detail'
        _arguments:
          news-title: news
      - routePath: '/{category-name}'
        _controller: 'News::list'
        _arguments:
          category-name: overwriteDemand/categories
      - routePath: '/{tag-name}'
        _controller: 'News::list'
        _arguments:
          tag-name: overwriteDemand/tags
    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'
      category-name:
        type: PersistedAliasMapper
        tableName: sys_category
        routeFieldName: slug
      tag-name:
        type: PersistedAliasMapper
        tableName: tx_news_domain_model_tag
        routeFieldName: slug
Copied!

For more examples and background information see the routing examples in the "News" manual.

EXT: Blog with custom aspect

Taken from https://typo3.com routing configuration and the blog extension.

Blog Archive:

config/my_site/config.yaml (excerpt)
# ...
routeEnhancers:
  BlogArchive:
    type: Extbase
    extension: Blog
    plugin: Archive
    routes:
      -
        routePath: '/{year}'
        _controller: 'Post::listPostsByDate'
        _arguments:
          year: year
      -
        routePath: '/{year}/page-{page}'
        _controller: 'Post::listPostsByDate'
        _arguments:
          year: year
          page: '@widget_0/currentPage'
      -
        routePath: '/{year}/{month}'
        _controller: 'Post::listPostsByDate'
        _arguments:
          year: year
          month: month
      -
        routePath: '/{year}/{month}/page-{page}'
        _controller: 'Post::listPostsByDate'
        _arguments:
          year: year
          month: month
          page: '@widget_0/currentPage'
    defaultController: 'Post::listPostsByDate'
    aspects:
      year:
        type: BlogStaticDatabaseMapper
        table: 'pages'
        field: 'crdate_year'
        groupBy: 'crdate_year'
        where:
        doktype: 137
      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
          -
            locale: 'fr_.*'
            map:
            janvier: 1
            fevrier: 2
            mars: 3
            avril: 4
            mai: 5
            juin: 6
            juillet: 7
            aout: 8
            septembre: 9
            octobre: 10
            novembre: 11
            decembre: 12
      page:
        type: StaticRangeMapper
        start: '1'
        end: '99'
Copied!

Posts by Author:

config/my_site/config.yaml (excerpt)
# ...
routeEnhancers:
  AuthorPosts:
    type: Extbase
    extension: Blog
    plugin: AuthorPosts
    routes:
      -
        routePath: '/{author_title}'
        _controller: 'Post::listPostsByAuthor'
        _arguments:
          author_title: author
      -
        routePath: '/{author_title}/page-{page}'
        _controller: 'Post::listPostsByAuthor'
        _arguments:
          author_title: author
          page: '@widget_0/currentPage'
    defaultController: 'Post::listPostsByAuthor'
    aspects:
      author_title:
        type: PersistedAliasMapper
        tableName: 'tx_blog_domain_model_author'
        routeFieldName: 'slug'
      page:
        type: StaticRangeMapper
        start: '1'
        end: '99'
Copied!

Category pages:

config/my_site/config.yaml (excerpt)
# ...
routeEnhancers:
  BlogCategory:
    type: Extbase
    extension: Blog
    plugin: Category
    routes:
      -
        routePath: '/{category_title}'
        _controller: 'Post::listPostsByCategory'
        _arguments:
          category_title: category
      -
        routePath: '/{category_title}/page-{page}'
        _controller: 'Post::listPostsByCategory'
        _arguments:
          category_title: category
          page: '@widget_0/currentPage'
    defaultController: 'Post::listPostsByCategory'
    aspects:
      category_title:
        type: PersistedAliasMapper
        tableName: sys_category
        routeFieldName: 'slug'
      page:
        type: StaticRangeMapper
        start: '1'
        end: '99'
Copied!

Blog Feeds:

config/my_site/config.yaml (excerpt)
# ...
routeEnhancers:
  PageTypeSuffix:
    type: PageType
    map:
      'blog.recent.xml': 200
      'blog.category.xml': 210
      'blog.tag.xml': 220
      'blog.archive.xml': 230
      'blog.comments.xml': 240
      'blog.author.xml': 250
Copied!

Blog Posts:

config/my_site/config.yaml (excerpt)
# ...
routeEnhancers:
  BlogPosts:
    type: Extbase
    extension: Blog
    plugin: Posts
    routes:
      -
        routePath: '/page-{page}'
        _controller: 'Post::listRecentPosts'
        _arguments:
          page: '@widget_0/currentPage'
    defaultController: 'Post::listRecentPosts'
    aspects:
      page:
        type: StaticRangeMapper
        start: '1'
        end: '99'
Copied!

Posts by Tag:

config/my_site/config.yaml (excerpt)
# ...
routeEnhancers:
  BlogTag:
    type: Extbase
    extension: Blog
    plugin: Tag
    routes:
      -
        routePath: '/{tag_title}'
        _controller: 'Post::listPostsByTag'
        _arguments:
          tag_title: tag
      -
        routePath: '/{tag_title}/page-{page}'
        _controller: 'Post::listPostsByTag'
        _arguments:
          tag_title: tag
          page: '@widget_0/currentPage'
    defaultController: 'Post::listPostsByTag'
    aspects:
      tag_title:
        type: PersistedAliasMapper
        tableName: tx_blog_domain_model_tag
        routeFieldName: 'slug'
      page:
        type: StaticRangeMapper
        start: '1'
        end: '99'
Copied!

BlogStaticDatabaseMapper:

packages/my_extension/Classes/Routing/Aspect/StaticDatabaseMapper.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Routing\Aspect;

use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Routing\Aspect\StaticMappableAspectInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;

class StaticDatabaseMapper implements StaticMappableAspectInterface, \Countable
{
    protected array $settings;
    protected string $field;
    protected string $table;
    protected string $groupBy;
    protected array $where;
    protected array $values;

    public function __construct(array $settings)
    {
        $field = $settings['field'] ?? null;
        $table = $settings['table'] ?? null;
        $where = $settings['where'] ?? [];
        $groupBy = $settings['groupBy'] ?? '';

        if (!is_string($field)) {
            throw new \InvalidArgumentException('field must be string', 1550156808);
        }
        if (!is_string($table)) {
            throw new \InvalidArgumentException('table must be string', 1550156812);
        }
        if (!is_string($groupBy)) {
            throw new \InvalidArgumentException('groupBy must be string', 1550158149);
        }
        if (!is_array($where)) {
            throw new \InvalidArgumentException('where must be an array', 1550157442);
        }

        $this->settings = $settings;
        $this->field = $field;
        $this->table = $table;
        $this->where = $where;
        $this->groupBy = $groupBy;
        $this->values = $this->buildValues();
    }

    public function count(): int
    {
        return count($this->values);
    }

    public function generate(string $value): ?string
    {
        return $this->respondWhenInValues($value);
    }

    public function resolve(string $value): ?string
    {
        return $this->respondWhenInValues($value);
    }

    protected function respondWhenInValues(string $value): ?string
    {
        if (in_array($value, $this->values, true)) {
            return $value;
        }
        return null;
    }

    /**
     * Builds range based on given settings and ensures each item is string.
     * The amount of items is limited to 1000 in order to avoid brute-force
     * scenarios and the risk of cache-flooding.
     *
     * In case that is not enough, creating a custom and more specific mapper
     * is encouraged. Using high values that are not distinct exposes the site
     * to the risk of cache-flooding.
     *
     * @return string[]
     * @throws \LengthException
     */
    protected function buildValues(): array
    {
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
            ->getQueryBuilderForTable($this->table);

        $queryBuilder
            ->select($this->field)
            ->from($this->table);

        if ($this->groupBy !== '') {
            $queryBuilder->groupBy($this->groupBy);
        }

        if (!empty($this->where)) {
            foreach ($this->where as $key => $value) {
                $queryBuilder->andWhere($key, $queryBuilder->createNamedParameter($value));
            }
        }

        return array_map('strval', array_column($queryBuilder->executeQuery()->fetchAllAssociative(), $this->field));
    }
}
Copied!

Usage with imports

On typo3.com we are using imports to make routing configurations easier to manage:

config/my_site/config.yaml (excerpt)
# ...
imports:
  - { resource: "EXT:my_extension/Configuration/Routes/Blog/BlogCategory.yaml" }
  - { resource: "EXT:my_extension/Configuration/Routes/Blog/BlogTag.yaml" }
  - { resource: "EXT:my_extension/Configuration/Routes/Blog/BlogArchive.yaml" }
  - { resource: "EXT:my_extension/Configuration/Routes/Blog/BlogAuthorPosts.yaml" }
  - { resource: "EXT:my_extension/Configuration/Routes/Blog/BlogFeedWidget.yaml" }
  - { resource: "EXT:my_extension/Configuration/Routes/Blog/BlogPosts.yaml" }
Copied!

Full project example config

Taken from an anonymous live project:

config/my_site/config.yaml (excerpt)
# ...

routeEnhancers:
  news:
    type: Extbase
    extension: mynews
    plugin: mynews
    routes:
      - routePath: '/news/detail/{news}'
        _controller: 'News::show'
        _arguments:
          news: 'news'

      - routePath: '/search-result/{searchFormHash}'
        _controller: 'News::list'
        _arguments:
          searchForm: 'searchFormHash'
    defaultController: 'News::show'
    aspects:
      news:
        routeValuePrefix: ''
        type: PersistedAliasMapper
        tableName: 'tx_mynews_domain_model_news'
        routeFieldName: slug
        valueFieldName: uid
  videos:
    type: Extbase
    extension: myvideos
    plugin: myvideos
    routes:
      -
        routePath: '/video-detail/detail/{videos}'
        _controller: 'Videos::show'
        _arguments:
          videos: videos
      -
        routePath: '/search-result/{searchFormHash}'
        _controller: 'Videos::list'
        _arguments:
          searchForm: searchForm
    defaultController: 'Videos::show'
    aspects:
      videos:
        routeValuePrefix: ''
        type: PersistedAliasMapper
        tableName: 'tx_myvideos_domain_model_videos'
        routeFieldName: slug
        valueFieldName: uid
  discipline:
    type: Extbase
    extension: myvideos
    plugin: overviewlist
    routes:
      -
        routePath: '/video-uebersicht/disziplin/{discipline}'
        _controller: 'Overview::discipline'
        _arguments:
          discipline: discipline
    defaultController: 'Overview::discipline'
    aspects:
      discipline:
        routeValuePrefix: ''
        type: PersistedAliasMapper
        tableName: 'tx_mytaxonomy_domain_model_discipline'
        routeFieldName: slug
        valueFieldName: uid
  events:
    type: Extbase
    extension: myapidata
    plugin: events
    routes:
      -
        routePath: '/events/detail/{uid}'
        _controller: 'Events::showByUid'
        _arguments:
          uid: uid
      -
        routePath: '/events/search-result/{searchFormHash}'
        _controller: 'Events::list'
        _arguments:
          searchForm: searchForm
    defaultController: 'Events::showByUid'
    aspects:
      uid:
        routeValuePrefix: ''
        type: PersistedAliasMapper
        tableName: 'tx_myapidata_domain_model_event'
        routeFieldName: slug
        valueFieldName: uid
  results:
    type: Extbase
    extension: myapidata
    plugin: results
    routes:
      -
        routePath: '/resultset/detail/{uid}'
        _controller: 'Results::showByUid'
        _arguments:
          uid: uid
      -
        routePath: '/resultset/search-result/{searchFormHash}'
        _controller: 'Results::list'
        _arguments:
          searchForm: searchForm
    defaultController: 'Results::showByUid'
    aspects:
    uid:
      routeValuePrefix: ''
      type: PersistedAliasMapper
      tableName: 'tx_myapidata_domain_model_event'
      routeFieldName: slug
      valueFieldName: uid
  teams:
    type: Extbase
    extension: myapidata
    plugin: teams
    routes:
      -
        routePath: '/detail/{team}'
        _controller: 'Team::show'
        _arguments:
          team: team
      -
        routePath: '/player/result/{searchFormHash}'
        _controller: 'Team::list'
        _arguments:
          searchForm: searchForm
    defaultController: 'Team::show'
    aspects:
      team:
        routeValuePrefix: ''
        type: PersistedAliasMapper
        tableName: 'tx_myapidata_domain_model_team'
        routeFieldName: slug
        valueFieldName: uid
  moreLoads:
    type: PageType
    map:
      'videos/events/videos.json': 1381404385
      'videos/categories/videos.json': 1381404386
      'videos/favorites/videos.json': 1381404389
      'videos/newest/videos.json': 1381404390
Copied!

EXT: DpnGlossary

Prerequisites:

  • The plugin for list view and detail view is added on one page.
  • The StaticMultiRangeMapper (a custom mapper) is available in the project.

Result:

  • List view: https://example.org/<YOUR_PLUGINPAGE_SLUG>
  • Detail view: https://example.org/<YOUR_PLUGINPAGE_SLUG>/term/the-term-title
config/my_site/config.yaml (excerpt)
# ...
routeEnhancers:
  DpnGlossary:
    type: Extbase
    limitToPages: 42, 86
    extension: DpnGlossary
    plugin: glossary
    routes:
      - { routePath: '/{character}', _controller: 'Term::list', _arguments: {'character': '@widget_0/character'} }
      - { routePath: '/{localized_term}/{term_name}', _controller: 'Term::show', _arguments: {'term_name': 'term'} }
    defaultController: 'Term::list'
    defaults:
      character: ''
    aspects:
      term_name:
        type: PersistedAliasMapper
        tableName: 'tx_dpnglossary_domain_model_term'
        routeFieldName: 'url_segment'
      character:
        type: StaticMultiRangeMapper
        ranges:
          - start: 'A'
            end: 'Z'
      localized_term:
        type: LocaleModifier
        default: 'term'
        localeMap:
          - locale: 'de_DE.*'
            value: 'begriff'
Copied!

Taken from dpn_glossary extension manual.