From 98cf5faee201eaa3b4e4529b0dbf7b3234cd6e82 Mon Sep 17 00:00:00 2001
From: Benni Mack <benni@typo3.org>
Date: Wed, 30 Nov 2022 16:00:17 +0100
Subject: [PATCH] [FEATURE] Allow placeholders in Backend URLs

This change introduces a Backend Route Result,
similar to the Frontend Route Result object,
using symfony's native URL routing component
in TYPO3 Backend now, to allow to use placeholders
in the URL. The Backend Route Result now
contains the resolved Route object and
the arguments defined in the Route, which
are then resolved during the PSR-15 middleware
when the Router kicks in.

Note: In a later stage Router->matchRequest()
will be deprecated.

Resolves: #99234
Releases: main
Change-Id: Id2030f8fe17457ea3791fdf4b88a375cc93ffd39
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/76865
Reviewed-by: Susanne Moog <look@susi.dev>
Tested-by: Susanne Moog <look@susi.dev>
Reviewed-by: Oliver Hader <oliver.hader@typo3.org>
Tested-by: Oliver Hader <oliver.hader@typo3.org>
Tested-by: core-ci <typo3@b13.com>
---
 Build/phpstan/phpstan-baseline.neon           | 10 ---
 .../Backend/Shortcut/ShortcutRepository.php   |  6 +-
 .../Middleware/BackendRouteInitialization.php | 10 ++-
 .../backend/Classes/Routing/RouteResult.php   | 82 ++++++++++++++++++
 .../sysext/backend/Classes/Routing/Router.php | 36 ++++----
 .../backend/Classes/Routing/UriBuilder.php    | 51 ++++++-----
 .../backend/Classes/ServiceProvider.php       |  3 +-
 .../Authentication/PasswordResetTest.php      |  7 +-
 .../Shortcut/ShortcutRepositoryTest.php       | 10 ++-
 .../Functional/Clipboard/ClipboardTest.php    |  7 ++
 .../Controller/BackendControllerTest.php      |  3 +-
 .../Controller/EditDocumentControllerTest.php | 29 ++++---
 .../MfaConfigurationControllerTest.php        |  2 +-
 .../Controller/MfaControllerTest.php          |  2 +-
 .../Controller/MfaSetupControllerTest.php     |  2 +-
 .../ResetPasswordControllerTest.php           |  3 +-
 .../Controller/ShortcutControllerTest.php     |  3 +-
 .../Middleware/BackendModuleValidatorTest.php | 21 +++--
 .../Tests/Functional/Routing/RouterTest.php   | 47 +++++++----
 .../Buttons/Action/ShortcutButtonTest.php     |  2 +-
 .../Tests/Unit/Routing/UriBuilderTest.php     |  9 +-
 .../Tests/Unit/View/ArrayBrowserTest.php      |  9 +-
 .../core/Classes/Routing/PageRouter.php       | 47 ++---------
 .../Classes/Routing/RequestContextFactory.php | 76 +++++++++++++++++
 .../core/Classes/Routing/SiteMatcher.php      | 31 ++-----
 typo3/sysext/core/Classes/Site/SiteFinder.php | 11 +--
 typo3/sysext/core/Configuration/Services.yaml |  3 +
 ...9234-DynamicURLPartsInTYPO3BackendURLs.rst | 51 +++++++++++
 .../Functional/Page/PageRendererTest.php      |  2 +-
 .../Tests/Unit/Routing/PageRouterTest.php     |  9 ++
 .../Tests/Unit/Routing/SiteMatcherTest.php    | 47 +++++++----
 .../Unit/Mvc/Web/Routing/UriBuilderTest.php   | 12 +--
 .../ViewHelpers/Be/LinkViewHelperTest.php     |  7 +-
 .../ViewHelpers/Be/UriViewHelperTest.php      |  7 +-
 .../ViewHelpers/Link/PageViewHelperTest.php   | 10 +--
 .../PageRendererViewHelperTest.php            |  6 +-
 .../ViewHelpers/Uri/PageViewHelperTest.php    | 16 ++--
 .../Typolink/AbstractTypolinkBuilder.php      | 11 ++-
 .../Unit/Middleware/SiteResolverTest.php      | 84 ++++++++++++-------
 39 files changed, 531 insertions(+), 253 deletions(-)
 create mode 100644 typo3/sysext/backend/Classes/Routing/RouteResult.php
 create mode 100644 typo3/sysext/core/Classes/Routing/RequestContextFactory.php
 create mode 100644 typo3/sysext/core/Documentation/Changelog/12.1/Feature-99234-DynamicURLPartsInTYPO3BackendURLs.rst

diff --git a/Build/phpstan/phpstan-baseline.neon b/Build/phpstan/phpstan-baseline.neon
index c77a9bc778ab..bcb978ef478c 100644
--- a/Build/phpstan/phpstan-baseline.neon
+++ b/Build/phpstan/phpstan-baseline.neon
@@ -1440,11 +1440,6 @@ parameters:
 			count: 2
 			path: ../../typo3/sysext/core/Tests/Unit/Resource/Repository/AbstractRepositoryTest.php
 
-		-
-			message: "#^Call to an undefined method TYPO3\\\\CMS\\\\Core\\\\Site\\\\SiteFinder\\:\\:method\\(\\)\\.$#"
-			count: 1
-			path: ../../typo3/sysext/core/Tests/Unit/Routing/SiteMatcherTest.php
-
 		-
 			message: "#^Constructor of class TYPO3\\\\CMS\\\\Core\\\\Tests\\\\Unit\\\\Tree\\\\TableConfiguration\\\\Fixtures\\\\TreeDataProviderFixture has an unused parameter \\$configuration\\.$#"
 			count: 1
@@ -2835,11 +2830,6 @@ parameters:
 			count: 1
 			path: ../../typo3/sysext/frontend/Tests/Unit/Middleware/SiteResolverTest.php
 
-		-
-			message: "#^Parameter \\#1 \\$finder of class TYPO3\\\\CMS\\\\Core\\\\Routing\\\\SiteMatcher constructor expects TYPO3\\\\CMS\\\\Core\\\\Site\\\\SiteFinder\\|null, TYPO3\\\\TestingFramework\\\\Core\\\\AccessibleObjectInterface given\\.$#"
-			count: 4
-			path: ../../typo3/sysext/frontend/Tests/Unit/Middleware/SiteResolverTest.php
-
 		-
 			message: "#^Right side of && is always true\\.$#"
 			count: 1
diff --git a/typo3/sysext/backend/Classes/Backend/Shortcut/ShortcutRepository.php b/typo3/sysext/backend/Classes/Backend/Shortcut/ShortcutRepository.php
index 0236581736b4..adb72101b9f3 100644
--- a/typo3/sysext/backend/Classes/Backend/Shortcut/ShortcutRepository.php
+++ b/typo3/sysext/backend/Classes/Backend/Shortcut/ShortcutRepository.php
@@ -17,7 +17,6 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Backend\Backend\Shortcut;
 
-use Symfony\Component\Routing\Route;
 use TYPO3\CMS\Backend\Module\ModuleProvider;
 use TYPO3\CMS\Backend\Routing\Router;
 use TYPO3\CMS\Backend\Routing\UriBuilder;
@@ -56,7 +55,8 @@ class ShortcutRepository
         protected readonly ConnectionPool $connectionPool,
         protected readonly IconFactory $iconFactory,
         protected readonly ModuleProvider $moduleProvider,
-        protected readonly Router $router
+        protected readonly Router $router,
+        protected readonly UriBuilder $uriBuilder,
     ) {
         $this->shortcutGroups = $this->initShortcutGroups();
         $this->shortcuts = $this->initShortcuts();
@@ -468,7 +468,7 @@ class ShortcutRepository
             $shortcut['group'] = $shortcutGroup;
             $shortcut['icon'] = $this->getShortcutIcon($routeIdentifier, $moduleName, $shortcut);
             $shortcut['label'] = ($row['description'] ?? false) ?: 'Shortcut'; // Fall back to "Shortcut", see: addShortcut()
-            $shortcut['href'] = (string)GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute($routeIdentifier, $arguments);
+            $shortcut['href'] = (string)$this->uriBuilder->buildUriFromRoute($routeIdentifier, $arguments);
             $shortcut['route'] = $routeIdentifier;
             $shortcut['module'] = $moduleName;
             $shortcut['pageId'] = $pageId;
diff --git a/typo3/sysext/backend/Classes/Middleware/BackendRouteInitialization.php b/typo3/sysext/backend/Classes/Middleware/BackendRouteInitialization.php
index 6dd8bea352e0..8b19de4b6e7c 100644
--- a/typo3/sysext/backend/Classes/Middleware/BackendRouteInitialization.php
+++ b/typo3/sysext/backend/Classes/Middleware/BackendRouteInitialization.php
@@ -28,6 +28,7 @@ use TYPO3\CMS\Backend\Routing\UriBuilder;
 use TYPO3\CMS\Core\Core\Bootstrap;
 use TYPO3\CMS\Core\Http\RedirectResponse;
 use TYPO3\CMS\Core\Http\Response;
+use TYPO3\CMS\Core\Routing\RequestContextFactory;
 
 /**
  * Injects the Router and tries to match the current request with a
@@ -51,6 +52,7 @@ class BackendRouteInitialization implements MiddlewareInterface
     public function __construct(
         protected readonly Router $router,
         protected readonly UriBuilder $uriBuilder,
+        protected readonly RequestContextFactory $requestContextFactory,
     ) {
     }
 
@@ -61,11 +63,13 @@ class BackendRouteInitialization implements MiddlewareInterface
     {
         // @todo Find another place for this call, since it's not related to this middleware anymore
         Bootstrap::loadExtTables();
+        $this->uriBuilder->setRequestContext($this->requestContextFactory->fromBackendRequest($request));
 
         try {
-            $route = $this->router->matchRequest($request);
-            $request = $request->withAttribute('route', $route);
-            $request = $request->withAttribute('target', $route->getOption('target'));
+            $routeResult = $this->router->matchResult($request);
+            $request = $request->withAttribute('routing', $routeResult);
+            $request = $request->withAttribute('route', $routeResult->getRoute());
+            $request = $request->withAttribute('target', $routeResult->getRoute()->getOption('target'));
         } catch (MethodNotAllowedException $e) {
             return new Response(null, 405);
         } catch (ResourceNotFoundException $e) {
diff --git a/typo3/sysext/backend/Classes/Routing/RouteResult.php b/typo3/sysext/backend/Classes/Routing/RouteResult.php
new file mode 100644
index 000000000000..da4a0dc785ac
--- /dev/null
+++ b/typo3/sysext/backend/Classes/Routing/RouteResult.php
@@ -0,0 +1,82 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Backend\Routing;
+
+use TYPO3\CMS\Core\Routing\RouteResultInterface;
+
+/**
+ * A route result for the TYPO3 Backend Routing,
+ * containing the matched Route and the related arguments found in the URL
+ */
+class RouteResult implements RouteResultInterface
+{
+    public function __construct(
+        protected Route $route,
+        protected array $arguments = [],
+    ) {
+    }
+
+    public function getRoute(): Route
+    {
+        return $this->route;
+    }
+
+    public function getRouteName(): string
+    {
+        return $this->route->getOption('_identifier');
+    }
+
+    public function getArguments(): array
+    {
+        return $this->arguments;
+    }
+
+    public function offsetExists($offset): bool
+    {
+        return $offset === 'route' || isset($this->arguments[$offset]);
+    }
+
+    public function offsetGet(mixed $offset): mixed
+    {
+        return match ($offset) {
+            'route' => $this->route,
+            default => $this->arguments[$offset],
+        };
+    }
+
+    public function offsetSet(mixed $offset = '', mixed $value = ''): void
+    {
+        switch ($offset) {
+            case 'route':
+                $this->route = $value;
+                break;
+            default:
+                $this->arguments[$offset] = $value;
+        }
+    }
+
+    public function offsetUnset(mixed $offset): void
+    {
+        switch ($offset) {
+            case 'route':
+                throw new \InvalidArgumentException('You can never unset the Route in a route result', 1669839336);
+            default:
+                unset($this->arguments[$offset]);
+        }
+    }
+}
diff --git a/typo3/sysext/backend/Classes/Routing/Router.php b/typo3/sysext/backend/Classes/Routing/Router.php
index 36d978706f23..bd9e9c756844 100644
--- a/typo3/sysext/backend/Classes/Routing/Router.php
+++ b/typo3/sysext/backend/Classes/Routing/Router.php
@@ -17,11 +17,11 @@ namespace TYPO3\CMS\Backend\Routing;
 
 use Psr\Http\Message\ServerRequestInterface;
 use Symfony\Component\Routing\Matcher\UrlMatcher;
-use Symfony\Component\Routing\RequestContext;
 use Symfony\Component\Routing\Route as SymfonyRoute;
 use Symfony\Component\Routing\RouteCollection as SymfonyRouteCollection;
 use TYPO3\CMS\Backend\Routing\Exception\MethodNotAllowedException;
 use TYPO3\CMS\Backend\Routing\Exception\ResourceNotFoundException;
+use TYPO3\CMS\Core\Routing\RequestContextFactory;
 use TYPO3\CMS\Core\SingletonInterface;
 
 /**
@@ -42,8 +42,9 @@ class Router implements SingletonInterface
      */
     protected SymfonyRouteCollection $routeCollection;
 
-    public function __construct()
-    {
+    public function __construct(
+        protected readonly RequestContextFactory $requestContextFactory
+    ) {
         $this->routeCollection = new SymfonyRouteCollection();
     }
     /**
@@ -116,11 +117,9 @@ class Router implements SingletonInterface
     }
 
     /**
-     * Tries to match a URI against the registered routes
-     *
-     * @return Route the first Route object found
+     * Matches a PSR-7 Request and returns a RouteResult with parameters and the resolved route.
      */
-    public function matchRequest(ServerRequestInterface $request)
+    public function matchResult(ServerRequestInterface $request): RouteResult
     {
         $path = $request->getUri()->getPath();
         if (($normalizedParams = $request->getAttribute('normalizedParams')) !== null) {
@@ -132,14 +131,9 @@ class Router implements SingletonInterface
             // (consolidate RouteDispatcher::evaluateReferrer() when changing 'login' to something different)
             $path = '/login';
         }
-        $context = new RequestContext(
-            $path,
-            $request->getMethod(),
-            (string)idn_to_ascii($request->getUri()->getHost()),
-            $request->getUri()->getScheme()
-        );
+        $requestContext = $this->requestContextFactory->fromBackendRequest($request);
         try {
-            $result = (new UrlMatcher($this->routeCollection, $context))->match($path);
+            $result = (new UrlMatcher($this->routeCollection, $requestContext))->match($path);
             $matchedSymfonyRoute = $this->routeCollection->get($result['_route']);
             if ($matchedSymfonyRoute === null) {
                 throw new ResourceNotFoundException('The requested resource "' . $path . '" was not found.', 1607596900);
@@ -158,6 +152,18 @@ class Router implements SingletonInterface
             $route->setMethods($methods);
         }
         $route->setOption('_identifier', $result['_route']);
-        return $route;
+        unset($result['_route']);
+        return new RouteResult($route, $result);
+    }
+
+    /**
+     * Tries to match a URI against the registered routes.
+     * Use ->matchResult() instead, as this method will be deprecated in the future.
+     *
+     * @return Route the first Route object found
+     */
+    public function matchRequest(ServerRequestInterface $request)
+    {
+        return $this->matchResult($request)->getRoute();
     }
 }
diff --git a/typo3/sysext/backend/Classes/Routing/UriBuilder.php b/typo3/sysext/backend/Classes/Routing/UriBuilder.php
index 8a84df8b5a68..ad1c2df96f36 100644
--- a/typo3/sysext/backend/Classes/Routing/UriBuilder.php
+++ b/typo3/sysext/backend/Classes/Routing/UriBuilder.php
@@ -15,19 +15,20 @@
 
 namespace TYPO3\CMS\Backend\Routing;
 
-use Psr\Http\Message\ServerRequestInterface;
 use Psr\Http\Message\UriInterface;
+use Symfony\Component\Routing\Generator\UrlGenerator;
+use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
+use Symfony\Component\Routing\RequestContext;
 use TYPO3\CMS\Backend\Routing\Exception\MethodNotAllowedException;
 use TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException;
 use TYPO3\CMS\Backend\Routing\Exception\RouteTypeNotAllowedException;
 use TYPO3\CMS\Core\Core\Environment;
+use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder;
 use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
+use TYPO3\CMS\Core\Http\ServerRequestFactory;
 use TYPO3\CMS\Core\Http\Uri;
-use TYPO3\CMS\Core\Routing\BackendEntryPointResolver;
+use TYPO3\CMS\Core\Routing\RequestContextFactory;
 use TYPO3\CMS\Core\SingletonInterface;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
-use TYPO3\CMS\Core\Utility\HttpUtility;
-use TYPO3\CMS\Core\Utility\PathUtility;
 
 /**
  * Main UrlGenerator for creating URLs for the Backend. Generates a URL based on
@@ -55,20 +56,29 @@ class UriBuilder implements SingletonInterface
     public const SHAREABLE_URL = 'share';
 
     /**
-     * @var array
+     * @var array<non-empty-string, UriInterface>
      */
-    protected $generated = [];
+    protected array $generated = [];
 
+    protected RequestContext|null $requestContext = null;
     /**
      * Loads the router to fetch the available routes from the Router to be used for generating routes
      */
     public function __construct(
         protected readonly Router $router,
-        protected readonly BackendEntryPointResolver $backendEntryPointResolver,
-        protected readonly FormProtectionFactory $formProtectionFactory
+        protected readonly FormProtectionFactory $formProtectionFactory,
+        protected readonly RequestContextFactory $requestContextFactory
     ) {
     }
 
+    /**
+     * @internal
+     */
+    public function setRequestContext(RequestContext $requestContext): void
+    {
+        $this->requestContext = $requestContext;
+    }
+
     /**
      * Generates a URL or path for a specific route based on the given route.
      * Currently used to link to the current script, it is encouraged to use "buildUriFromRoute" if possible.
@@ -149,27 +159,30 @@ class UriBuilder implements SingletonInterface
             ] + $parameters;
         }
 
-        $this->generated[$cacheIdentifier] = $this->buildUri($route->getPath(), $parameters, (string)$referenceType);
+        $this->generated[$cacheIdentifier] = $this->buildUri($name, $parameters, (string)$referenceType);
         return $this->generated[$cacheIdentifier];
     }
 
     /**
      * Internal method building a Uri object, merging the GET parameters array into a flat queryString
      *
-     * @param string $route The route path to prepend
+     * @param string $routeName The route name in the collection
      * @param array $parameters An array of GET parameters
      * @param string $referenceType The type of reference to be generated (one of the constants)
      */
-    protected function buildUri(string $route, array $parameters, string $referenceType): UriInterface
+    protected function buildUri(string $routeName, array $parameters, string $referenceType): UriInterface
     {
-        $path = ltrim($route . HttpUtility::buildQueryString($parameters, '?'), '/');
-        if ($referenceType === self::ABSOLUTE_PATH) {
-            $uri = PathUtility::getAbsoluteWebPath(Environment::getBackendPath() . '/' . $path);
-        } elseif (($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface) {
-            $uri = $this->backendEntryPointResolver->getUriFromRequest($GLOBALS['TYPO3_REQUEST'], $path);
+        if (isset($this->requestContext)) {
+            $requestContext = $this->requestContext;
+        } elseif (isset($GLOBALS['TYPO3_REQUEST'])) {
+            $requestContext = $this->requestContextFactory->fromBackendRequest($GLOBALS['TYPO3_REQUEST']);
+        } elseif (!Environment::isCli()) {
+            $requestContext = $this->requestContextFactory->fromBackendRequest(ServerRequestFactory::fromGlobals()->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE));
         } else {
-            $uri = $path;
+            $requestContext = new RequestContext('/typo3/');
         }
-        return $uri instanceof UriInterface ? $uri : GeneralUtility::makeInstance(Uri::class, $uri);
+        $urlGenerator = new UrlGenerator($this->router->getRouteCollection(), $requestContext);
+        $url = $urlGenerator->generate($routeName, $parameters, $referenceType === self::ABSOLUTE_PATH ? UrlGeneratorInterface::ABSOLUTE_PATH : UrlGeneratorInterface::ABSOLUTE_URL);
+        return new Uri($url);
     }
 }
diff --git a/typo3/sysext/backend/Classes/ServiceProvider.php b/typo3/sysext/backend/Classes/ServiceProvider.php
index 140581a53eb9..360e2a17b020 100644
--- a/typo3/sysext/backend/Classes/ServiceProvider.php
+++ b/typo3/sysext/backend/Classes/ServiceProvider.php
@@ -42,6 +42,7 @@ use TYPO3\CMS\Core\Imaging\IconRegistry;
 use TYPO3\CMS\Core\Package\AbstractServiceProvider;
 use TYPO3\CMS\Core\Package\Cache\PackageDependentCacheIdentifier;
 use TYPO3\CMS\Core\Routing\BackendEntryPointResolver;
+use TYPO3\CMS\Core\Routing\RequestContextFactory;
 
 /**
  * @internal
@@ -120,8 +121,8 @@ class ServiceProvider extends AbstractServiceProvider
     {
         return self::new($container, UriBuilder::class, [
             $container->get(Router::class),
-            $container->get(BackendEntryPointResolver::class),
             $container->get(FormProtectionFactory::class),
+            $container->get(RequestContextFactory::class),
         ]);
     }
 
diff --git a/typo3/sysext/backend/Tests/Functional/Authentication/PasswordResetTest.php b/typo3/sysext/backend/Tests/Functional/Authentication/PasswordResetTest.php
index aaa50536617a..f0c39107993d 100644
--- a/typo3/sysext/backend/Tests/Functional/Authentication/PasswordResetTest.php
+++ b/typo3/sysext/backend/Tests/Functional/Authentication/PasswordResetTest.php
@@ -21,7 +21,9 @@ use Psr\Log\LoggerInterface;
 use Psr\Log\LoggerTrait;
 use TYPO3\CMS\Backend\Authentication\PasswordReset;
 use TYPO3\CMS\Core\Context\Context;
+use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder;
 use TYPO3\CMS\Core\Http\ServerRequest;
+use TYPO3\CMS\Core\Http\Uri;
 use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
 
 class PasswordResetTest extends FunctionalTestCase
@@ -172,7 +174,10 @@ class PasswordResetTest extends FunctionalTestCase
         $subject = new PasswordReset();
         $subject->setLogger($this->logger);
         $context = new Context();
-        $request = new ServerRequest();
+        $uri = new Uri('https://localhost/typo3/');
+        $request = new ServerRequest($uri);
+        $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE);
+        $GLOBALS['TYPO3_REQUEST'] = $request;
         $subject->initiateReset($request, $context, $emailAddress);
         self::assertEquals('info', $this->logger->records[0]['level']);
         self::assertEquals($emailAddress, $this->logger->records[0]['context']['email']);
diff --git a/typo3/sysext/backend/Tests/Functional/Backend/Shortcut/ShortcutRepositoryTest.php b/typo3/sysext/backend/Tests/Functional/Backend/Shortcut/ShortcutRepositoryTest.php
index c137af942ab8..e46916b8e9f8 100644
--- a/typo3/sysext/backend/Tests/Functional/Backend/Shortcut/ShortcutRepositoryTest.php
+++ b/typo3/sysext/backend/Tests/Functional/Backend/Shortcut/ShortcutRepositoryTest.php
@@ -20,9 +20,12 @@ namespace TYPO3\CMS\Backend\Tests\Functional\Backend\Shortcut;
 use TYPO3\CMS\Backend\Backend\Shortcut\ShortcutRepository;
 use TYPO3\CMS\Backend\Module\ModuleProvider;
 use TYPO3\CMS\Backend\Routing\Router;
+use TYPO3\CMS\Backend\Routing\UriBuilder;
 use TYPO3\CMS\Core\Core\Bootstrap;
 use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Http\ServerRequest;
 use TYPO3\CMS\Core\Imaging\IconFactory;
+use TYPO3\CMS\Core\Routing\RequestContextFactory;
 use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
 
 class ShortcutRepositoryTest extends FunctionalTestCase
@@ -37,11 +40,16 @@ class ShortcutRepositoryTest extends FunctionalTestCase
         $this->setUpBackendUser(1);
         Bootstrap::initializeLanguageObject();
 
+        $request = new ServerRequest('https://localhost/typo3/');
+        $requestContextFactory = $this->get(RequestContextFactory::class);
+        $uriBuilder = $this->get(UriBuilder::class);
+        $uriBuilder->setRequestContext($requestContextFactory->fromBackendRequest($request));
         $this->subject = new ShortcutRepository(
             $this->get(ConnectionPool::class),
             $this->get(IconFactory::class),
             $this->get(ModuleProvider::class),
-            $this->get(Router::class)
+            $this->get(Router::class),
+            $this->get(UriBuilder::class),
         );
     }
 
diff --git a/typo3/sysext/backend/Tests/Functional/Clipboard/ClipboardTest.php b/typo3/sysext/backend/Tests/Functional/Clipboard/ClipboardTest.php
index e6d4ae8c8286..b5891d17211a 100644
--- a/typo3/sysext/backend/Tests/Functional/Clipboard/ClipboardTest.php
+++ b/typo3/sysext/backend/Tests/Functional/Clipboard/ClipboardTest.php
@@ -18,8 +18,11 @@ declare(strict_types=1);
 namespace TYPO3\CMS\Backend\Tests\Functional\Clipboard;
 
 use TYPO3\CMS\Backend\Clipboard\Clipboard;
+use TYPO3\CMS\Backend\Routing\UriBuilder;
 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
 use TYPO3\CMS\Core\Core\Bootstrap;
+use TYPO3\CMS\Core\Http\ServerRequest;
+use TYPO3\CMS\Core\Routing\RequestContextFactory;
 use TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataHandlerFactory;
@@ -43,6 +46,10 @@ class ClipboardTest extends FunctionalTestCase
     protected function setUp(): void
     {
         parent::setUp();
+        $request = new ServerRequest('https://localhost/typo3/');
+        $requestContextFactory = $this->get(RequestContextFactory::class);
+        $uriBuilder = $this->get(UriBuilder::class);
+        $uriBuilder->setRequestContext($requestContextFactory->fromBackendRequest($request));
         $this->subject = GeneralUtility::makeInstance(Clipboard::class);
         $this->withDatabaseSnapshot(
             function () {
diff --git a/typo3/sysext/backend/Tests/Functional/Controller/BackendControllerTest.php b/typo3/sysext/backend/Tests/Functional/Controller/BackendControllerTest.php
index 917adfaa1dfc..9a4338f694a1 100644
--- a/typo3/sysext/backend/Tests/Functional/Controller/BackendControllerTest.php
+++ b/typo3/sysext/backend/Tests/Functional/Controller/BackendControllerTest.php
@@ -61,10 +61,11 @@ class BackendControllerTest extends FunctionalTestCase
         $eventListener = GeneralUtility::makeInstance(ListenerProvider::class);
         $eventListener->addListener(AfterBackendPageRenderEvent::class, 'after-backend-page-render-listener');
 
-        $request = (new ServerRequest())
+        $request = (new ServerRequest('https://example.com/typo3/main'))
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE)
             ->withAttribute('route', new Route('/main', ['packageName' => 'typo3/cms-backend', '_identifier' => 'main']));
 
+        $GLOBALS['TYPO3_REQUEST'] = $request;
         $subject = $this->get(BackendController::class);
         $subject->mainAction($request);
 
diff --git a/typo3/sysext/backend/Tests/Functional/Controller/EditDocumentControllerTest.php b/typo3/sysext/backend/Tests/Functional/Controller/EditDocumentControllerTest.php
index 06718099ec05..a37dc853bb76 100644
--- a/typo3/sysext/backend/Tests/Functional/Controller/EditDocumentControllerTest.php
+++ b/typo3/sysext/backend/Tests/Functional/Controller/EditDocumentControllerTest.php
@@ -65,13 +65,13 @@ class EditDocumentControllerTest extends FunctionalTestCase
         $queryParams = $this->getQueryParamsWithDefaults($defaultValues);
         $parsedBody = $this->getParsedBody();
 
-        $response = $this->subject->mainAction(
-            $request
-                ->withAttribute('normalizedParams', $this->normalizedParams)
-                ->withAttribute('route', new Route('path', ['packageName' => 'typo3/cms-backend']))
-                ->withQueryParams($queryParams)
-                ->withParsedBody($parsedBody)
-        );
+        $request = $request
+            ->withAttribute('normalizedParams', $this->normalizedParams)
+            ->withAttribute('route', new Route('path', ['packageName' => 'typo3/cms-backend']))
+            ->withQueryParams($queryParams)
+            ->withParsedBody($parsedBody);
+        $GLOBALS['TYPO3_REQUEST'] = $request;
+        $response = $this->subject->mainAction($request);
 
         $newRecord = BackendUtility::getRecord('tt_content', 2);
         self::assertEquals(
@@ -96,14 +96,13 @@ class EditDocumentControllerTest extends FunctionalTestCase
 
         $queryParams = $this->getQueryParamsWithDefaults($defaultValues);
         $parsedBody = $this->getParsedBody(['colPos' => 0, 'CType' => 'text']);
-
-        $response = $this->subject->mainAction(
-            $request
-                ->withAttribute('normalizedParams', $this->normalizedParams)
-                ->withAttribute('route', new Route('path', ['packageName' => 'typo3/cms-backend']))
-                ->withQueryParams($queryParams)
-                ->withParsedBody($parsedBody)
-        );
+        $request = $request
+            ->withAttribute('normalizedParams', $this->normalizedParams)
+            ->withAttribute('route', new Route('path', ['packageName' => 'typo3/cms-backend']))
+            ->withQueryParams($queryParams)
+            ->withParsedBody($parsedBody);
+        $GLOBALS['TYPO3_REQUEST'] = $request;
+        $response = $this->subject->mainAction($request);
 
         $newRecord = BackendUtility::getRecord('tt_content', 2);
         self::assertEquals(
diff --git a/typo3/sysext/backend/Tests/Functional/Controller/MfaConfigurationControllerTest.php b/typo3/sysext/backend/Tests/Functional/Controller/MfaConfigurationControllerTest.php
index 46385fa792eb..af1b82e1beaa 100644
--- a/typo3/sysext/backend/Tests/Functional/Controller/MfaConfigurationControllerTest.php
+++ b/typo3/sysext/backend/Tests/Functional/Controller/MfaConfigurationControllerTest.php
@@ -61,7 +61,7 @@ class MfaConfigurationControllerTest extends FunctionalTestCase
         );
         $this->subject->injectMfaProviderRegistry($this->get(MfaProviderRegistry::class));
 
-        $this->request = (new ServerRequest())
+        $this->request = (new ServerRequest('https://example.com/typo3/'))
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE)
             ->withAttribute('route', new Route('path', ['packageName' => 'typo3/cms-backend']));
         $this->normalizedParams = new NormalizedParams([], [], '', '');
diff --git a/typo3/sysext/backend/Tests/Functional/Controller/MfaControllerTest.php b/typo3/sysext/backend/Tests/Functional/Controller/MfaControllerTest.php
index ca59bb603504..11601a6c6374 100644
--- a/typo3/sysext/backend/Tests/Functional/Controller/MfaControllerTest.php
+++ b/typo3/sysext/backend/Tests/Functional/Controller/MfaControllerTest.php
@@ -70,7 +70,7 @@ class MfaControllerTest extends FunctionalTestCase
         );
         $this->subject->injectMfaProviderRegistry($this->get(MfaProviderRegistry::class));
 
-        $this->request = (new ServerRequest())
+        $this->request = (new ServerRequest('https://example.com/typo3/'))
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE)
             ->withAttribute('route', new Route('path', ['packageName' => 'typo3/cms-backend']));
     }
diff --git a/typo3/sysext/backend/Tests/Functional/Controller/MfaSetupControllerTest.php b/typo3/sysext/backend/Tests/Functional/Controller/MfaSetupControllerTest.php
index e08d809d2be1..6bfec9562de9 100644
--- a/typo3/sysext/backend/Tests/Functional/Controller/MfaSetupControllerTest.php
+++ b/typo3/sysext/backend/Tests/Functional/Controller/MfaSetupControllerTest.php
@@ -72,7 +72,7 @@ class MfaSetupControllerTest extends FunctionalTestCase
         );
         $this->subject->injectMfaProviderRegistry($this->get(MfaProviderRegistry::class));
 
-        $this->request = (new ServerRequest())
+        $this->request = (new ServerRequest('https://example.com/typo3/'))
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE)
             ->withAttribute('route', new Route('path', ['packageName' => 'typo3/cms-backend']));
     }
diff --git a/typo3/sysext/backend/Tests/Functional/Controller/ResetPasswordControllerTest.php b/typo3/sysext/backend/Tests/Functional/Controller/ResetPasswordControllerTest.php
index ebb1cef0da51..c653dfb2765c 100644
--- a/typo3/sysext/backend/Tests/Functional/Controller/ResetPasswordControllerTest.php
+++ b/typo3/sysext/backend/Tests/Functional/Controller/ResetPasswordControllerTest.php
@@ -74,7 +74,7 @@ class ResetPasswordControllerTest extends FunctionalTestCase
             $this->get(BackendViewFactory::class),
         );
 
-        $this->request = (new ServerRequest())
+        $this->request = (new ServerRequest('https://example.com/typo3/'))
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE)
             ->withAttribute('route', new Route('path', ['packageName' => 'typo3/cms-backend']));
 
@@ -94,6 +94,7 @@ class ResetPasswordControllerTest extends FunctionalTestCase
 
         $this->expectExceptionCode(1618342858);
         $this->expectException(PropagateResponseException::class);
+        $GLOBALS['TYPO3_REQUEST'] = $this->request;
         $this->subject->forgetPasswordFormAction($this->request);
     }
 
diff --git a/typo3/sysext/backend/Tests/Functional/Controller/ShortcutControllerTest.php b/typo3/sysext/backend/Tests/Functional/Controller/ShortcutControllerTest.php
index 2aaba5a626c8..43c317548ccd 100644
--- a/typo3/sysext/backend/Tests/Functional/Controller/ShortcutControllerTest.php
+++ b/typo3/sysext/backend/Tests/Functional/Controller/ShortcutControllerTest.php
@@ -47,7 +47,8 @@ class ShortcutControllerTest extends FunctionalTestCase
             $this->get(ShortcutRepository::class),
             new BackendViewFactory($this->get(RenderingContextFactory::class), $this->get(PackageManager::class))
         );
-        $this->request = (new ServerRequest())->withAttribute('normalizedParams', new NormalizedParams([], [], '', ''));
+        $this->request = (new ServerRequest('https://example.com/typo3/'))->withAttribute('normalizedParams', new NormalizedParams([], [], '', ''));
+        $GLOBALS['TYPO3_REQUEST'] = $this->request;
     }
 
     /**
diff --git a/typo3/sysext/backend/Tests/Functional/Middleware/BackendModuleValidatorTest.php b/typo3/sysext/backend/Tests/Functional/Middleware/BackendModuleValidatorTest.php
index cf7550979621..06621e0a7a9c 100644
--- a/typo3/sysext/backend/Tests/Functional/Middleware/BackendModuleValidatorTest.php
+++ b/typo3/sysext/backend/Tests/Functional/Middleware/BackendModuleValidatorTest.php
@@ -76,8 +76,10 @@ class BackendModuleValidatorTest extends FunctionalTestCase
             ]
         );
 
+        $request = $this->request->withAttribute('route', new Route('/some/route', ['module' => $module]));
+        $GLOBALS['TYPO3_REQUEST'] = $request;
         $response = $this->subject->process(
-            $this->request->withAttribute('route', new Route('/some/route', ['module' => $module])),
+            $request,
             $this->requestHandler
         );
 
@@ -102,10 +104,13 @@ class BackendModuleValidatorTest extends FunctionalTestCase
             ]
         );
 
+        $request = $this->request
+            ->withQueryParams(['pointer' => 0, 'reverse' => true])
+            ->withAttribute('route', new Route('/some/route', ['module' => $module]));
+        $GLOBALS['TYPO3_REQUEST'] = $request;
+
         $response = $this->subject->process(
-            $this->request
-                ->withQueryParams(['pointer' => 0, 'reverse' => true])
-                ->withAttribute('route', new Route('/some/route', ['module' => $module])),
+            $request,
             $this->requestHandler
         );
 
@@ -198,10 +203,12 @@ class BackendModuleValidatorTest extends FunctionalTestCase
             ]
         );
 
+        $request = $this->request
+            ->withHeader('Sec-Fetch-Dest', 'document')
+            ->withAttribute('route', new Route('/some/route', ['_identifier' => 'web_layout', 'module' => $module]));
+        $GLOBALS['TYPO3_REQUEST'] = $request;
         $response = $this->subject->process(
-            $this->request
-                ->withHeader('Sec-Fetch-Dest', 'document')
-                ->withAttribute('route', new Route('/some/route', ['_identifier' => 'web_layout', 'module' => $module])),
+            $request,
             $this->requestHandler
         );
 
diff --git a/typo3/sysext/backend/Tests/Functional/Routing/RouterTest.php b/typo3/sysext/backend/Tests/Functional/Routing/RouterTest.php
index d3a6d93acf00..72cd64d5bd63 100644
--- a/typo3/sysext/backend/Tests/Functional/Routing/RouterTest.php
+++ b/typo3/sysext/backend/Tests/Functional/Routing/RouterTest.php
@@ -23,7 +23,6 @@ use TYPO3\CMS\Backend\Routing\Route;
 use TYPO3\CMS\Backend\Routing\Router;
 use TYPO3\CMS\Core\Http\NormalizedParams;
 use TYPO3\CMS\Core\Http\ServerRequest;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
 
 class RouterTest extends FunctionalTestCase
@@ -35,7 +34,7 @@ class RouterTest extends FunctionalTestCase
      */
     public function routerReturnsRouteForAlias(): void
     {
-        $subject = GeneralUtility::makeInstance(Router::class);
+        $subject = $this->get(Router::class);
         $subject->addRoute(
             'new_route_identifier',
             new Route('/new/route/path', []),
@@ -48,50 +47,62 @@ class RouterTest extends FunctionalTestCase
     /**
      * @test
      */
-    public function matchRequestFindsProperRoute(): void
+    public function matchResultFindsProperRoute(): void
     {
-        $subject = GeneralUtility::makeInstance(Router::class);
+        $subject = $this->get(Router::class);
         $request = new ServerRequest('https://example.com/login', 'GET');
         $request = $request->withAttribute('normalizedParams', NormalizedParams::createFromRequest($request));
-        $resultRoute = $subject->matchRequest($request);
-        self::assertInstanceOf(Route::class, $resultRoute);
-        self::assertEquals('/login', $resultRoute->getPath());
+        $result = $subject->matchResult($request);
+        self::assertEquals('/login', $result->getRoute()->getPath());
     }
 
     /**
      * @test
      */
-    public function matchRequestThrowsExceptionOnInvalidRoute(): void
+    public function matchResultThrowsExceptionOnInvalidRoute(): void
     {
-        $subject = GeneralUtility::makeInstance(Router::class);
+        $subject = $this->get(Router::class);
         $request = new ServerRequest('https://example.com/this-path/does-not-exist', 'GET');
         $request = $request->withAttribute('normalizedParams', NormalizedParams::createFromRequest($request));
         $this->expectException(ResourceNotFoundException::class);
-        $subject->matchRequest($request);
+        $subject->matchResult($request);
     }
 
     /**
      * @test
      */
-    public function matchRequestThrowsInvalidMethodForValidRoute(): void
+    public function matchResultThrowsInvalidMethodForValidRoute(): void
     {
-        $subject = GeneralUtility::makeInstance(Router::class);
+        $subject = $this->get(Router::class);
         $request = new ServerRequest('https://example.com/login/password-reset/initiate-reset', 'GET');
         $request = $request->withAttribute('normalizedParams', NormalizedParams::createFromRequest($request));
         $this->expectException(MethodNotAllowedException::class);
-        $subject->matchRequest($request);
+        $subject->matchResult($request);
     }
 
     /**
      * @test
      */
-    public function matchRequestReturnsRouteWithMethodLimitation(): void
+    public function matchResultReturnsRouteWithMethodLimitation(): void
     {
-        $subject = GeneralUtility::makeInstance(Router::class);
+        $subject = $this->get(Router::class);
         $request = new ServerRequest('https://example.com/login/password-reset/initiate-reset', 'POST');
         $request = $request->withAttribute('normalizedParams', NormalizedParams::createFromRequest($request));
-        $resultRoute = $subject->matchRequest($request);
-        self::assertInstanceOf(Route::class, $resultRoute);
-        self::assertEquals('/login/password-reset/initiate-reset', $resultRoute->getPath());
+        $result = $subject->matchResult($request);
+        self::assertEquals('/login/password-reset/initiate-reset', $result->getRoute()->getPath());
+    }
+
+    /**
+     * @test
+     */
+    public function matchResultReturnsRouteWithPlaceholderAndMethodLimitation(): void
+    {
+        $subject = $this->get(Router::class);
+        $subject->addRoute('custom-route', new Route('/my-path/{identifier}', []));
+        $request = new ServerRequest('https://example.com/my-path/my-identifier', 'POST');
+        $request = $request->withAttribute('normalizedParams', NormalizedParams::createFromRequest($request));
+        $result = $subject->matchResult($request);
+        self::assertEquals('custom-route', $result->getRouteName());
+        self::assertEquals(['identifier' => 'my-identifier'], $result->getArguments());
     }
 }
diff --git a/typo3/sysext/backend/Tests/Functional/Template/Components/Buttons/Action/ShortcutButtonTest.php b/typo3/sysext/backend/Tests/Functional/Template/Components/Buttons/Action/ShortcutButtonTest.php
index 613310ea0de0..e7988d347112 100644
--- a/typo3/sysext/backend/Tests/Functional/Template/Components/Buttons/Action/ShortcutButtonTest.php
+++ b/typo3/sysext/backend/Tests/Functional/Template/Components/Buttons/Action/ShortcutButtonTest.php
@@ -64,7 +64,7 @@ class ShortcutButtonTest extends FunctionalTestCase
         $this->setUpBackendUser(1);
         Bootstrap::initializeLanguageObject();
         $serverParams = array_replace($_SERVER, ['HTTP_HOST' => 'example.com', 'SCRIPT_NAME' => '/typo3/index.php']);
-        $request = new ServerRequest('https://example.com/typo3/index.php', 'GET', null, $serverParams);
+        $request = new ServerRequest('http://example.com/typo3/index.php', 'GET', null, $serverParams);
         $GLOBALS['TYPO3_REQUEST'] = $request
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE)
             ->withAttribute('normalizedParams', NormalizedParams::createFromServerParams($serverParams));
diff --git a/typo3/sysext/backend/Tests/Unit/Routing/UriBuilderTest.php b/typo3/sysext/backend/Tests/Unit/Routing/UriBuilderTest.php
index c9b198244fef..dc8131ec9002 100644
--- a/typo3/sysext/backend/Tests/Unit/Routing/UriBuilderTest.php
+++ b/typo3/sysext/backend/Tests/Unit/Routing/UriBuilderTest.php
@@ -24,6 +24,7 @@ use TYPO3\CMS\Backend\Routing\UriBuilder;
 use TYPO3\CMS\Core\FormProtection\DisabledFormProtection;
 use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
 use TYPO3\CMS\Core\Routing\BackendEntryPointResolver;
+use TYPO3\CMS\Core\Routing\RequestContextFactory;
 use TYPO3\CMS\Core\Utility\StringUtility;
 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 
@@ -81,13 +82,14 @@ class UriBuilderTest extends UnitTestCase
         array $routeParameters,
         string $expectation
     ): void {
-        $router = new Router();
+        $requestContextFactory = new RequestContextFactory(new BackendEntryPointResolver());
+        $router = new Router($requestContextFactory);
         foreach ($routes as $nameRoute => $route) {
             $router->addRoute($nameRoute, $route);
         }
         $formProtectionFactory = $this->createMock(FormProtectionFactory::class);
         $formProtectionFactory->method('createForType')->willReturn(new DisabledFormProtection());
-        $subject = new UriBuilder($router, new BackendEntryPointResolver(), $formProtectionFactory);
+        $subject = new UriBuilder($router, $formProtectionFactory, $requestContextFactory);
         $uri = $subject->buildUriFromRoute(
             $routeName,
             $routeParameters
@@ -106,7 +108,8 @@ class UriBuilderTest extends UnitTestCase
 
         $this->expectException(RouteNotFoundException::class);
         $this->expectExceptionCode(1476050190);
-        $subject = new UriBuilder(new Router(), new BackendEntryPointResolver(), $formProtectionFactory);
+        $requestContextFactory = new RequestContextFactory(new BackendEntryPointResolver());
+        $subject = new UriBuilder(new Router($requestContextFactory), $formProtectionFactory, $requestContextFactory);
         $subject->buildUriFromRoute(StringUtility::getUniqueId('any'));
     }
 }
diff --git a/typo3/sysext/backend/Tests/Unit/View/ArrayBrowserTest.php b/typo3/sysext/backend/Tests/Unit/View/ArrayBrowserTest.php
index a89724d5ad66..3384d622904f 100644
--- a/typo3/sysext/backend/Tests/Unit/View/ArrayBrowserTest.php
+++ b/typo3/sysext/backend/Tests/Unit/View/ArrayBrowserTest.php
@@ -23,6 +23,7 @@ use TYPO3\CMS\Backend\View\ArrayBrowser;
 use TYPO3\CMS\Core\FormProtection\DisabledFormProtection;
 use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
 use TYPO3\CMS\Core\Routing\BackendEntryPointResolver;
+use TYPO3\CMS\Core\Routing\RequestContextFactory;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 
@@ -40,7 +41,9 @@ class ArrayBrowserTest extends UnitTestCase
     {
         $formProtectionFactory = $this->createMock(FormProtectionFactory::class);
         $formProtectionFactory->method('createForType')->willReturn(new DisabledFormProtection());
-        GeneralUtility::setSingletonInstance(UriBuilder::class, new UriBuilder(new Router(), new BackendEntryPointResolver(), $formProtectionFactory));
+        $requestContextFactory = new RequestContextFactory(new BackendEntryPointResolver());
+        $uriBuilderMock = $this->getMockBuilder(UriBuilder::class)->setConstructorArgs([new Router($requestContextFactory), $formProtectionFactory, $requestContextFactory])->getMock();
+        GeneralUtility::setSingletonInstance(UriBuilder::class, $uriBuilderMock);
         $subject = new ArrayBrowser();
         self::assertEquals([], $subject->depthKeys([], []));
     }
@@ -52,7 +55,9 @@ class ArrayBrowserTest extends UnitTestCase
     {
         $formProtectionFactory = $this->createMock(FormProtectionFactory::class);
         $formProtectionFactory->method('createForType')->willReturn(new DisabledFormProtection());
-        GeneralUtility::setSingletonInstance(UriBuilder::class, new UriBuilder(new Router(), new BackendEntryPointResolver(), $formProtectionFactory));
+        $requestContextFactory = new RequestContextFactory(new BackendEntryPointResolver());
+        $uriBuilderMock = $this->getMockBuilder(UriBuilder::class)->setConstructorArgs([new Router($requestContextFactory), $formProtectionFactory, $requestContextFactory])->getMock();
+        GeneralUtility::setSingletonInstance(UriBuilder::class, $uriBuilderMock);
         $subject = new ArrayBrowser();
         self::assertEquals([0 => 1], $subject->depthKeys(['foo'], []));
     }
diff --git a/typo3/sysext/core/Classes/Routing/PageRouter.php b/typo3/sysext/core/Classes/Routing/PageRouter.php
index fab7d0da5ea9..4fcc663e4902 100644
--- a/typo3/sysext/core/Classes/Routing/PageRouter.php
+++ b/typo3/sysext/core/Classes/Routing/PageRouter.php
@@ -21,7 +21,6 @@ use Psr\Http\Message\ServerRequestInterface;
 use Psr\Http\Message\UriInterface;
 use Symfony\Component\Routing\Exception\MissingMandatoryParametersException;
 use Symfony\Component\Routing\Exception\ResourceNotFoundException;
-use Symfony\Component\Routing\RequestContext;
 use TYPO3\CMS\Core\Context\Context;
 use TYPO3\CMS\Core\Context\LanguageAspectFactory;
 use TYPO3\CMS\Core\Domain\Repository\PageRepository;
@@ -68,35 +67,15 @@ use TYPO3\CMS\Frontend\Page\CacheHashCalculator;
  */
 class PageRouter implements RouterInterface
 {
-    /**
-     * @var Site
-     */
-    protected $site;
-
-    /**
-     * @var EnhancerFactory
-     */
-    protected $enhancerFactory;
-
-    /**
-     * @var AspectFactory
-     */
-    protected $aspectFactory;
-
-    /**
-     * @var CacheHashCalculator
-     */
-    protected $cacheHashCalculator;
-
-    /**
-     * @var \TYPO3\CMS\Core\Context\Context
-     */
-    protected $context;
+    protected Site $site;
+    protected EnhancerFactory $enhancerFactory;
+    protected AspectFactory $aspectFactory;
+    protected CacheHashCalculator $cacheHashCalculator;
+    protected Context $context;
+    protected RequestContextFactory $requestContextFactory;
 
     /**
      * A page router is always bound to a specific site.
-     *
-     * @param \TYPO3\CMS\Core\Context\Context|null $context
      */
     public function __construct(Site $site, Context $context = null)
     {
@@ -105,6 +84,7 @@ class PageRouter implements RouterInterface
         $this->enhancerFactory = GeneralUtility::makeInstance(EnhancerFactory::class);
         $this->aspectFactory = GeneralUtility::makeInstance(AspectFactory::class, $this->context);
         $this->cacheHashCalculator = GeneralUtility::makeInstance(CacheHashCalculator::class);
+        $this->requestContextFactory = GeneralUtility::makeInstance(RequestContextFactory::class);
     }
 
     /**
@@ -306,18 +286,9 @@ class PageRouter implements RouterInterface
             }
         }
 
-        $scheme = $language->getBase()->getScheme();
         $mappableProcessor = new MappableProcessor();
-        $context = new RequestContext(
-            // page segment (slug & enhanced part) is supposed to start with '/'
-            rtrim($language->getBase()->getPath(), '/'),
-            'GET',
-            $language->getBase()->getHost(),
-            $scheme ?: 'https',
-            $scheme === 'http' ? $language->getBase()->getPort() ?? 80 : 80,
-            $scheme === 'https' ? $language->getBase()->getPort() ?? 443 : 443
-        );
-        $generator = new UrlGenerator($collection, $context);
+        $requestContext = $this->requestContextFactory->fromSiteLanguage($language);
+        $generator = new UrlGenerator($collection, $requestContext);
         $generator->injectMappableProcessor($mappableProcessor);
         // set default route flag after all routes have been processed
         $defaultRouteForPage->setOption('_isDefault', true);
diff --git a/typo3/sysext/core/Classes/Routing/RequestContextFactory.php b/typo3/sysext/core/Classes/Routing/RequestContextFactory.php
new file mode 100644
index 000000000000..0eddbf450c62
--- /dev/null
+++ b/typo3/sysext/core/Classes/Routing/RequestContextFactory.php
@@ -0,0 +1,76 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\Routing;
+
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Message\UriInterface;
+use Symfony\Component\Routing\RequestContext;
+use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
+
+/**
+ * @internal this is not part of the TYPO3 Core public API, as it serves as an internal
+ * bridge between symfony/routing component and PSR-7 requests
+ */
+class RequestContextFactory
+{
+    public function __construct(
+        protected readonly BackendEntryPointResolver $backendEntryPointResolver
+    ) {
+    }
+
+    public function fromBackendRequest(ServerRequestInterface $request): RequestContext
+    {
+        $scheme = $request->getUri()->getScheme();
+        return new RequestContext(
+            $this->backendEntryPointResolver->getPathFromRequest($request),
+            $request->getMethod(),
+            (string)idn_to_ascii($request->getUri()->getHost()),
+            $request->getUri()->getScheme(),
+            $scheme === 'http' ? $request->getUri()->getPort() ?? 80 : 80,
+            $scheme === 'https' ? $request->getUri()->getPort() ?? 443 : 443,
+        );
+    }
+
+    public function fromUri(UriInterface $uri, string $method = 'GET'): RequestContext
+    {
+        return new RequestContext(
+            '',
+            $method,
+            (string)idn_to_ascii($uri->getHost()),
+            $uri->getScheme(),
+            // Ports are only necessary for URL generation in Symfony which is not used by TYPO3
+            80,
+            443,
+            $uri->getPath()
+        );
+    }
+
+    public function fromSiteLanguage(SiteLanguage $language): RequestContext
+    {
+        $scheme = $language->getBase()->getScheme();
+        return new RequestContext(
+            // page segment (slug & enhanced part) is supposed to start with '/'
+            rtrim($language->getBase()->getPath(), '/'),
+            'GET',
+            $language->getBase()->getHost(),
+            $scheme ?: 'https',
+            $scheme === 'http' ? $language->getBase()->getPort() ?? 80 : 80,
+            $scheme === 'https' ? $language->getBase()->getPort() ?? 443 : 443
+        );
+    }
+}
diff --git a/typo3/sysext/core/Classes/Routing/SiteMatcher.php b/typo3/sysext/core/Classes/Routing/SiteMatcher.php
index 541114f68542..2097a77a8886 100644
--- a/typo3/sysext/core/Classes/Routing/SiteMatcher.php
+++ b/typo3/sysext/core/Classes/Routing/SiteMatcher.php
@@ -20,7 +20,6 @@ namespace TYPO3\CMS\Core\Routing;
 use Psr\Http\Message\ServerRequestInterface;
 use Symfony\Component\Routing\Exception\NoConfigurationException;
 use Symfony\Component\Routing\Exception\ResourceNotFoundException;
-use Symfony\Component\Routing\RequestContext;
 use TYPO3\CMS\Core\Cache\CacheManager;
 use TYPO3\CMS\Core\Exception\SiteNotFoundException;
 use TYPO3\CMS\Core\Http\NormalizedParams;
@@ -48,19 +47,10 @@ use TYPO3\CMS\Core\Utility\RootlineUtility;
  */
 class SiteMatcher implements SingletonInterface
 {
-    /**
-     * @var SiteFinder
-     */
-    protected $finder;
-
-    /**
-     * Injects necessary objects.
-     *
-     * @param SiteFinder|null $finder
-     */
-    public function __construct(SiteFinder $finder = null)
-    {
-        $this->finder = $finder ?? GeneralUtility::makeInstance(SiteFinder::class);
+    public function __construct(
+        protected readonly SiteFinder $finder,
+        protected readonly RequestContextFactory $requestContextFactory
+    ) {
     }
 
     /**
@@ -135,17 +125,8 @@ class SiteMatcher implements SingletonInterface
         // on the incoming URL.
         if (!($language instanceof SiteLanguage)) {
             $collection = $this->getRouteCollectionForAllSites();
-            $context = new RequestContext(
-                '',
-                $request->getMethod(),
-                (string)idn_to_ascii($uri->getHost()),
-                $uri->getScheme(),
-                // Ports are only necessary for URL generation in Symfony which is not used by TYPO3
-                80,
-                443,
-                $uri->getPath()
-            );
-            $matcher = new BestUrlMatcher($collection, $context);
+            $requestContext = $this->requestContextFactory->fromUri($uri, $request->getMethod());
+            $matcher = new BestUrlMatcher($collection, $requestContext);
             try {
                 $result = $matcher->match($uri->getPath());
                 return new SiteRouteResult(
diff --git a/typo3/sysext/core/Classes/Site/SiteFinder.php b/typo3/sysext/core/Classes/Site/SiteFinder.php
index 9db5d9904dcc..db4f76767326 100644
--- a/typo3/sysext/core/Classes/Site/SiteFinder.php
+++ b/typo3/sysext/core/Classes/Site/SiteFinder.php
@@ -32,19 +32,16 @@ class SiteFinder
     /**
      * @var Site[]
      */
-    protected $sites = [];
+    protected array $sites = [];
 
     /**
-     * Short-hand to quickly fetch a site based on a rootPageId
+     * Shorthand to quickly fetch a site based on a rootPageId
      *
      * @var array
      */
-    protected $mappingRootPageIdToIdentifier = [];
+    protected array $mappingRootPageIdToIdentifier = [];
 
-    /**
-     * @var SiteConfiguration
-     */
-    protected $siteConfiguration;
+    protected SiteConfiguration $siteConfiguration;
 
     /**
      * Fetches all existing configurations as Site objects
diff --git a/typo3/sysext/core/Configuration/Services.yaml b/typo3/sysext/core/Configuration/Services.yaml
index ad806b3c852f..e945bef45fe8 100644
--- a/typo3/sysext/core/Configuration/Services.yaml
+++ b/typo3/sysext/core/Configuration/Services.yaml
@@ -141,6 +141,9 @@ services:
   TYPO3\CMS\Core\Routing\BackendEntryPointResolver:
     public: true
 
+  TYPO3\CMS\Core\Routing\RequestContextFactory:
+    public: true
+
   # FAL security checks for backend users
   TYPO3\CMS\Core\Resource\Security\StoragePermissionsAspect:
     tags:
diff --git a/typo3/sysext/core/Documentation/Changelog/12.1/Feature-99234-DynamicURLPartsInTYPO3BackendURLs.rst b/typo3/sysext/core/Documentation/Changelog/12.1/Feature-99234-DynamicURLPartsInTYPO3BackendURLs.rst
new file mode 100644
index 000000000000..c4c5937e2c25
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/12.1/Feature-99234-DynamicURLPartsInTYPO3BackendURLs.rst
@@ -0,0 +1,51 @@
+.. include:: /Includes.rst.txt
+
+.. _feature-99234-1669840449:
+
+=========================================================
+Feature: #99234 - Dynamic URL parts in TYPO3 Backend URLs
+=========================================================
+
+See :issue:`99234`
+
+Description
+===========
+
+TYPO3's Backend URL routing now uses Symfony's Routing component for resolving
+and generating URLs.
+
+This way, it is possible for extension authors to register Backend Routes with
+path segments that contain dynamic parts, which are then resolved into a request
+attribute called 'routing'.
+
+These routes are defined within the route path as named placeholders.
+
+
+Impact
+======
+
+It is possible to define routes with placeholders in an extensions' :file:'Routes.php':
+
+.. code-block:: php
+
+	return [
+	    'my_route' => [
+	        'path' => '/rollback-item/{identifier}',
+	        'target' => \MyVendor\MyPackage\Controller\RollbackController::class . '::handle',
+	    ],
+	];
+
+
+Within the Controller:
+
+.. code-block:: php
+
+	public function handle(ServerRequestInterface $request): ResponseInterface
+	{
+		$routing = $request->getAttribute('routing');
+		$myIdentifier = $routing['identifier'];
+		$route = $routing->getRoute();
+        ...
+	}
+
+.. index:: Backend, PHP-API, ext:backend
diff --git a/typo3/sysext/core/Tests/Functional/Page/PageRendererTest.php b/typo3/sysext/core/Tests/Functional/Page/PageRendererTest.php
index 71416a122056..c9783b3577f2 100644
--- a/typo3/sysext/core/Tests/Functional/Page/PageRendererTest.php
+++ b/typo3/sysext/core/Tests/Functional/Page/PageRendererTest.php
@@ -454,7 +454,7 @@ class PageRendererTest extends FunctionalTestCase
 
         $GLOBALS['LANG'] = $this->get(LanguageServiceFactory::class)->createFromUserPreferences($GLOBALS['BE_USER']);
 
-        $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())
+        $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest('https://example.com/typo3/'))
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE);
 
         $subject = $this->createPageRenderer();
diff --git a/typo3/sysext/core/Tests/Unit/Routing/PageRouterTest.php b/typo3/sysext/core/Tests/Unit/Routing/PageRouterTest.php
index a04b916e03d8..661b1b5fb9fc 100644
--- a/typo3/sysext/core/Tests/Unit/Routing/PageRouterTest.php
+++ b/typo3/sysext/core/Tests/Unit/Routing/PageRouterTest.php
@@ -18,9 +18,11 @@ declare(strict_types=1);
 namespace TYPO3\CMS\Core\Tests\Unit\Routing;
 
 use TYPO3\CMS\Core\Http\ServerRequest;
+use TYPO3\CMS\Core\Routing\BackendEntryPointResolver;
 use TYPO3\CMS\Core\Routing\PageArguments;
 use TYPO3\CMS\Core\Routing\PageRouter;
 use TYPO3\CMS\Core\Routing\PageSlugCandidateProvider;
+use TYPO3\CMS\Core\Routing\RequestContextFactory;
 use TYPO3\CMS\Core\Routing\RouteNotFoundException;
 use TYPO3\CMS\Core\Routing\SiteRouteResult;
 use TYPO3\CMS\Core\Site\Entity\Site;
@@ -31,6 +33,13 @@ class PageRouterTest extends UnitTestCase
 {
     protected bool $resetSingletonInstances = true;
 
+    protected function setUp(): void
+    {
+        $requestContextFactory = new RequestContextFactory(new BackendEntryPointResolver());
+        GeneralUtility::addInstance(RequestContextFactory::class, $requestContextFactory);
+        parent::setUp(); // TODO: Change the autogenerated stub
+    }
+
     /**
      * @test
      */
diff --git a/typo3/sysext/core/Tests/Unit/Routing/SiteMatcherTest.php b/typo3/sysext/core/Tests/Unit/Routing/SiteMatcherTest.php
index 970bf15204f6..fc1738162a63 100644
--- a/typo3/sysext/core/Tests/Unit/Routing/SiteMatcherTest.php
+++ b/typo3/sysext/core/Tests/Unit/Routing/SiteMatcherTest.php
@@ -17,7 +17,10 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Core\Tests\Unit\Routing;
 
+use TYPO3\CMS\Core\Configuration\SiteConfiguration;
 use TYPO3\CMS\Core\Http\ServerRequest;
+use TYPO3\CMS\Core\Routing\BackendEntryPointResolver;
+use TYPO3\CMS\Core\Routing\RequestContextFactory;
 use TYPO3\CMS\Core\Routing\SiteMatcher;
 use TYPO3\CMS\Core\Routing\SiteRouteResult;
 use TYPO3\CMS\Core\Site\Entity\Site;
@@ -71,8 +74,9 @@ class SiteMatcherTest extends UnitTestCase
                 ],
             ],
         ]);
-        $finderMock = $this->createSiteFinderMock($site, $secondSite);
-        $subject = new SiteMatcher($finderMock);
+        $finderMock = $this->createSiteFinder($site, $secondSite);
+        $requestContextFactory = new RequestContextFactory(new BackendEntryPointResolver());
+        $subject = new SiteMatcher($finderMock, $requestContextFactory);
 
         $request = new ServerRequest('http://9-5.typo3.test/da/my-page/');
         /** @var SiteRouteResult $result */
@@ -166,8 +170,9 @@ class SiteMatcherTest extends UnitTestCase
                 ],
             ],
         ]);
-        $finderMock = $this->createSiteFinderMock($site, $secondSite);
-        $subject = new SiteMatcher($finderMock);
+        $finderMock = $this->createSiteFinder($site, $secondSite);
+        $requestContextFactory = new RequestContextFactory(new BackendEntryPointResolver());
+        $subject = new SiteMatcher($finderMock, $requestContextFactory);
 
         $request = new ServerRequest('https://www.example.com/de');
         /** @var SiteRouteResult $result */
@@ -247,8 +252,9 @@ class SiteMatcherTest extends UnitTestCase
             ],
         ]);
 
-        $finderMock = $this->createSiteFinderMock($mainSite, $dkSite, $frSite);
-        $subject = new SiteMatcher($finderMock);
+        $finderMock = $this->createSiteFinder($mainSite, $dkSite, $frSite);
+        $requestContextFactory = new RequestContextFactory(new BackendEntryPointResolver());
+        $subject = new SiteMatcher($finderMock, $requestContextFactory);
 
         $request = new ServerRequest($requestUri);
         /** @var SiteRouteResult $result */
@@ -258,18 +264,23 @@ class SiteMatcherTest extends UnitTestCase
         self::assertSame($expectedLocale, $result->getLanguage()->getLocale());
     }
 
-    private function createSiteFinderMock(Site ...$sites): SiteFinder
+    private function createSiteFinder(Site ...$sites): SiteFinder
     {
-        /** @var SiteFinder $finderMock */
-        $finderMock = $this
-            ->getMockBuilder(SiteFinder::class)
-            ->onlyMethods(['getAllSites'])
-            ->disableOriginalConstructor()
-            ->getMock();
-        $finderMock->method('getAllSites')->willReturn(array_combine(
-            array_map(static function (Site $site) { return $site->getIdentifier(); }, $sites),
-            $sites
-        ));
-        return $finderMock;
+        $siteConfiguration = new class ($sites) extends SiteConfiguration {
+            public function __construct(
+                protected array $sites
+            ) {
+                // empty by default
+            }
+
+            public function getAllExistingSites(bool $useCache = true): array
+            {
+                return array_combine(
+                    array_map(static function (Site $site) { return $site->getIdentifier(); }, $this->sites),
+                    $this->sites
+                );
+            }
+        };
+        return new SiteFinder(new $siteConfiguration($sites));
     }
 }
diff --git a/typo3/sysext/extbase/Tests/Unit/Mvc/Web/Routing/UriBuilderTest.php b/typo3/sysext/extbase/Tests/Unit/Mvc/Web/Routing/UriBuilderTest.php
index d9f98b066558..70f5dcc8e8a1 100644
--- a/typo3/sysext/extbase/Tests/Unit/Mvc/Web/Routing/UriBuilderTest.php
+++ b/typo3/sysext/extbase/Tests/Unit/Mvc/Web/Routing/UriBuilderTest.php
@@ -30,6 +30,7 @@ use TYPO3\CMS\Core\Http\NormalizedParams;
 use TYPO3\CMS\Core\Http\ServerRequest;
 use TYPO3\CMS\Core\Http\Uri;
 use TYPO3\CMS\Core\Routing\BackendEntryPointResolver;
+use TYPO3\CMS\Core\Routing\RequestContextFactory;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Extbase\Configuration\ConfigurationManager;
 use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
@@ -93,13 +94,14 @@ class UriBuilderTest extends UnitTestCase
         $this->uriBuilder->injectExtensionService($this->mockExtensionService);
         $this->uriBuilder->initializeObject();
         $this->uriBuilder->_set('contentObject', $this->mockContentObject);
-        $router = GeneralUtility::makeInstance(Router::class);
+        $requestContextFactory = new RequestContextFactory(new BackendEntryPointResolver());
+        $router = new Router($requestContextFactory);
         $router->addRoute('module_key', new Route('/test/Path', []));
         $router->addRoute('module_key2', new Route('/test/Path2', []));
         $router->addRoute('', new Route('', []));
         $formProtectionFactory = $this->createMock(FormProtectionFactory::class);
         $formProtectionFactory->method('createForType')->willReturn(new DisabledFormProtection());
-        GeneralUtility::setSingletonInstance(BackendUriBuilder::class, new BackendUriBuilder($router, new BackendEntryPointResolver(), $formProtectionFactory));
+        GeneralUtility::setSingletonInstance(BackendUriBuilder::class, new BackendUriBuilder($router, $formProtectionFactory, $requestContextFactory));
     }
 
     /**
@@ -382,7 +384,7 @@ class UriBuilderTest extends UnitTestCase
         $_SERVER['SCRIPT_NAME'] = '/index.php';
         $_SERVER['ORIG_SCRIPT_NAME'] = '/index.php';
         $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
-        $GLOBALS['TYPO3_REQUEST'] = $this->getRequestWithRouteAttribute()
+        $GLOBALS['TYPO3_REQUEST'] = $this->getRequestWithRouteAttribute(baseUri: 'http://baseuri/typo3/')
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE)
             ->withAttribute('normalizedParams', NormalizedParams::createFromServerParams($_SERVER));
         $this->uriBuilder->setCreateAbsoluteUri(true);
@@ -880,8 +882,8 @@ class UriBuilderTest extends UnitTestCase
         self::assertIsArray($result);
     }
 
-    protected function getRequestWithRouteAttribute(string $path = '/test/Path'): ServerRequestInterface
+    protected function getRequestWithRouteAttribute(string $path = '/test/Path', string $baseUri = ''): ServerRequestInterface
     {
-        return (new ServerRequest(new Uri('')))->withAttribute('route', new Route($path, []));
+        return (new ServerRequest(new Uri($baseUri)))->withAttribute('route', new Route($path, []));
     }
 }
diff --git a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Be/LinkViewHelperTest.php b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Be/LinkViewHelperTest.php
index 26c2ef98b796..3f7d7dd48903 100644
--- a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Be/LinkViewHelperTest.php
+++ b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Be/LinkViewHelperTest.php
@@ -21,6 +21,7 @@ use TYPO3\CMS\Backend\Routing\Router;
 use TYPO3\CMS\Backend\Routing\UriBuilder;
 use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
 use TYPO3\CMS\Core\Routing\BackendEntryPointResolver;
+use TYPO3\CMS\Core\Routing\RequestContextFactory;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Fluid\Core\Rendering\RenderingContextFactory;
 use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
@@ -37,7 +38,8 @@ class LinkViewHelperTest extends FunctionalTestCase
     {
         // Mock Uribuilder in this functional test so we don't have to work with existing routes
         $formProtectionFactoryMock = $this->createMock(FormProtectionFactory::class);
-        $uriBuilderMock = $this->getMockBuilder(UriBuilder::class)->setConstructorArgs([new Router(), new BackendEntryPointResolver(), $formProtectionFactoryMock])->getMock();
+        $requestContextFactory = new RequestContextFactory(new BackendEntryPointResolver());
+        $uriBuilderMock = $this->getMockBuilder(UriBuilder::class)->setConstructorArgs([new Router($requestContextFactory), $formProtectionFactoryMock, $requestContextFactory])->getMock();
         $uriBuilderMock->expects(self::once())->method('buildUriFromRoute')
             ->with('theRouteArgument', ['parameter' => 'to pass'], 'theReferenceTypeArgument')->willReturn('theUri');
         GeneralUtility::setSingletonInstance(UriBuilder::class, $uriBuilderMock);
@@ -56,7 +58,8 @@ class LinkViewHelperTest extends FunctionalTestCase
     {
         // Mock Uribuilder in this functional test so we don't have to work with existing routes
         $formProtectionFactoryMock = $this->createMock(FormProtectionFactory::class);
-        $uriBuilderMock = $this->getMockBuilder(UriBuilder::class)->setConstructorArgs([new Router(), new BackendEntryPointResolver(), $formProtectionFactoryMock])->getMock();
+        $requestContextFactory = new RequestContextFactory(new BackendEntryPointResolver());
+        $uriBuilderMock = $this->getMockBuilder(UriBuilder::class)->setConstructorArgs([new Router($requestContextFactory), $formProtectionFactoryMock, $requestContextFactory])->getMock();
         $uriBuilderMock->expects(self::once())->method('buildUriFromRoute')
             ->with('theRouteArgument', [], 'theReferenceTypeArgument')->willReturn('theUri');
         GeneralUtility::setSingletonInstance(UriBuilder::class, $uriBuilderMock);
diff --git a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Be/UriViewHelperTest.php b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Be/UriViewHelperTest.php
index be2165be43d8..c3c7945969cb 100644
--- a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Be/UriViewHelperTest.php
+++ b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Be/UriViewHelperTest.php
@@ -21,6 +21,7 @@ use TYPO3\CMS\Backend\Routing\Router;
 use TYPO3\CMS\Backend\Routing\UriBuilder;
 use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
 use TYPO3\CMS\Core\Routing\BackendEntryPointResolver;
+use TYPO3\CMS\Core\Routing\RequestContextFactory;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Fluid\Core\Rendering\RenderingContextFactory;
 use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
@@ -37,7 +38,8 @@ class UriViewHelperTest extends FunctionalTestCase
     {
         // Mock Uribuilder in this functional test so we don't have to work with existing routes
         $formProtectionFactoryMock = $this->createMock(FormProtectionFactory::class);
-        $uriBuilderMock = $this->getMockBuilder(UriBuilder::class)->setConstructorArgs([new Router(), new BackendEntryPointResolver(), $formProtectionFactoryMock])->getMock();
+        $requestContextFactory = new RequestContextFactory(new BackendEntryPointResolver());
+        $uriBuilderMock = $this->getMockBuilder(UriBuilder::class)->setConstructorArgs([new Router($requestContextFactory), $formProtectionFactoryMock, $requestContextFactory])->getMock();
         $uriBuilderMock->expects(self::once())->method('buildUriFromRoute')
             ->with('theRouteArgument', ['parameter' => 'to pass'], 'theReferenceTypeArgument')->willReturn('theUri');
         GeneralUtility::setSingletonInstance(UriBuilder::class, $uriBuilderMock);
@@ -56,7 +58,8 @@ class UriViewHelperTest extends FunctionalTestCase
     {
         // Mock Uribuilder in this functional test so we don't have to work with existing routes
         $formProtectionFactoryMock = $this->createMock(FormProtectionFactory::class);
-        $uriBuilderMock = $this->getMockBuilder(UriBuilder::class)->setConstructorArgs([new Router(), new BackendEntryPointResolver(), $formProtectionFactoryMock])->getMock();
+        $requestContextFactory = new RequestContextFactory(new BackendEntryPointResolver());
+        $uriBuilderMock = $this->getMockBuilder(UriBuilder::class)->setConstructorArgs([new Router($requestContextFactory), $formProtectionFactoryMock, $requestContextFactory])->getMock();
         $uriBuilderMock->expects(self::once())->method('buildUriFromRoute')
             ->with('theRouteArgument', [], 'theReferenceTypeArgument')->willReturn('theUri');
         GeneralUtility::setSingletonInstance(UriBuilder::class, $uriBuilderMock);
diff --git a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Link/PageViewHelperTest.php b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Link/PageViewHelperTest.php
index 9c2f6bb9eec4..4dbc40f43ea8 100644
--- a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Link/PageViewHelperTest.php
+++ b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Link/PageViewHelperTest.php
@@ -111,7 +111,7 @@ class PageViewHelperTest extends FunctionalTestCase
      */
     public function renderInBackendCoreContextAddsSection(): void
     {
-        $request = new ServerRequest();
+        $request = new ServerRequest('http://localhost/typo3/');
         $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE);
         $request = $request->withAttribute('route', new Route('dummy', ['_identifier' => 'web_layout']));
         $view = new StandaloneView();
@@ -126,7 +126,7 @@ class PageViewHelperTest extends FunctionalTestCase
      */
     public function renderInBackendCoreContextCreatesAbsoluteLink(): void
     {
-        $request = new ServerRequest(null, null, 'php://input', [], ['HTTP_HOST' => 'localhost', 'SCRIPT_NAME' => 'typo3/index.php']);
+        $request = new ServerRequest('http://localhost/typo3/', null, 'php://input', [], ['HTTP_HOST' => 'localhost', 'SCRIPT_NAME' => 'typo3/index.php']);
         $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE);
         $request = $request->withAttribute('route', new Route('dummy', ['_identifier' => 'web_layout']));
         $GLOBALS['TYPO3_REQUEST'] = $request;
@@ -142,7 +142,7 @@ class PageViewHelperTest extends FunctionalTestCase
      */
     public function renderInBackendExtbaseContextCreatesLinkWithId(): void
     {
-        $request = new ServerRequest();
+        $request = new ServerRequest('http://localhost/typo3/');
         $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE);
         $request = $request->withAttribute('route', new Route('module/web/layout', ['_identifier' => 'web_layout']));
         $request = $request->withAttribute('extbase', new ExtbaseRequestParameters());
@@ -161,7 +161,7 @@ class PageViewHelperTest extends FunctionalTestCase
      */
     public function renderInBackendExtbaseContextCreatesAbsoluteLinkWithId(): void
     {
-        $request = new ServerRequest(null, null, 'php://input', [], ['HTTP_HOST' => 'localhost', 'SCRIPT_NAME' => 'typo3/index.php']);
+        $request = new ServerRequest('http://localhost/typo3/', null, 'php://input', [], ['HTTP_HOST' => 'localhost', 'SCRIPT_NAME' => 'typo3/index.php']);
         $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE);
         $request = $request->withAttribute('route', new Route('module/web/layout', ['_identifier' => 'web_layout']));
         $request = $request->withAttribute('extbase', new ExtbaseRequestParameters());
@@ -216,7 +216,7 @@ class PageViewHelperTest extends FunctionalTestCase
             'test',
             $this->buildSiteConfiguration(1, '/'),
         );
-        $request = new ServerRequest();
+        $request = new ServerRequest('http://localhost/typo3/');
         $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE);
         $GLOBALS['TYPO3_REQUEST'] = $request;
         $GLOBALS['TSFE'] = $this->createMock(TypoScriptFrontendController::class);
diff --git a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/PageRendererViewHelperTest.php b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/PageRendererViewHelperTest.php
index d07d5c9714df..9737c1703443 100644
--- a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/PageRendererViewHelperTest.php
+++ b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/PageRendererViewHelperTest.php
@@ -72,7 +72,7 @@ class PageRendererViewHelperTest extends FunctionalTestCase
         $view->render();
         $pageRenderer = $this->get(PageRenderer::class);
         // PageRenderer depends on request to determine FE vs. BE
-        $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE);
+        $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest('https://example.com/typo3/'))->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE);
         self::assertStringContainsString($expected, $pageRenderer->renderResponse()->getBody()->__toString());
     }
 
@@ -85,7 +85,7 @@ class PageRendererViewHelperTest extends FunctionalTestCase
         $context->getTemplatePaths()->setTemplateSource('<f:be.pageRenderer addJsInlineLabels="{0: \'login.header\'}" />');
         $extbaseRequestParameters = new ExtbaseRequestParameters();
         $extbaseRequestParameters->setControllerExtensionName('Backend');
-        $serverRequest = (new ServerRequest())->withAttribute('extbase', $extbaseRequestParameters)->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE);
+        $serverRequest = (new ServerRequest('https://example.com/typo3/'))->withAttribute('extbase', $extbaseRequestParameters)->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE);
         $GLOBALS['TYPO3_REQUEST'] = $serverRequest;
         $context->setRequest(new Request($serverRequest));
         $view = new TemplateView($context);
@@ -93,7 +93,7 @@ class PageRendererViewHelperTest extends FunctionalTestCase
         $view->render();
         $pageRenderer = $this->get(PageRenderer::class);
         // PageRenderer depends on request to determine FE vs. BE
-        $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE);
+        $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest('https://example.com/typo3/'))->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE);
         self::assertStringContainsString('"lang":{"login.header":"Login"}', $pageRenderer->renderResponse()->getBody()->__toString());
     }
 }
diff --git a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Uri/PageViewHelperTest.php b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Uri/PageViewHelperTest.php
index b567a9d2ac3d..005be0d76069 100644
--- a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Uri/PageViewHelperTest.php
+++ b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Uri/PageViewHelperTest.php
@@ -53,7 +53,7 @@ class PageViewHelperTest extends FunctionalTestCase
      */
     public function renderInBackendCoreContextCreatesNoUriWithoutRoute(): void
     {
-        $request = new ServerRequest();
+        $request = new ServerRequest('http://localhost/typo3/');
         $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE);
         $view = new StandaloneView();
         $view->setRequest($request);
@@ -67,7 +67,7 @@ class PageViewHelperTest extends FunctionalTestCase
      */
     public function renderInBackendCoreContextCreatesUriWithRouteFromQueryString(): void
     {
-        $request = new ServerRequest();
+        $request = new ServerRequest('http://localhost/typo3/');
         $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE);
         $request = $request->withQueryParams(['route' => 'web_layout']);
         $view = new StandaloneView();
@@ -82,7 +82,7 @@ class PageViewHelperTest extends FunctionalTestCase
      */
     public function renderInBackendCoreContextCreatesUriWithRouteFromAdditionalParams(): void
     {
-        $request = new ServerRequest();
+        $request = new ServerRequest('http://localhost/typo3/');
         $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE);
         $view = new StandaloneView();
         $view->setRequest($request);
@@ -96,7 +96,7 @@ class PageViewHelperTest extends FunctionalTestCase
      */
     public function renderInBackendCoreContextCreatesUriWithRouteFromRequest(): void
     {
-        $request = new ServerRequest();
+        $request = new ServerRequest('http://localhost/typo3/');
         $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE);
         $request = $request->withAttribute('route', new Route('dummy', ['_identifier' => 'web_layout']));
         $view = new StandaloneView();
@@ -111,7 +111,7 @@ class PageViewHelperTest extends FunctionalTestCase
      */
     public function renderInBackendCoreContextAddsSection(): void
     {
-        $request = new ServerRequest();
+        $request = new ServerRequest('http://localhost/typo3/');
         $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE);
         $request = $request->withAttribute('route', new Route('dummy', ['_identifier' => 'web_layout']));
         $view = new StandaloneView();
@@ -126,7 +126,7 @@ class PageViewHelperTest extends FunctionalTestCase
      */
     public function renderInBackendCoreContextCreatesAbsoluteUri(): void
     {
-        $request = new ServerRequest(null, null, 'php://input', [], ['HTTP_HOST' => 'localhost', 'SCRIPT_NAME' => 'typo3/index.php']);
+        $request = new ServerRequest('http://localhost/typo3/', null, 'php://input', [], ['HTTP_HOST' => 'localhost', 'SCRIPT_NAME' => 'typo3/index.php']);
         $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE);
         $request = $request->withAttribute('route', new Route('dummy', ['_identifier' => 'web_layout']));
         $GLOBALS['TYPO3_REQUEST'] = $request;
@@ -142,7 +142,7 @@ class PageViewHelperTest extends FunctionalTestCase
      */
     public function renderInBackendExtbaseContextCreatesUriWithId(): void
     {
-        $request = new ServerRequest();
+        $request = new ServerRequest('http://localhost/typo3/');
         $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE);
         $request = $request->withAttribute('route', new Route('module/web/layout', ['_identifier' => 'web_layout']));
         $request = $request->withAttribute('extbase', new ExtbaseRequestParameters());
@@ -161,7 +161,7 @@ class PageViewHelperTest extends FunctionalTestCase
      */
     public function renderInBackendExtbaseContextCreatesAbsoluteUriWithId(): void
     {
-        $request = new ServerRequest(null, null, 'php://input', [], ['HTTP_HOST' => 'localhost', 'SCRIPT_NAME' => 'typo3/index.php']);
+        $request = new ServerRequest('http://localhost/typo3/', null, 'php://input', [], ['HTTP_HOST' => 'localhost', 'SCRIPT_NAME' => 'typo3/index.php']);
         $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE);
         $request = $request->withAttribute('route', new Route('module/web/layout', ['_identifier' => 'web_layout']));
         $request = $request->withAttribute('extbase', new ExtbaseRequestParameters());
diff --git a/typo3/sysext/frontend/Classes/Typolink/AbstractTypolinkBuilder.php b/typo3/sysext/frontend/Classes/Typolink/AbstractTypolinkBuilder.php
index 619514c7ec4b..437e639a8918 100644
--- a/typo3/sysext/frontend/Classes/Typolink/AbstractTypolinkBuilder.php
+++ b/typo3/sysext/frontend/Classes/Typolink/AbstractTypolinkBuilder.php
@@ -231,15 +231,18 @@ abstract class AbstractTypolinkBuilder
             $language = $site->getDefaultLanguage();
         }
 
-        $id = $request->getQueryParams()['id'] ?? $request->getParsedBody()['id'] ?? $site->getRootPageId();
-        $type = $request->getQueryParams()['type'] ?? $request->getParsedBody()['type'] ?? '0';
-
+        $pageArguments = $request->getAttribute('routing');
+        if (!($pageArguments instanceof PageArguments)) {
+            $id = $request->getQueryParams()['id'] ?? $request->getParsedBody()['id'] ?? $site->getRootPageId();
+            $type = $request->getQueryParams()['type'] ?? $request->getParsedBody()['type'] ?? '0';
+            $pageArguments = new PageArguments((int)$id, (string)$type, []);
+        }
         $this->typoScriptFrontendController = GeneralUtility::makeInstance(
             TypoScriptFrontendController::class,
             GeneralUtility::makeInstance(Context::class),
             $site,
             $language,
-            $request->getAttribute('routing', new PageArguments((int)$id, (string)$type, [])),
+            $pageArguments,
             GeneralUtility::makeInstance(FrontendUserAuthentication::class)
         );
         $this->typoScriptFrontendController->sys_page = GeneralUtility::makeInstance(PageRepository::class);
diff --git a/typo3/sysext/frontend/Tests/Unit/Middleware/SiteResolverTest.php b/typo3/sysext/frontend/Tests/Unit/Middleware/SiteResolverTest.php
index 66dc978b8457..8e4c6a46797f 100644
--- a/typo3/sysext/frontend/Tests/Unit/Middleware/SiteResolverTest.php
+++ b/typo3/sysext/frontend/Tests/Unit/Middleware/SiteResolverTest.php
@@ -21,9 +21,12 @@ use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use Psr\Http\Server\RequestHandlerInterface;
 use TYPO3\CMS\Core\Cache\CacheManager;
+use TYPO3\CMS\Core\Configuration\SiteConfiguration;
 use TYPO3\CMS\Core\Http\JsonResponse;
 use TYPO3\CMS\Core\Http\NullResponse;
 use TYPO3\CMS\Core\Http\ServerRequest;
+use TYPO3\CMS\Core\Routing\BackendEntryPointResolver;
+use TYPO3\CMS\Core\Routing\RequestContextFactory;
 use TYPO3\CMS\Core\Routing\SiteMatcher;
 use TYPO3\CMS\Core\Site\Entity\Site;
 use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
@@ -98,20 +101,18 @@ class SiteResolverTest extends UnitTestCase
     {
         $incomingUrl = 'https://a-random-domain.com/mysite/';
         $siteIdentifier = 'full-site';
-        $this->siteFinder->_set('sites', [
-            $siteIdentifier => new Site($siteIdentifier, 13, [
-                'base' => '/mysite/',
-                'languages' => [
-                    0 => [
-                        'languageId' => 0,
-                        'locale' => 'fr_FR.UTF-8',
-                        'base' => '/',
-                    ],
+        $siteFinder = $this->createSiteFinder(new Site($siteIdentifier, 13, [
+            'base' => '/mysite/',
+            'languages' => [
+                0 => [
+                    'languageId' => 0,
+                    'locale' => 'fr_FR.UTF-8',
+                    'base' => '/',
                 ],
-            ]),
-        ]);
-
-        $subject = new SiteResolver(new SiteMatcher($this->siteFinder));
+            ],
+        ]));
+        $requestContextFactory = new RequestContextFactory(new BackendEntryPointResolver());
+        $subject = new SiteResolver(new SiteMatcher($siteFinder, $requestContextFactory));
 
         $request = new ServerRequest($incomingUrl, 'GET');
         $response = $subject->process($request, $this->siteFoundRequestHandler);
@@ -141,8 +142,8 @@ class SiteResolverTest extends UnitTestCase
     public function detectSubsiteInsideNestedUrlStructure(): void
     {
         $incomingUrl = 'https://www.random-result.com/mysubsite/you-know-why/';
-        $this->siteFinder->_set('sites', [
-            'outside-site' => new Site('outside-site', 13, [
+        $siteFinder = $this->createSiteFinder(
+            new Site('outside-site', 13, [
                 'base' => '/',
                 'languages' => [
                     0 => [
@@ -152,7 +153,7 @@ class SiteResolverTest extends UnitTestCase
                     ],
                 ],
             ]),
-            'sub-site' => new Site('sub-site', 15, [
+            new Site('sub-site', 15, [
                 'base' => '/mysubsite/',
                 'languages' => [
                     0 => [
@@ -162,9 +163,10 @@ class SiteResolverTest extends UnitTestCase
                     ],
                 ],
             ]),
-        ]);
+        );
 
-        $subject = new SiteResolver(new SiteMatcher($this->siteFinder));
+        $requestContextFactory = new RequestContextFactory(new BackendEntryPointResolver());
+        $subject = new SiteResolver(new SiteMatcher($siteFinder, $requestContextFactory));
 
         $request = new ServerRequest($incomingUrl, 'GET');
         $response = $subject->process($request, $this->siteFoundRequestHandler);
@@ -221,8 +223,8 @@ class SiteResolverTest extends UnitTestCase
      */
     public function detectSubSubsiteInsideNestedUrlStructure($incomingUrl, $expectedSiteIdentifier, $expectedRootPageId, $expectedBase): void
     {
-        $this->siteFinder->_set('sites', [
-            'outside-site' => new Site('outside-site', 13, [
+        $siteFinder = $this->createSiteFinder(
+            new Site('outside-site', 13, [
                 'base' => '/',
                 'languages' => [
                     0 => [
@@ -232,7 +234,7 @@ class SiteResolverTest extends UnitTestCase
                     ],
                 ],
             ]),
-            'sub-site' => new Site('sub-site', 14, [
+            new Site('sub-site', 14, [
                 'base' => '/mysubsite/',
                 'languages' => [
                     0 => [
@@ -242,7 +244,7 @@ class SiteResolverTest extends UnitTestCase
                     ],
                 ],
             ]),
-            'subsub-site' => new Site('subsub-site', 15, [
+            new Site('subsub-site', 15, [
                 'base' => '/mysubsite/micro-site/',
                 'languages' => [
                     0 => [
@@ -252,9 +254,10 @@ class SiteResolverTest extends UnitTestCase
                     ],
                 ],
             ]),
-        ]);
+        );
 
-        $subject = new SiteResolver(new SiteMatcher($this->siteFinder));
+        $requestContextFactory = new RequestContextFactory(new BackendEntryPointResolver());
+        $subject = new SiteResolver(new SiteMatcher($siteFinder, $requestContextFactory));
 
         $request = new ServerRequest($incomingUrl, 'GET');
         $response = $subject->process($request, $this->siteFoundRequestHandler);
@@ -326,8 +329,8 @@ class SiteResolverTest extends UnitTestCase
      */
     public function detectProperLanguageByIncomingUrl($incomingUrl, $expectedSiteIdentifier, $expectedRootPageId, $expectedLanguageId, $expectedBase): void
     {
-        $this->siteFinder->_set('sites', [
-            'outside-site' => new Site('outside-site', 13, [
+        $siteFinder = $this->createSiteFinder(
+            new Site('outside-site', 13, [
                 'base' => '/',
                 'languages' => [
                     0 => [
@@ -342,7 +345,7 @@ class SiteResolverTest extends UnitTestCase
                     ],
                 ],
             ]),
-            'sub-site' => new Site('sub-site', 14, [
+            new Site('sub-site', 14, [
                 'base' => '/mysubsite/',
                 'languages' => [
                     2 => [
@@ -352,7 +355,7 @@ class SiteResolverTest extends UnitTestCase
                     ],
                 ],
             ]),
-            'subsub-site' => new Site('subsub-site', 15, [
+            new Site('subsub-site', 15, [
                 'base' => '/mysubsite/micro-site/',
                 'languages' => [
                     13 => [
@@ -362,9 +365,10 @@ class SiteResolverTest extends UnitTestCase
                     ],
                 ],
             ]),
-        ]);
+        );
 
-        $subject = new SiteResolver(new SiteMatcher($this->siteFinder));
+        $requestContextFactory = new RequestContextFactory(new BackendEntryPointResolver());
+        $subject = new SiteResolver(new SiteMatcher($siteFinder, $requestContextFactory));
 
         $request = new ServerRequest($incomingUrl, 'GET');
         $response = $subject->process($request, $this->siteFoundRequestHandler);
@@ -380,4 +384,24 @@ class SiteResolverTest extends UnitTestCase
             self::assertEquals($expectedBase, $result['language-base']);
         }
     }
+
+    private function createSiteFinder(Site ...$sites): SiteFinder
+    {
+        $siteConfiguration = new class ($sites) extends SiteConfiguration {
+            public function __construct(
+                protected array $sites
+            ) {
+                // empty by default
+            }
+
+            public function getAllExistingSites(bool $useCache = true): array
+            {
+                return array_combine(
+                    array_map(static function (Site $site) { return $site->getIdentifier(); }, $this->sites),
+                    $this->sites
+                );
+            }
+        };
+        return new SiteFinder(new $siteConfiguration($sites));
+    }
 }
-- 
GitLab