From b677ffbd0be8af6cdab86cdf0c42163cb566bd32 Mon Sep 17 00:00:00 2001
From: Benjamin Franzke <bfr@qbus.de>
Date: Thu, 17 Feb 2022 18:30:31 +0100
Subject: [PATCH] [TASK] Use dependency injection in PageRenderer
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Add dependency injection to PageRenderer and it's direct
dependencies. Most prominently this avoids the static
assets-cache injection from TYPO3 bootstrap and refactors to
regular injection instead.
ResourceCompressor is adapted to be delay it's initialization
to be injectable without immediate side-effects.

Backend Router and UriBuilder are not injected because they are
a) only needed in backend context and
b) should ideally be moved out of the PageRenderer
   into EXT:backend anyway.

Resolves: #97030
Releases: main
Change-Id: I8e72e5f87a095372439c3cd8106318ae272c6ce0
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/73594
Tested-by: core-ci <typo3@b13.com>
Tested-by: Benni Mack <benni@typo3.org>
Tested-by: Stefan Bürk <stefan@buerk.tech>
Tested-by: Benjamin Franzke <bfr@qbus.de>
Reviewed-by: Benni Mack <benni@typo3.org>
Reviewed-by: Stefan Bürk <stefan@buerk.tech>
Reviewed-by: Benjamin Franzke <bfr@qbus.de>
---
 Build/phpstan/phpstan-baseline.neon           |   5 -
 .../sysext/core/Classes/Core/BootService.php  |   3 -
 typo3/sysext/core/Classes/Core/Bootstrap.php  |   2 -
 .../sysext/core/Classes/Page/PageRenderer.php | 151 +++++++----------
 .../Classes/Resource/ResourceCompressor.php   |  19 ++-
 .../Service/MarkerBasedTemplateService.php    |  40 ++---
 typo3/sysext/core/Configuration/Services.yaml |  10 ++
 .../Functional/Page/PageRendererTest.php      |  34 +++-
 .../Unit/Page/PageRendererFactoryTrait.php    |  79 +++++++++
 .../core/Tests/Unit/Page/PageRendererTest.php |  23 ++-
 .../ResourceCompressorIntegrationTest.php     |  15 +-
 .../MarkerBasedTemplateServiceTest.php        |  11 +-
 .../ContentObject/ImageContentObject.php      |  11 +-
 .../FluidTemplateContentObjectTest.php        | 153 +++++++++++++++++-
 .../ContentObject/ImageContentObjectTest.php  |  34 ++--
 .../Menu/AbstractMenuContentObjectTest.php    |   7 +
 .../TypoScriptFrontendControllerTest.php      |  10 +-
 .../Tests/Unit/Plugin/AbstractPluginTest.php  |   6 +
 18 files changed, 431 insertions(+), 182 deletions(-)
 create mode 100644 typo3/sysext/core/Tests/Unit/Page/PageRendererFactoryTrait.php

diff --git a/Build/phpstan/phpstan-baseline.neon b/Build/phpstan/phpstan-baseline.neon
index 72db186eba44..247eaf864bc5 100644
--- a/Build/phpstan/phpstan-baseline.neon
+++ b/Build/phpstan/phpstan-baseline.neon
@@ -1320,11 +1320,6 @@ parameters:
 			count: 1
 			path: ../../typo3/sysext/core/Classes/Page/PageRenderer.php
 
-		-
-			message: "#^Static property TYPO3\\\\CMS\\\\Core\\\\Page\\\\PageRenderer\\:\\:\\$cache \\(TYPO3\\\\CMS\\\\Core\\\\Cache\\\\Frontend\\\\FrontendInterface\\) on left side of \\?\\? is not nullable\\.$#"
-			count: 1
-			path: ../../typo3/sysext/core/Classes/Page/PageRenderer.php
-
 		-
 			message: "#^Strict comparison using \\=\\=\\= between array\\{settings\\?\\: non\\-empty\\-array, lang\\?\\: non\\-empty\\-array\\}&non\\-empty\\-array and array\\{\\} will always evaluate to false\\.$#"
 			count: 1
diff --git a/typo3/sysext/core/Classes/Core/BootService.php b/typo3/sysext/core/Classes/Core/BootService.php
index c54dad2628bb..1915b1b7cf75 100644
--- a/typo3/sysext/core/Classes/Core/BootService.php
+++ b/typo3/sysext/core/Classes/Core/BootService.php
@@ -22,7 +22,6 @@ use Psr\EventDispatcher\EventDispatcherInterface;
 use TYPO3\CMS\Core\Core\Event\BootCompletedEvent;
 use TYPO3\CMS\Core\DependencyInjection\ContainerBuilder;
 use TYPO3\CMS\Core\Package\PackageManager;
-use TYPO3\CMS\Core\Page\PageRenderer;
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 
@@ -126,8 +125,6 @@ class BootService
         $beUserBackup = $GLOBALS['BE_USER'] ?? null;
 
         $container->get('boot.state')->complete = false;
-        $assetsCache = $container->get('cache.assets');
-        PageRenderer::setCache($assetsCache);
         $eventDispatcher = $container->get(EventDispatcherInterface::class);
         ExtensionManagementUtility::setEventDispatcher($eventDispatcher);
         Bootstrap::loadTypo3LoadedExtAndExtLocalconf($allowCaching, $container->get('cache.core'));
diff --git a/typo3/sysext/core/Classes/Core/Bootstrap.php b/typo3/sysext/core/Classes/Core/Bootstrap.php
index d7fb7c9548da..490457b8d0df 100644
--- a/typo3/sysext/core/Classes/Core/Bootstrap.php
+++ b/typo3/sysext/core/Classes/Core/Bootstrap.php
@@ -44,7 +44,6 @@ use TYPO3\CMS\Core\Package\Cache\PackageCacheInterface;
 use TYPO3\CMS\Core\Package\Cache\PackageStatesPackageCache;
 use TYPO3\CMS\Core\Package\FailsafePackageManager;
 use TYPO3\CMS\Core\Package\PackageManager;
-use TYPO3\CMS\Core\Page\PageRenderer;
 use TYPO3\CMS\Core\Service\DependencyOrderingService;
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -155,7 +154,6 @@ class Bootstrap
         }
 
         $eventDispatcher = $container->get(EventDispatcherInterface::class);
-        PageRenderer::setCache($assetsCache);
         ExtensionManagementUtility::setEventDispatcher($eventDispatcher);
         static::loadTypo3LoadedExtAndExtLocalconf(true, $coreCache);
         static::unsetReservedGlobalVariables();
diff --git a/typo3/sysext/core/Classes/Page/PageRenderer.php b/typo3/sysext/core/Classes/Page/PageRenderer.php
index 7d9321eb5648..cd7ed9ac9a30 100644
--- a/typo3/sysext/core/Classes/Page/PageRenderer.php
+++ b/typo3/sysext/core/Classes/Page/PageRenderer.php
@@ -18,7 +18,6 @@ namespace TYPO3\CMS\Core\Page;
 use Psr\Http\Message\ServerRequestInterface;
 use TYPO3\CMS\Backend\Routing\Router;
 use TYPO3\CMS\Backend\Routing\UriBuilder;
-use TYPO3\CMS\Core\Cache\CacheManager;
 use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
 use TYPO3\CMS\Core\Core\Environment;
 use TYPO3\CMS\Core\Http\ApplicationType;
@@ -80,11 +79,6 @@ class PageRenderer implements SingletonInterface
      */
     protected $moveJsFromHeaderToFooter = false;
 
-    /**
-     * @var Locales
-     */
-    protected $locales;
-
     /**
      * The language key
      * Two character string or 'default'
@@ -101,11 +95,6 @@ class PageRenderer implements SingletonInterface
      */
     protected $languageDependencies = [];
 
-    /**
-     * @var ResourceCompressor
-     */
-    protected $compressor;
-
     // Arrays containing associative array for the included files
     /**
      * @var array<string, array>
@@ -331,30 +320,21 @@ class PageRenderer implements SingletonInterface
      */
     protected $endingSlash = '';
 
-    /**
-     * @var MetaTagManagerRegistry
-     */
-    protected $metaTagRegistry;
-
     protected JavaScriptRenderer $javaScriptRenderer;
 
-    /**
-     * @var FrontendInterface
-     */
-    protected static $cache;
-
-    /**
-     * @param string $templateFile Declare the used template file. Omit this parameter will use default template
-     */
-    public function __construct($templateFile = '')
-    {
+    public function __construct(
+        protected readonly FrontendInterface $assetsCache,
+        protected readonly Locales $locales,
+        protected readonly MarkerBasedTemplateService $templateService,
+        protected readonly MetaTagManagerRegistry $metaTagRegistry,
+        protected readonly PackageManager $packageManager,
+        protected readonly AssetRenderer $assetRenderer,
+        protected readonly ResourceCompressor $resourceCompressor,
+        protected readonly RelativeCssPathFixer $relativeCssPathFixer,
+        protected readonly LocalizationFactory $localizationFactory,
+    ) {
         $this->reset();
-        $this->locales = GeneralUtility::makeInstance(Locales::class);
-        if ($templateFile !== '') {
-            $this->templateFile = $templateFile;
-        }
 
-        $this->metaTagRegistry = GeneralUtility::makeInstance(MetaTagManagerRegistry::class);
         $this->setMetaTag('name', 'generator', 'TYPO3 CMS');
     }
 
@@ -366,7 +346,16 @@ class PageRenderer implements SingletonInterface
     {
         foreach ($state as $var => $value) {
             switch ($var) {
+            case 'assetsCache':
             case 'locales':
+            case 'packageManager':
+            case 'assetRenderer':
+            case 'templateService':
+            case 'resourceCompressor':
+            case 'relativeCssPathFixer':
+            case 'localizationFactory':
+            case 'responseFactory':
+            case 'streamFactory':
                 break;
             case 'metaTagRegistry':
                 $this->metaTagRegistry->updateState($value);
@@ -390,7 +379,16 @@ class PageRenderer implements SingletonInterface
         $state = [];
         foreach (get_object_vars($this) as $var => $value) {
             switch ($var) {
+            case 'assetsCache':
             case 'locales':
+            case 'packageManager':
+            case 'assetRenderer':
+            case 'templateService':
+            case 'resourceCompressor':
+            case 'relativeCssPathFixer':
+            case 'localizationFactory':
+            case 'responseFactory':
+            case 'streamFactory':
                 break;
             case 'metaTagRegistry':
                 $state[$var] = $this->metaTagRegistry->getState();
@@ -406,14 +404,6 @@ class PageRenderer implements SingletonInterface
         return $state;
     }
 
-    /**
-     * @param FrontendInterface $cache
-     */
-    public static function setCache(FrontendInterface $cache)
-    {
-        static::$cache = $cache;
-    }
-
     public function getJavaScriptRenderer(): JavaScriptRenderer
     {
         return $this->javaScriptRenderer;
@@ -1356,21 +1346,18 @@ class PageRenderer implements SingletonInterface
             return;
         }
 
-        $packageManager = GeneralUtility::makeInstance(PackageManager::class);
-        $packages = $packageManager->getActivePackages();
+        $packages = $this->packageManager->getActivePackages();
         $isDevelopment = Environment::getContext()->isDevelopment();
-        $cacheIdentifier = (new PackageDependentCacheIdentifier($packageManager))
+        $cacheIdentifier = (new PackageDependentCacheIdentifier($this->packageManager))
               ->withPrefix('RequireJS')
               ->withAdditionalHashedIdentifier(($isDevelopment ? ':dev' : '') . GeneralUtility::getIndpEnv('TYPO3_REQUEST_SCRIPT'))
               ->toString();
-        /** @var FrontendInterface $cache */
-        $cache = static::$cache ?? GeneralUtility::makeInstance(CacheManager::class)->getCache('assets');
-        $requireJsConfig = $cache->get($cacheIdentifier);
+        $requireJsConfig = $this->assetsCache->get($cacheIdentifier);
 
         // if we did not get a configuration from the cache, compute and store it in the cache
         if (!isset($requireJsConfig['internal']) || !isset($requireJsConfig['public'])) {
             $requireJsConfig = $this->computeRequireJsConfig($isDevelopment, $packages);
-            $cache->set($cacheIdentifier, $requireJsConfig);
+            $this->assetsCache->set($cacheIdentifier, $requireJsConfig);
         }
 
         $this->requireJsConfig = array_merge_recursive($this->additionalRequireJsConfig, $requireJsConfig['internal']);
@@ -1762,8 +1749,7 @@ class PageRenderer implements SingletonInterface
 
         // The page renderer needs a full reset when the page was rendered
         $this->reset();
-        $templateService = GeneralUtility::makeInstance(MarkerBasedTemplateService::class);
-        return trim($templateService->substituteMarkerArray($template, $markerArray, '###|###'));
+        return trim($this->templateService->substituteMarkerArray($template, $markerArray, '###|###'));
     }
 
     /**
@@ -1797,8 +1783,7 @@ class PageRenderer implements SingletonInterface
         $this->prepareRendering();
         $markerArray = $this->getPreparedMarkerArrayForPageWithUncachedObjects($substituteHash);
         $template = $this->getTemplate();
-        $templateService = GeneralUtility::makeInstance(MarkerBasedTemplateService::class);
-        return trim($templateService->substituteMarkerArray($template, $markerArray, '###|###'));
+        return trim($this->templateService->substituteMarkerArray($template, $markerArray, '###|###'));
     }
 
     /**
@@ -1888,16 +1873,15 @@ class PageRenderer implements SingletonInterface
             $jsInline = '';
         }
         // Use AssetRenderer to inject all JavaScripts and CSS files
-        $assetRenderer = GeneralUtility::makeInstance(AssetRenderer::class);
-        $jsInline .= $assetRenderer->renderInlineJavaScript(true);
-        $jsFooterInline .= $assetRenderer->renderInlineJavaScript();
-        $jsFiles .= $assetRenderer->renderJavaScript(true);
-        $jsFooterFiles .= $assetRenderer->renderJavaScript();
-        $cssInline .= $assetRenderer->renderInlineStyleSheets(true);
+        $jsInline .= $this->assetRenderer->renderInlineJavaScript(true);
+        $jsFooterInline .= $this->assetRenderer->renderInlineJavaScript();
+        $jsFiles .= $this->assetRenderer->renderJavaScript(true);
+        $jsFooterFiles .= $this->assetRenderer->renderJavaScript();
+        $cssInline .= $this->assetRenderer->renderInlineStyleSheets(true);
         // append inline CSS to footer (as there is no cssFooterInline)
-        $jsFooterFiles .= $assetRenderer->renderInlineStyleSheets();
-        $cssLibs .= $assetRenderer->renderStyleSheets(true, $this->endingSlash);
-        $cssFiles .= $assetRenderer->renderStyleSheets(false, $this->endingSlash);
+        $jsFooterFiles .= $this->assetRenderer->renderInlineStyleSheets();
+        $cssLibs .= $this->assetRenderer->renderStyleSheets(true, $this->endingSlash);
+        $cssFiles .= $this->assetRenderer->renderStyleSheets(false, $this->endingSlash);
 
         $this->executePostRenderHook($jsLibs, $jsFiles, $jsFooterFiles, $cssLibs, $cssFiles, $jsInline, $cssInline, $jsFooterInline, $jsFooterLibs);
         return [$jsLibs, $jsFiles, $jsFooterFiles, $cssLibs, $cssFiles, $jsInline, $cssInline, $jsFooterInline, $jsFooterLibs];
@@ -2406,9 +2390,6 @@ class PageRenderer implements SingletonInterface
      */
     protected function readLLfile($fileRef)
     {
-        /** @var LocalizationFactory $languageFactory */
-        $languageFactory = GeneralUtility::makeInstance(LocalizationFactory::class);
-
         if ($this->lang !== 'default') {
             $languages = array_reverse($this->languageDependencies);
             // At least we need to have English
@@ -2421,7 +2402,7 @@ class PageRenderer implements SingletonInterface
 
         $localLanguage = [];
         foreach ($languages as $language) {
-            $tempLL = $languageFactory->getParsedData($fileRef, $language);
+            $tempLL = $this->localizationFactory->getParsedData($fileRef, $language);
 
             $localLanguage['default'] = $tempLL['default'];
             if (!isset($localLanguage[$this->lang])) {
@@ -2469,9 +2450,9 @@ class PageRenderer implements SingletonInterface
                 ];
                 GeneralUtility::callUserFunction($GLOBALS['TYPO3_CONF_VARS'][$this->getApplicationType()]['jsConcatenateHandler'], $params, $this);
             } else {
-                $this->jsLibs = $this->getCompressor()->concatenateJsFiles($this->jsLibs);
-                $this->jsFiles = $this->getCompressor()->concatenateJsFiles($this->jsFiles);
-                $this->jsFooterFiles = $this->getCompressor()->concatenateJsFiles($this->jsFooterFiles);
+                $this->jsLibs = $this->resourceCompressor->concatenateJsFiles($this->jsLibs);
+                $this->jsFiles = $this->resourceCompressor->concatenateJsFiles($this->jsFiles);
+                $this->jsFooterFiles = $this->resourceCompressor->concatenateJsFiles($this->jsFooterFiles);
             }
         }
     }
@@ -2492,8 +2473,8 @@ class PageRenderer implements SingletonInterface
                 ];
                 GeneralUtility::callUserFunction($GLOBALS['TYPO3_CONF_VARS'][$this->getApplicationType()]['cssConcatenateHandler'], $params, $this);
             } else {
-                $this->cssLibs = $this->getCompressor()->concatenateCssFiles($this->cssLibs);
-                $this->cssFiles = $this->getCompressor()->concatenateCssFiles($this->cssFiles);
+                $this->cssLibs = $this->resourceCompressor->concatenateCssFiles($this->cssLibs);
+                $this->cssFiles = $this->resourceCompressor->concatenateCssFiles($this->cssFiles);
             }
         }
     }
@@ -2524,8 +2505,8 @@ class PageRenderer implements SingletonInterface
                 ];
                 GeneralUtility::callUserFunction($GLOBALS['TYPO3_CONF_VARS'][$this->getApplicationType()]['cssCompressHandler'], $params, $this);
             } else {
-                $this->cssLibs = $this->getCompressor()->compressCssFiles($this->cssLibs);
-                $this->cssFiles = $this->getCompressor()->compressCssFiles($this->cssFiles);
+                $this->cssLibs = $this->resourceCompressor->compressCssFiles($this->cssLibs);
+                $this->cssFiles = $this->resourceCompressor->compressCssFiles($this->cssFiles);
             }
         }
     }
@@ -2552,29 +2533,16 @@ class PageRenderer implements SingletonInterface
                 // Traverse the arrays, compress files
                 foreach ($this->jsInline ?? [] as $name => $properties) {
                     if ($properties['compress'] ?? false) {
-                        $this->jsInline[$name]['code'] = $this->getCompressor()->compressJavaScriptSource($properties['code']);
+                        $this->jsInline[$name]['code'] = $this->resourceCompressor->compressJavaScriptSource($properties['code']);
                     }
                 }
-                $this->jsLibs = $this->getCompressor()->compressJsFiles($this->jsLibs);
-                $this->jsFiles = $this->getCompressor()->compressJsFiles($this->jsFiles);
-                $this->jsFooterFiles = $this->getCompressor()->compressJsFiles($this->jsFooterFiles);
+                $this->jsLibs = $this->resourceCompressor->compressJsFiles($this->jsLibs);
+                $this->jsFiles = $this->resourceCompressor->compressJsFiles($this->jsFiles);
+                $this->jsFooterFiles = $this->resourceCompressor->compressJsFiles($this->jsFooterFiles);
             }
         }
     }
 
-    /**
-     * Returns instance of \TYPO3\CMS\Core\Resource\ResourceCompressor
-     *
-     * @return ResourceCompressor
-     */
-    protected function getCompressor()
-    {
-        if ($this->compressor === null) {
-            $this->compressor = GeneralUtility::makeInstance(ResourceCompressor::class);
-        }
-        return $this->compressor;
-    }
-
     /**
      * Processes a Javascript file dependent on the current context
      *
@@ -2587,7 +2555,7 @@ class PageRenderer implements SingletonInterface
     {
         $filename = $this->getStreamlinedFileName($filename, false);
         if ($this->compressJavascript) {
-            $filename = $this->getCompressor()->compressJsFile($filename);
+            $filename = $this->resourceCompressor->compressJsFile($filename);
         } elseif ($this->getApplicationType() === 'FE') {
             $filename = GeneralUtility::createVersionNumberedFilename($filename);
         }
@@ -2764,7 +2732,7 @@ class PageRenderer implements SingletonInterface
         if ($cssInline === false) {
             return '';
         }
-        $cssInlineFix = $this->getPathFixer()->fixRelativeUrlPaths($cssInline, '/' . PathUtility::dirname($file) . '/');
+        $cssInlineFix = $this->relativeCssPathFixer->fixRelativeUrlPaths($cssInline, '/' . PathUtility::dirname($file) . '/');
         return '<style'
             . ' media="' . htmlspecialchars($properties['media']) . '"'
             . ($properties['title'] ? ' title="' . htmlspecialchars($properties['title']) . '"' : '')
@@ -2774,11 +2742,6 @@ class PageRenderer implements SingletonInterface
             . '-->' . LF . '/*]]>*/' . LF . '</style>' . LF;
     }
 
-    protected function getPathFixer(): RelativeCssPathFixer
-    {
-        return GeneralUtility::makeInstance(RelativeCssPathFixer::class);
-    }
-
     /**
      * String 'FE' if in FrontendApplication, 'BE' otherwise (also in CLI without request object)
      *
diff --git a/typo3/sysext/core/Classes/Resource/ResourceCompressor.php b/typo3/sysext/core/Classes/Resource/ResourceCompressor.php
index b1f358a8c6c6..6fbf5badb219 100644
--- a/typo3/sysext/core/Classes/Resource/ResourceCompressor.php
+++ b/typo3/sysext/core/Classes/Resource/ResourceCompressor.php
@@ -63,11 +63,13 @@ class ResourceCompressor
 	FileETag MTime Size
 </FilesMatch>';
 
-    /**
-     * Constructor
-     */
-    public function __construct()
+    protected bool $initialized = false;
+
+    protected function initialize(): void
     {
+        if ($this->initialized) {
+            return;
+        }
         // we check for existence of our targetDirectory
         if (!is_dir(Environment::getPublicPath() . '/' . $this->targetDirectory)) {
             GeneralUtility::mkdir_deep(Environment::getPublicPath() . '/' . $this->targetDirectory);
@@ -96,6 +98,8 @@ class ResourceCompressor
             }
         }
         $this->setRootPath($applicationType === 'BE' ? Environment::getBackendPath() . '/' : Environment::getPublicPath() . '/');
+
+        $this->initialized = true;
     }
 
     /**
@@ -118,6 +122,7 @@ class ResourceCompressor
      */
     public function concatenateCssFiles(array $cssFiles)
     {
+        $this->initialize();
         $filesToIncludeByType = ['all' => []];
         foreach ($cssFiles as $key => $fileOptions) {
             // no concatenation allowed for this file, so continue
@@ -165,6 +170,7 @@ class ResourceCompressor
      */
     public function concatenateJsFiles(array $jsFiles)
     {
+        $this->initialize();
         $concatenatedJsFileIsAsync = false;
         $allFilesToConcatenateAreAsync = true;
         $filesToInclude = [];
@@ -317,6 +323,7 @@ class ResourceCompressor
      */
     public function compressCssFiles(array $cssFiles)
     {
+        $this->initialize();
         $filesAfterCompression = [];
         foreach ($cssFiles as $key => $fileOptions) {
             // if compression is enabled
@@ -346,6 +353,7 @@ class ResourceCompressor
      */
     public function compressCssFile($filename)
     {
+        $this->initialize();
         // generate the unique name of the file
         $filenameAbsolute = GeneralUtility::resolveBackPath($this->rootPath . $this->getFilenameFromMainDir($filename));
         if (@file_exists($filenameAbsolute)) {
@@ -378,6 +386,7 @@ class ResourceCompressor
      */
     public function compressJsFiles(array $jsFiles)
     {
+        $this->initialize();
         $filesAfterCompression = [];
         foreach ($jsFiles as $fileName => $fileOptions) {
             // If compression is enabled
@@ -401,6 +410,7 @@ class ResourceCompressor
      */
     public function compressJsFile($filename)
     {
+        $this->initialize();
         // generate the unique name of the file
         $filenameAbsolute = GeneralUtility::resolveBackPath($this->rootPath . $this->getFilenameFromMainDir($filename));
         if (@file_exists($filenameAbsolute)) {
@@ -619,6 +629,7 @@ class ResourceCompressor
 
     public function compressJavaScriptSource(string $javaScriptSourceCode): string
     {
+        $this->initialize();
         $fakeThis = null;
         foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_div.php']['minifyJavaScript'] ?? [] as $hookMethod) {
             try {
diff --git a/typo3/sysext/core/Classes/Service/MarkerBasedTemplateService.php b/typo3/sysext/core/Classes/Service/MarkerBasedTemplateService.php
index a4561738833e..34f38548efad 100644
--- a/typo3/sysext/core/Classes/Service/MarkerBasedTemplateService.php
+++ b/typo3/sysext/core/Classes/Service/MarkerBasedTemplateService.php
@@ -15,7 +15,7 @@
 
 namespace TYPO3\CMS\Core\Service;
 
-use TYPO3\CMS\Core\Cache\CacheManager;
+use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
 
@@ -25,6 +25,12 @@ use TYPO3\CMS\Core\Utility\MathUtility;
  */
 class MarkerBasedTemplateService
 {
+    public function __construct(
+        protected readonly FrontendInterface $hashCache,
+        protected readonly FrontendInterface $runtimeCache,
+    ) {
+    }
+
     /**
      * Returns the first subpart encapsulated in the marker, $marker
      * (possibly present in $content as a HTML comment)
@@ -355,7 +361,6 @@ class MarkerBasedTemplateService
      */
     public function substituteMarkerArrayCached($content, array $markContentArray = null, array $subpartContentArray = null, array $wrappedSubpartContentArray = null)
     {
-        $runtimeCache = $this->getRuntimeCache();
         // If not arrays then set them
         if ($markContentArray === null) {
             // Plain markers
@@ -378,16 +383,15 @@ class MarkerBasedTemplateService
         }
         asort($keysToReplace);
         $storeKey = md5('substituteMarkerArrayCached_storeKey:' . serialize([$content, $keysToReplace]));
-        $fromCache = $runtimeCache->get($storeKey);
+        $fromCache = $this->runtimeCache->get($storeKey);
         if ($fromCache) {
             $storeArr = $fromCache;
         } else {
-            $cache = $this->getCache();
-            $storeArrDat = $cache->get($storeKey);
+            $storeArrDat = $this->hashCache->get($storeKey);
             if (is_array($storeArrDat)) {
                 $storeArr = $storeArrDat;
                 // Setting the data in the first level cache
-                $runtimeCache->set($storeKey, $storeArr);
+                $this->runtimeCache->set($storeKey, $storeArr);
             } else {
                 // Finding subparts and substituting them with the subpart as a marker
                 foreach ($sPkeys as $sPK) {
@@ -419,9 +423,9 @@ class MarkerBasedTemplateService
                     $storeArr['c'] = preg_split($regex, $content); // contains all content parts around markers
                     $storeArr['k'] = $wrappedKeys; // contains all markers incl. ###
                     // Setting the data inside the second-level cache
-                    $runtimeCache->set($storeKey, $storeArr);
+                    $this->runtimeCache->set($storeKey, $storeArr);
                     // Storing the cached data permanently
-                    $cache->set($storeKey, $storeArr, ['substMarkArrayCached'], 0);
+                    $this->hashCache->set($storeKey, $storeArr, ['substMarkArrayCached'], 0);
                 }
             }
         }
@@ -507,24 +511,4 @@ class MarkerBasedTemplateService
         }
         return $markContentArray;
     }
-
-    /**
-     * Second-level cache
-     *
-     * @return \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
-     */
-    protected function getCache()
-    {
-        return GeneralUtility::makeInstance(CacheManager::class)->getCache('hash');
-    }
-
-    /**
-     * First-level cache (runtime cache)
-     *
-     * @return \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
-     */
-    protected function getRuntimeCache()
-    {
-        return GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
-    }
 }
diff --git a/typo3/sysext/core/Configuration/Services.yaml b/typo3/sysext/core/Configuration/Services.yaml
index 0ef00aa8d515..920e463c30aa 100644
--- a/typo3/sysext/core/Configuration/Services.yaml
+++ b/typo3/sysext/core/Configuration/Services.yaml
@@ -210,6 +210,10 @@ services:
         method: 'warmupCaches'
         event: TYPO3\CMS\Core\Cache\Event\CacheWarmupEvent
 
+  TYPO3\CMS\Core\Page\PageRenderer:
+    arguments:
+      $assetsCache: '@cache.assets'
+
   TYPO3\CMS\Core\Page\AssetRenderer:
     public: true
     arguments:
@@ -281,6 +285,12 @@ services:
   TYPO3\CMS\Core\Domain\Access\RecordAccessVoter:
     public: true
 
+  TYPO3\CMS\Core\Service\MarkerBasedTemplateService:
+    public: true
+    arguments:
+      $hashCache: '@cache.assets'
+      $runtimeCache: '@cache.runtime'
+
   # Core caches, cache.core and cache.assets are injected as early
   # entries in TYPO3\CMS\Core\Core\Bootstrap and therefore omitted here
   cache.hash:
diff --git a/typo3/sysext/core/Tests/Functional/Page/PageRendererTest.php b/typo3/sysext/core/Tests/Functional/Page/PageRendererTest.php
index f7f0ad0f7ee6..8799e8879b6e 100644
--- a/typo3/sysext/core/Tests/Functional/Page/PageRendererTest.php
+++ b/typo3/sysext/core/Tests/Functional/Page/PageRendererTest.php
@@ -25,7 +25,15 @@ use TYPO3\CMS\Core\Authentication\IpLocker;
 use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder;
 use TYPO3\CMS\Core\Http\ServerRequest;
 use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
+use TYPO3\CMS\Core\Localization\Locales;
+use TYPO3\CMS\Core\Localization\LocalizationFactory;
+use TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry;
+use TYPO3\CMS\Core\Package\PackageManager;
+use TYPO3\CMS\Core\Page\AssetRenderer;
 use TYPO3\CMS\Core\Page\PageRenderer;
+use TYPO3\CMS\Core\Resource\RelativeCssPathFixer;
+use TYPO3\CMS\Core\Resource\ResourceCompressor;
+use TYPO3\CMS\Core\Service\MarkerBasedTemplateService;
 use TYPO3\CMS\Core\Session\Backend\SessionBackendInterface;
 use TYPO3\CMS\Core\Session\UserSessionManager;
 use TYPO3\CMS\Core\Utility\StringUtility;
@@ -40,6 +48,22 @@ class PageRendererTest extends FunctionalTestCase
      */
     protected bool $initializeDatabase = false;
 
+    protected function createPageRenderer(): PageRenderer
+    {
+        $container = $this->getContainer();
+        return new PageRenderer(
+            $container->get('cache.assets'),
+            $container->get(Locales::class),
+            $container->get(MarkerBasedTemplateService::class),
+            $container->get(MetaTagManagerRegistry::class),
+            $container->get(PackageManager::class),
+            $container->get(AssetRenderer::class),
+            new ResourceCompressor(),
+            new RelativeCssPathFixer(),
+            $container->get(LocalizationFactory::class),
+        );
+    }
+
     /**
      * @test
      */
@@ -47,7 +71,7 @@ class PageRendererTest extends FunctionalTestCase
     {
         $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest('https://www.example.com/'))
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE);
-        $subject = new PageRenderer();
+        $subject = $this->createPageRenderer();
         $subject->setCharSet('utf-8');
         $subject->setLanguage('default');
 
@@ -152,7 +176,7 @@ class PageRendererTest extends FunctionalTestCase
         self::assertStringContainsString('<meta property="og:image" content="/path/to/image1.jpg" />', $renderedString);
         self::assertStringContainsString('<meta property="og:image" content="/path/to/image2.jpg" />', $renderedString);
 
-        $stateBasedSubject = new PageRenderer();
+        $stateBasedSubject = $this->createPageRenderer();
         $stateBasedSubject->updateState(unserialize($state, ['allowed_classes' => false]));
         $stateBasedRenderedString = $stateBasedSubject->render();
         self::assertStringContainsString($expectedPrologueString, $stateBasedRenderedString);
@@ -199,7 +223,7 @@ class PageRendererTest extends FunctionalTestCase
     {
         $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest('https://www.example.com/'))
             ->withAttribute('applicationType', $requestType);
-        $subject = new PageRenderer();
+        $subject = $this->createPageRenderer();
         $subject->setCharSet('utf-8');
         $subject->setLanguage('default');
 
@@ -286,7 +310,7 @@ class PageRendererTest extends FunctionalTestCase
     {
         $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest('https://www.example.com/'))
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE);
-        $subject = new PageRenderer();
+        $subject = $this->createPageRenderer();
         $subject->setCharSet('utf-8');
         $subject->setLanguage('default');
 
@@ -386,7 +410,7 @@ class PageRendererTest extends FunctionalTestCase
         $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE);
 
-        $subject = new PageRenderer();
+        $subject = $this->createPageRenderer();
         $subject->setCharSet('utf-8');
         $subject->setLanguage('default');
 
diff --git a/typo3/sysext/core/Tests/Unit/Page/PageRendererFactoryTrait.php b/typo3/sysext/core/Tests/Unit/Page/PageRendererFactoryTrait.php
new file mode 100644
index 000000000000..03f9ef9cf1d2
--- /dev/null
+++ b/typo3/sysext/core/Tests/Unit/Page/PageRendererFactoryTrait.php
@@ -0,0 +1,79 @@
+<?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\Tests\Unit\Page;
+
+use Prophecy\PhpUnit\ProphecyTrait;
+use Psr\Container\ContainerInterface;
+use Psr\EventDispatcher\EventDispatcherInterface;
+use TYPO3\CMS\Core\Cache\CacheManager;
+use TYPO3\CMS\Core\Cache\Frontend\NullFrontend;
+use TYPO3\CMS\Core\EventDispatcher\EventDispatcher;
+use TYPO3\CMS\Core\EventDispatcher\ListenerProvider;
+use TYPO3\CMS\Core\Localization\LanguageStore;
+use TYPO3\CMS\Core\Localization\Locales;
+use TYPO3\CMS\Core\Localization\LocalizationFactory;
+use TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry;
+use TYPO3\CMS\Core\Package\PackageManager;
+use TYPO3\CMS\Core\Page\AssetRenderer;
+use TYPO3\CMS\Core\Resource\RelativeCssPathFixer;
+use TYPO3\CMS\Core\Resource\ResourceCompressor;
+use TYPO3\CMS\Core\Service\DependencyOrderingService;
+use TYPO3\CMS\Core\Service\MarkerBasedTemplateService;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+trait PageRendererFactoryTrait
+{
+    use ProphecyTrait;
+
+    protected function getPageRendererConstructorArgs(
+        PackageManager $packageManager = null,
+        CacheManager $cacheManager = null,
+    ): array {
+        $packageManager ??= new PackageManager(new DependencyOrderingService());
+        $cacheManagerProphecy = $this->prophesize(CacheManager::class);
+        $cacheManager ??= $cacheManagerProphecy->reveal();
+
+        /**
+         * prepare an EventDispatcher for ::makeInstance(AssetRenderer)
+         * @see \TYPO3\CMS\Core\Page\PageRenderer::renderJavaScriptAndCss
+         */
+        GeneralUtility::setSingletonInstance(
+            EventDispatcherInterface::class,
+            new EventDispatcher(
+                new ListenerProvider($this->createMock(ContainerInterface::class))
+            )
+        );
+
+        $assetRenderer = new AssetRenderer();
+
+        return [
+            new NullFrontend('assets'),
+            new Locales(),
+            new MarkerBasedTemplateService(
+                new NullFrontend('hash'),
+                new NullFrontend('runtime'),
+            ),
+            new MetaTagManagerRegistry(),
+            $packageManager,
+            $assetRenderer,
+            new ResourceCompressor(),
+            new RelativeCssPathFixer(),
+            new LocalizationFactory(new LanguageStore($packageManager), $cacheManager),
+        ];
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Page/PageRendererTest.php b/typo3/sysext/core/Tests/Unit/Page/PageRendererTest.php
index 8a6e8ab629e0..ba2c5c520a11 100644
--- a/typo3/sysext/core/Tests/Unit/Page/PageRendererTest.php
+++ b/typo3/sysext/core/Tests/Unit/Page/PageRendererTest.php
@@ -33,6 +33,7 @@ use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 class PageRendererTest extends UnitTestCase
 {
     use ProphecyTrait;
+    use PageRendererFactoryTrait;
 
     /**
      * @var bool Reset singletons created by subject
@@ -56,6 +57,7 @@ class PageRendererTest extends UnitTestCase
     {
         /** @var PageRenderer|AccessibleObjectInterface $pageRenderer */
         $pageRenderer = $this->getMockBuilder(PageRenderer::class)
+            ->setConstructorArgs($this->getPageRendererConstructorArgs())
             ->onlyMethods(['reset', 'prepareRendering', 'renderJavaScriptAndCss', 'getPreparedMarkerArray', 'getTemplate'])
             ->getMock();
 
@@ -265,8 +267,11 @@ class PageRendererTest extends UnitTestCase
      */
     public function getAddedMetaTag(): void
     {
-        /** @var PageRenderer|\PHPUnit\Framework\MockObject\MockObject|\TYPO3\TestingFramework\Core\AccessibleObjectInterface $subject */
-        $subject = $this->getAccessibleMock(PageRenderer::class, ['whatDoesThisDo']);
+        /** @var PageRenderer|\PHPUnit\Framework\MockObject\MockObject */
+        $subject = $this->getMockBuilder(PageRenderer::class)
+            ->setConstructorArgs($this->getPageRendererConstructorArgs())
+            ->setMethodsExcept(['setMetaTag', 'getMetaTag'])
+            ->getMock();
         $subject->setMetaTag('nAme', 'Author', 'foobar');
         $actualResult = $subject->getMetaTag('naMe', 'AUTHOR');
         $expectedResult = [
@@ -282,8 +287,11 @@ class PageRendererTest extends UnitTestCase
      */
     public function overrideMetaTag(): void
     {
-        /** @var PageRenderer|\PHPUnit\Framework\MockObject\MockObject|\TYPO3\TestingFramework\Core\AccessibleObjectInterface $subject */
-        $subject = $this->getAccessibleMock(PageRenderer::class, ['whatDoesThisDo']);
+        /** @var PageRenderer|\PHPUnit\Framework\MockObject\MockObject */
+        $subject = $this->getMockBuilder(PageRenderer::class)
+            ->setConstructorArgs($this->getPageRendererConstructorArgs())
+            ->setMethodsExcept(['setMetaTag', 'getMetaTag'])
+            ->getMock();
         $subject->setMetaTag('nAme', 'Author', 'Axel Foley');
         $subject->setMetaTag('nAme', 'Author', 'foobar');
         $actualResult = $subject->getMetaTag('naMe', 'AUTHOR');
@@ -300,8 +308,11 @@ class PageRendererTest extends UnitTestCase
      */
     public function unsetAddedMetaTag(): void
     {
-        /** @var PageRenderer|\PHPUnit\Framework\MockObject\MockObject|\TYPO3\TestingFramework\Core\AccessibleObjectInterface $subject */
-        $subject = $this->getAccessibleMock(PageRenderer::class, ['whatDoesThisDo']);
+        /** @var PageRenderer|\PHPUnit\Framework\MockObject\MockObject */
+        $subject = $this->getMockBuilder(PageRenderer::class)
+            ->setConstructorArgs($this->getPageRendererConstructorArgs())
+            ->setMethodsExcept(['setMetaTag', 'removeMetaTag', 'getMetaTag'])
+            ->getMock();
         $subject->setMetaTag('nAme', 'Author', 'foobar');
         $subject->removeMetaTag('naMe', 'AUTHOR');
         $actualResult = $subject->getMetaTag('naMe', 'AUTHOR');
diff --git a/typo3/sysext/core/Tests/Unit/Resource/ResourceCompressorIntegrationTest.php b/typo3/sysext/core/Tests/Unit/Resource/ResourceCompressorIntegrationTest.php
index 012c2bb9f092..a797ced922d7 100644
--- a/typo3/sysext/core/Tests/Unit/Resource/ResourceCompressorIntegrationTest.php
+++ b/typo3/sysext/core/Tests/Unit/Resource/ResourceCompressorIntegrationTest.php
@@ -51,9 +51,10 @@ class ResourceCompressorIntegrationTest extends BaseTestCase
     /**
      * @test
      */
-    public function constructorCreatesTargetDirectory(): void
+    public function initializeCreatesTargetDirectory(): void
     {
-        $this->resourceCompressor = new TestableResourceCompressor();
+        $this->resourceCompressor = $this->getAccessibleMock(TestableResourceCompressor::class, null);
+        $this->resourceCompressor->_call('initialize');
         $dir = Environment::getPublicPath() . '/' . $this->resourceCompressor->getTargetDirectory();
         self::assertFileExists($dir);
     }
@@ -61,10 +62,11 @@ class ResourceCompressorIntegrationTest extends BaseTestCase
     /**
      * @test
      */
-    public function constructorCreatesHtaccessFileIfSet(): void
+    public function initializeCreatesHtaccessFileIfSet(): void
     {
         $GLOBALS['TYPO3_CONF_VARS']['SYS']['generateApacheHtaccess'] = true;
-        $this->resourceCompressor = new TestableResourceCompressor();
+        $this->resourceCompressor = $this->getAccessibleMock(TestableResourceCompressor::class, null);
+        $this->resourceCompressor->_call('initialize');
         $htaccessPath = Environment::getPublicPath() . '/' . $this->resourceCompressor->getTargetDirectory() . '.htaccess';
         self::assertStringEqualsFile($htaccessPath, $this->resourceCompressor->getHtaccessTemplate());
     }
@@ -72,10 +74,11 @@ class ResourceCompressorIntegrationTest extends BaseTestCase
     /**
      * @test
      */
-    public function constructorDoesNotCreateHtaccessFileIfSetToFalse(): void
+    public function initializeDoesNotCreateHtaccessFileIfSetToFalse(): void
     {
         $GLOBALS['TYPO3_CONF_VARS']['SYS']['generateApacheHtaccess'] = false;
-        $this->resourceCompressor = new TestableResourceCompressor();
+        $this->resourceCompressor = $this->getAccessibleMock(TestableResourceCompressor::class, null);
+        $this->resourceCompressor->_call('initialize');
         $htaccessPath = Environment::getPublicPath() . '/' . $this->resourceCompressor->getTargetDirectory() . '.htaccess';
         // @todo remove condition and else branch as soon as phpunit v8 goes out of support
         if (method_exists($this, 'assertFileDoesNotExist')) {
diff --git a/typo3/sysext/core/Tests/Unit/Service/MarkerBasedTemplateServiceTest.php b/typo3/sysext/core/Tests/Unit/Service/MarkerBasedTemplateServiceTest.php
index 6ad1d7a1dc4b..e217485b6afb 100644
--- a/typo3/sysext/core/Tests/Unit/Service/MarkerBasedTemplateServiceTest.php
+++ b/typo3/sysext/core/Tests/Unit/Service/MarkerBasedTemplateServiceTest.php
@@ -17,12 +17,9 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Core\Tests\Unit\Service;
 
-use Prophecy\Argument;
 use Prophecy\PhpUnit\ProphecyTrait;
-use TYPO3\CMS\Core\Cache\CacheManager;
 use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
 use TYPO3\CMS\Core\Service\MarkerBasedTemplateService;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 
 /**
@@ -45,11 +42,11 @@ class MarkerBasedTemplateServiceTest extends UnitTestCase
     protected function setUp(): void
     {
         parent::setUp();
-        $cacheManagerProphecy = $this->prophesize(CacheManager::class);
-        GeneralUtility::setSingletonInstance(CacheManager::class, $cacheManagerProphecy->reveal());
         $cacheFrontendProphecy = $this->prophesize(FrontendInterface::class);
-        $cacheManagerProphecy->getCache(Argument::cetera())->willReturn($cacheFrontendProphecy->reveal());
-        $this->templateService = new MarkerBasedTemplateService();
+        $this->templateService = new MarkerBasedTemplateService(
+            $cacheFrontendProphecy->reveal(),
+            $cacheFrontendProphecy->reveal(),
+        );
     }
 
     /**
diff --git a/typo3/sysext/frontend/Classes/ContentObject/ImageContentObject.php b/typo3/sysext/frontend/Classes/ContentObject/ImageContentObject.php
index 0b9b2c54e17e..b4ee37ccef2a 100644
--- a/typo3/sysext/frontend/Classes/ContentObject/ImageContentObject.php
+++ b/typo3/sysext/frontend/Classes/ContentObject/ImageContentObject.php
@@ -27,6 +27,11 @@ use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
  */
 class ImageContentObject extends AbstractContentObject
 {
+    public function __construct(
+        protected readonly MarkerBasedTemplateService $markerTemplateService,
+    ) {
+    }
+
     /**
      * Rendering the cObject, IMAGE
      *
@@ -96,8 +101,7 @@ class ImageContentObject extends AbstractContentObject
             'selfClosingTagSlash' => !empty($tsfe->xhtmlDoctype) ? ' /' : '',
         ];
 
-        $markerTemplateEngine = GeneralUtility::makeInstance(MarkerBasedTemplateService::class);
-        $theValue = $markerTemplateEngine->substituteMarkerArray($imageTagTemplate, $imageTagValues, '###|###', true, true);
+        $theValue = $this->markerTemplateService->substituteMarkerArray($imageTagTemplate, $imageTagValues, '###|###', true, true);
 
         $linkWrap = (string)$this->cObj->stdWrapValue('linkWrap', $conf ?? []);
         if ($linkWrap !== '') {
@@ -243,8 +247,7 @@ class ImageContentObject extends AbstractContentObject
                     $sourceConfiguration['src'] = htmlspecialchars($urlPrefix . $sourceInfo[3]);
                     $sourceConfiguration['selfClosingTagSlash'] = !empty($tsfe->xhtmlDoctype) ? ' /' : '';
 
-                    $markerTemplateEngine = GeneralUtility::makeInstance(MarkerBasedTemplateService::class);
-                    $oneSourceCollection = $markerTemplateEngine->substituteMarkerArray($sourceLayout, $sourceConfiguration, '###|###', true, true);
+                    $oneSourceCollection = $this->markerTemplateService->substituteMarkerArray($sourceLayout, $sourceConfiguration, '###|###', true, true);
 
                     foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['getImageSourceCollection'] ?? [] as $className) {
                         $hookObject = GeneralUtility::makeInstance($className);
diff --git a/typo3/sysext/frontend/Tests/Unit/ContentObject/FluidTemplateContentObjectTest.php b/typo3/sysext/frontend/Tests/Unit/ContentObject/FluidTemplateContentObjectTest.php
index 5d2464d9fc57..8af65248ea59 100644
--- a/typo3/sysext/frontend/Tests/Unit/ContentObject/FluidTemplateContentObjectTest.php
+++ b/typo3/sysext/frontend/Tests/Unit/ContentObject/FluidTemplateContentObjectTest.php
@@ -27,6 +27,7 @@ use TYPO3\CMS\Core\Core\Environment;
 use TYPO3\CMS\Core\Page\ImportMap;
 use TYPO3\CMS\Core\Page\ImportMapFactory;
 use TYPO3\CMS\Core\Page\PageRenderer;
+use TYPO3\CMS\Core\Tests\Unit\Page\PageRendererFactoryTrait;
 use TYPO3\CMS\Core\TypoScript\TemplateService;
 use TYPO3\CMS\Core\TypoScript\TypoScriptService;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -49,6 +50,7 @@ use TYPO3Fluid\Fluid\View\AbstractTemplateView;
 class FluidTemplateContentObjectTest extends UnitTestCase
 {
     use ProphecyTrait;
+    use PageRendererFactoryTrait;
 
     /**
      * @var bool Reset singletons created by subject
@@ -135,6 +137,10 @@ class FluidTemplateContentObjectTest extends UnitTestCase
      */
     public function renderCallsInitializeStandaloneViewInstance(): void
     {
+        GeneralUtility::setSingletonInstance(
+            PageRenderer::class,
+            new PageRenderer(...$this->getPageRendererConstructorArgs()),
+        );
         $this->addMockViewToSubject();
         $this->subject->expects(self::once())->method('initializeStandaloneViewInstance');
         $this->subject->render([]);
@@ -158,6 +164,10 @@ class FluidTemplateContentObjectTest extends UnitTestCase
                 ],
             ],
         ];
+        GeneralUtility::setSingletonInstance(
+            PageRenderer::class,
+            new PageRenderer(...$this->getPageRendererConstructorArgs()),
+        );
         $contentObjectRenderer = $this->prophesize(ContentObjectRenderer::class);
 
         $standAloneView = $this->prophesize(StandaloneView::class);
@@ -183,6 +193,10 @@ class FluidTemplateContentObjectTest extends UnitTestCase
     {
         $configuration = ['file' => 'EXT:core/bar.html'];
 
+        GeneralUtility::setSingletonInstance(
+            PageRenderer::class,
+            new PageRenderer(...$this->getPageRendererConstructorArgs()),
+        );
         $contentObjectRenderer = $this->prophesize(ContentObjectRenderer::class);
         $contentObjectRenderer->stdWrapValue(Argument::cetera())->shouldBeCalledTimes(8);
         $contentObjectRenderer->stdWrapValue('file', $configuration)->willReturn('EXT:core/bar.html');
@@ -212,6 +226,10 @@ class FluidTemplateContentObjectTest extends UnitTestCase
             ],
         ];
 
+        GeneralUtility::setSingletonInstance(
+            PageRenderer::class,
+            new PageRenderer(...$this->getPageRendererConstructorArgs()),
+        );
         $contentObjectRenderer = $this->prophesize(ContentObjectRenderer::class);
         $contentObjectRenderer->stdWrapValue(Argument::cetera())->shouldBeCalledTimes(7);
         $contentObjectRenderer->cObjGetSingle('FILE', ['file' => Environment::getPublicPath() . '/foo/bar.html'], 'template')->willReturn('baz');
@@ -241,6 +259,10 @@ class FluidTemplateContentObjectTest extends UnitTestCase
                 1 => 'dummyPath2/',
             ],
         ];
+        GeneralUtility::setSingletonInstance(
+            PageRenderer::class,
+            new PageRenderer(...$this->getPageRendererConstructorArgs()),
+        );
         $contentObjectRenderer = $this->prophesize(ContentObjectRenderer::class);
         $contentObjectRenderer->stdWrapValue(Argument::cetera())->shouldBeCalledTimes(8);
         $contentObjectRenderer->stdWrapValue('templateName', $configuration)->willReturn('foo');
@@ -275,6 +297,10 @@ class FluidTemplateContentObjectTest extends UnitTestCase
             ],
         ];
 
+        GeneralUtility::setSingletonInstance(
+            PageRenderer::class,
+            new PageRenderer(...$this->getPageRendererConstructorArgs()),
+        );
         $contentObjectRenderer = $this->prophesize(ContentObjectRenderer::class);
         $contentObjectRenderer->stdWrapValue(Argument::cetera())->shouldBeCalledTimes(8);
         $contentObjectRenderer->stdWrapValue('templateName', $configuration)->willReturn('bar');
@@ -301,6 +327,10 @@ class FluidTemplateContentObjectTest extends UnitTestCase
     {
         $configuration = ['layoutRootPath' => 'foo/bar.html'];
 
+        GeneralUtility::setSingletonInstance(
+            PageRenderer::class,
+            new PageRenderer(...$this->getPageRendererConstructorArgs()),
+        );
         $contentObjectRenderer = $this->prophesize(ContentObjectRenderer::class);
         $contentObjectRenderer->stdWrapValue(Argument::cetera())->shouldBeCalledTimes(8);
         $contentObjectRenderer->stdWrapValue('layoutRootPath', $configuration)->willReturn('foo/bar.html');
@@ -331,6 +361,10 @@ class FluidTemplateContentObjectTest extends UnitTestCase
             ],
         ];
 
+        GeneralUtility::setSingletonInstance(
+            PageRenderer::class,
+            new PageRenderer(...$this->getPageRendererConstructorArgs()),
+        );
         $contentObjectRenderer = $this->prophesize(ContentObjectRenderer::class);
         $contentObjectRenderer->stdWrapValue(Argument::cetera())->shouldBeCalledTimes(8);
 
@@ -363,6 +397,10 @@ class FluidTemplateContentObjectTest extends UnitTestCase
             ],
         ];
 
+        GeneralUtility::setSingletonInstance(
+            PageRenderer::class,
+            new PageRenderer(...$this->getPageRendererConstructorArgs()),
+        );
         $contentObjectRenderer = $this->prophesize(ContentObjectRenderer::class);
         $contentObjectRenderer->stdWrapValue(Argument::cetera())->shouldBeCalledTimes(8);
         $contentObjectRenderer->stdWrap('FILE', ['file' => 'foo/bar.html'])->shouldBeCalled();
@@ -387,6 +425,10 @@ class FluidTemplateContentObjectTest extends UnitTestCase
      */
     public function fallbacksForLayoutRootPathAreSet(): void
     {
+        GeneralUtility::setSingletonInstance(
+            PageRenderer::class,
+            new PageRenderer(...$this->getPageRendererConstructorArgs()),
+        );
         $this->addMockViewToSubject();
         $this->standaloneView
             ->expects(self::once())
@@ -408,6 +450,10 @@ class FluidTemplateContentObjectTest extends UnitTestCase
             'layoutRootPaths.' => [10 => 'foo/bar.html', 20 => 'foo/bar2.html'],
         ];
 
+        GeneralUtility::setSingletonInstance(
+            PageRenderer::class,
+            new PageRenderer(...$this->getPageRendererConstructorArgs()),
+        );
         $contentObjectRenderer = $this->prophesize(ContentObjectRenderer::class);
         $contentObjectRenderer->stdWrapValue(Argument::cetera())->shouldBeCalledTimes(8);
         $contentObjectRenderer->stdWrapValue('layoutRootPath', $configuration)->willReturn('foo/main.html');
@@ -437,6 +483,10 @@ class FluidTemplateContentObjectTest extends UnitTestCase
     {
         $configuration = ['partialRootPath' => 'foo/bar.html'];
 
+        GeneralUtility::setSingletonInstance(
+            PageRenderer::class,
+            new PageRenderer(...$this->getPageRendererConstructorArgs()),
+        );
         $contentObjectRenderer = $this->prophesize(ContentObjectRenderer::class);
         $contentObjectRenderer->stdWrapValue(Argument::cetera())->shouldBeCalledTimes(8);
         $contentObjectRenderer->stdWrapValue('partialRootPath', $configuration)->willReturn('foo/bar.html');
@@ -470,6 +520,10 @@ class FluidTemplateContentObjectTest extends UnitTestCase
             ],
         ];
 
+        GeneralUtility::setSingletonInstance(
+            PageRenderer::class,
+            new PageRenderer(...$this->getPageRendererConstructorArgs()),
+        );
         $contentObjectRenderer = $this->prophesize(ContentObjectRenderer::class);
 
         $standAloneView = $this->prophesize(StandaloneView::class);
@@ -497,6 +551,10 @@ class FluidTemplateContentObjectTest extends UnitTestCase
             'partialRootPath.' => ['bar' => 'baz'],
         ];
 
+        GeneralUtility::setSingletonInstance(
+            PageRenderer::class,
+            new PageRenderer(...$this->getPageRendererConstructorArgs()),
+        );
         $contentObjectRenderer = $this->prophesize(ContentObjectRenderer::class);
         $subject = new FluidTemplateContentObject($this->contentDataProcessor);
         $subject->setContentObjectRenderer($contentObjectRenderer->reveal());
@@ -519,6 +577,10 @@ class FluidTemplateContentObjectTest extends UnitTestCase
      */
     public function fallbacksForPartialRootPathAreSet(): void
     {
+        GeneralUtility::setSingletonInstance(
+            PageRenderer::class,
+            new PageRenderer(...$this->getPageRendererConstructorArgs()),
+        );
         $this->addMockViewToSubject();
         $this->standaloneView
             ->expects(self::once())
@@ -537,6 +599,10 @@ class FluidTemplateContentObjectTest extends UnitTestCase
             'partialRootPaths.' => [10 => 'foo', 20 => 'bar'],
         ];
 
+        GeneralUtility::setSingletonInstance(
+            PageRenderer::class,
+            new PageRenderer(...$this->getPageRendererConstructorArgs()),
+        );
         $contentObjectRenderer = $this->prophesize(ContentObjectRenderer::class);
         $contentObjectRenderer->stdWrapValue(Argument::cetera())->shouldBeCalledTimes(8);
         $contentObjectRenderer->stdWrapValue('partialRootPath', $configuration)->willReturn(Environment::getPublicPath() . '/main');
@@ -570,6 +636,10 @@ class FluidTemplateContentObjectTest extends UnitTestCase
             'format' => 'xml',
         ];
 
+        GeneralUtility::setSingletonInstance(
+            PageRenderer::class,
+            new PageRenderer(...$this->getPageRendererConstructorArgs()),
+        );
         $contentObjectRenderer = $this->prophesize(ContentObjectRenderer::class);
         $contentObjectRenderer->stdWrapValue(Argument::cetera())->shouldBeCalledTimes(8);
         $contentObjectRenderer->stdWrapValue('format', $configuration)->willReturn('xml');
@@ -598,6 +668,10 @@ class FluidTemplateContentObjectTest extends UnitTestCase
             'format.' => ['bar' => 'baz'],
         ];
 
+        GeneralUtility::setSingletonInstance(
+            PageRenderer::class,
+            new PageRenderer(...$this->getPageRendererConstructorArgs()),
+        );
         $contentObjectRenderer = $this->prophesize(ContentObjectRenderer::class);
 
         $subject = new FluidTemplateContentObject($this->contentDataProcessor);
@@ -625,6 +699,10 @@ class FluidTemplateContentObjectTest extends UnitTestCase
             ],
         ];
 
+        GeneralUtility::setSingletonInstance(
+            PageRenderer::class,
+            new PageRenderer(...$this->getPageRendererConstructorArgs()),
+        );
         $contentObjectRenderer = $this->prophesize(ContentObjectRenderer::class);
         $contentObjectRenderer->stdWrapValue(Argument::cetera())->shouldBeCalledTimes(8);
         $contentObjectRenderer->stdWrapValue('pluginName', ['pluginName' => 'foo'])->willReturn('foo');
@@ -659,6 +737,10 @@ class FluidTemplateContentObjectTest extends UnitTestCase
             ],
         ];
 
+        GeneralUtility::setSingletonInstance(
+            PageRenderer::class,
+            new PageRenderer(...$this->getPageRendererConstructorArgs()),
+        );
         $contentObjectRenderer = $this->prophesize(ContentObjectRenderer::class);
 
         $subject = new FluidTemplateContentObject($this->contentDataProcessor);
@@ -686,6 +768,10 @@ class FluidTemplateContentObjectTest extends UnitTestCase
             ],
         ];
 
+        GeneralUtility::setSingletonInstance(
+            PageRenderer::class,
+            new PageRenderer(...$this->getPageRendererConstructorArgs()),
+        );
         $contentObjectRenderer = $this->prophesize(ContentObjectRenderer::class);
         $contentObjectRenderer->stdWrapValue(Argument::cetera())->shouldBeCalledTimes(8);
         $contentObjectRenderer->stdWrapValue('controllerExtensionName', ['controllerExtensionName' => 'foo'])->willReturn('foo');
@@ -720,6 +806,10 @@ class FluidTemplateContentObjectTest extends UnitTestCase
             ],
         ];
 
+        GeneralUtility::setSingletonInstance(
+            PageRenderer::class,
+            new PageRenderer(...$this->getPageRendererConstructorArgs()),
+        );
         $contentObjectRenderer = $this->prophesize(ContentObjectRenderer::class);
 
         $subject = new FluidTemplateContentObject($this->contentDataProcessor);
@@ -747,6 +837,10 @@ class FluidTemplateContentObjectTest extends UnitTestCase
             ],
         ];
 
+        GeneralUtility::setSingletonInstance(
+            PageRenderer::class,
+            new PageRenderer(...$this->getPageRendererConstructorArgs()),
+        );
         $contentObjectRenderer = $this->prophesize(ContentObjectRenderer::class);
         $contentObjectRenderer->stdWrapValue(Argument::cetera())->shouldBeCalledTimes(8);
         $contentObjectRenderer->stdWrapValue('controllerName', ['controllerName' => 'foo'])->willReturn('foo');
@@ -781,6 +875,10 @@ class FluidTemplateContentObjectTest extends UnitTestCase
             ],
         ];
 
+        GeneralUtility::setSingletonInstance(
+            PageRenderer::class,
+            new PageRenderer(...$this->getPageRendererConstructorArgs()),
+        );
         $contentObjectRenderer = $this->prophesize(ContentObjectRenderer::class);
 
         $subject = new FluidTemplateContentObject($this->contentDataProcessor);
@@ -808,6 +906,10 @@ class FluidTemplateContentObjectTest extends UnitTestCase
             ],
         ];
 
+        GeneralUtility::setSingletonInstance(
+            PageRenderer::class,
+            new PageRenderer(...$this->getPageRendererConstructorArgs()),
+        );
         $contentObjectRenderer = $this->prophesize(ContentObjectRenderer::class);
         $contentObjectRenderer->stdWrapValue(Argument::cetera())->shouldBeCalledTimes(8);
         $contentObjectRenderer->stdWrapValue('controllerActionName', ['controllerActionName' => 'foo'])->willReturn('foo');
@@ -841,6 +943,11 @@ class FluidTemplateContentObjectTest extends UnitTestCase
             ],
         ];
 
+        GeneralUtility::setSingletonInstance(
+            PageRenderer::class,
+            new PageRenderer(...$this->getPageRendererConstructorArgs()),
+        );
+
         $contentObjectRenderer = $this->prophesize(ContentObjectRenderer::class);
 
         $subject = new FluidTemplateContentObject($this->contentDataProcessor);
@@ -878,6 +985,11 @@ class FluidTemplateContentObjectTest extends UnitTestCase
             ],
         ];
 
+        GeneralUtility::setSingletonInstance(
+            PageRenderer::class,
+            new PageRenderer(...$this->getPageRendererConstructorArgs()),
+        );
+
         /** @var TypoScriptService|MockObject $typoScriptServiceMock */
         $typoScriptServiceMock = $this->getMockBuilder(TypoScriptService::class)->getMock();
         $typoScriptServiceMock
@@ -944,6 +1056,11 @@ class FluidTemplateContentObjectTest extends UnitTestCase
      */
     public function renderCallsCObjGetSingleForAllowedVariable(): void
     {
+        GeneralUtility::setSingletonInstance(
+            PageRenderer::class,
+            new PageRenderer(...$this->getPageRendererConstructorArgs()),
+        );
+
         $configuration = [
             'variables.' => [
                 'aVar' => 'TEXT',
@@ -974,6 +1091,11 @@ class FluidTemplateContentObjectTest extends UnitTestCase
      */
     public function renderAssignsRenderedContentObjectVariableToView(): void
     {
+        GeneralUtility::setSingletonInstance(
+            PageRenderer::class,
+            new PageRenderer(...$this->getPageRendererConstructorArgs()),
+        );
+
         $configuration = [
             'variables.' => [
                 'aVar' => 'TEXT',
@@ -1008,6 +1130,11 @@ class FluidTemplateContentObjectTest extends UnitTestCase
      */
     public function renderAssignsContentObjectRendererDataToView(): void
     {
+        GeneralUtility::setSingletonInstance(
+            PageRenderer::class,
+            new PageRenderer(...$this->getPageRendererConstructorArgs()),
+        );
+
         $this->addMockViewToSubject();
         $this->contentObjectRenderer->data = ['foo'];
         $this->standaloneView
@@ -1022,6 +1149,11 @@ class FluidTemplateContentObjectTest extends UnitTestCase
      */
     public function renderAssignsContentObjectRendererCurrentValueToView(): void
     {
+        GeneralUtility::setSingletonInstance(
+            PageRenderer::class,
+            new PageRenderer(...$this->getPageRendererConstructorArgs()),
+        );
+
         $this->addMockViewToSubject();
         $this->contentObjectRenderer->data = ['currentKey' => 'currentValue'];
         $this->contentObjectRenderer->currentValKey = 'currentKey';
@@ -1037,6 +1169,11 @@ class FluidTemplateContentObjectTest extends UnitTestCase
      */
     public function renderCallsRenderOnStandaloneView(): void
     {
+        GeneralUtility::setSingletonInstance(
+            PageRenderer::class,
+            new PageRenderer(...$this->getPageRendererConstructorArgs()),
+        );
+
         $this->addMockViewToSubject();
         $this->standaloneView
             ->expects(self::once())
@@ -1055,6 +1192,11 @@ class FluidTemplateContentObjectTest extends UnitTestCase
             ],
         ];
 
+        GeneralUtility::setSingletonInstance(
+            PageRenderer::class,
+            new PageRenderer(...$this->getPageRendererConstructorArgs()),
+        );
+
         $contentObjectRenderer = $this->prophesize(ContentObjectRenderer::class);
 
         $subject = new FluidTemplateContentObject($this->contentDataProcessor);
@@ -1084,10 +1226,13 @@ class FluidTemplateContentObjectTest extends UnitTestCase
         ?string $expectedHeader,
         ?string $expectedFooter
     ): void {
-        $pageRendererMock = $this->getMockBuilder(PageRenderer::class)->onlyMethods([
-            'addHeaderData',
-            'addFooterData',
-        ])->getMock();
+        $pageRendererMock = $this->getMockBuilder(PageRenderer::class)
+            ->setConstructorArgs($this->getPageRendererConstructorArgs())
+            ->onlyMethods([
+                'addHeaderData',
+                'addFooterData',
+            ])
+            ->getMock();
         if ($expectedHeader !== null && !empty(trim($expectedHeader))) {
             $pageRendererMock->expects(self::once())->method('addHeaderData')->with($expectedHeader);
         } else {
diff --git a/typo3/sysext/frontend/Tests/Unit/ContentObject/ImageContentObjectTest.php b/typo3/sysext/frontend/Tests/Unit/ContentObject/ImageContentObjectTest.php
index d9565300e9aa..cb5e4bd40d45 100644
--- a/typo3/sysext/frontend/Tests/Unit/ContentObject/ImageContentObjectTest.php
+++ b/typo3/sysext/frontend/Tests/Unit/ContentObject/ImageContentObjectTest.php
@@ -19,7 +19,9 @@ namespace TYPO3\CMS\Frontend\Tests\Unit\ContentObject;
 
 use PHPUnit\Framework\MockObject\MockObject;
 use Prophecy\PhpUnit\ProphecyTrait;
+use TYPO3\CMS\Core\Cache\Frontend\NullFrontend;
 use TYPO3\CMS\Core\Resource\ResourceFactory;
+use TYPO3\CMS\Core\Service\MarkerBasedTemplateService;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\StringUtility;
 use TYPO3\CMS\Frontend\ContentObject\ContentObjectOneSourceCollectionHookInterface;
@@ -52,7 +54,12 @@ class ImageContentObjectTest extends UnitTestCase
         $tsfe = $this->prophesize(TypoScriptFrontendController::class);
         $GLOBALS['TSFE'] = $tsfe->reveal();
         $contentObjectRenderer = new ContentObjectRenderer($tsfe->reveal());
-        $this->subject = $this->getAccessibleMock(ImageContentObject::class, ['dummy']);
+        $this->subject = $this->getAccessibleMock(ImageContentObject::class, ['dummy'], [
+            new MarkerBasedTemplateService(
+                new NullFrontend('hash'),
+                new NullFrontend('runtime'),
+            ),
+        ]);
         $this->subject->setContentObjectRenderer($contentObjectRenderer);
     }
 
@@ -80,8 +87,7 @@ class ImageContentObjectTest extends UnitTestCase
     public function getImageTagTemplateFallsBackToDefaultTemplateIfNoTemplateIsFound($key, $configuration): void
     {
         $defaultImgTagTemplate = '<img src="###SRC###" width="###WIDTH###" height="###HEIGHT###" ###PARAMS### ###ALTPARAMS### ###BORDER######SELFCLOSINGTAGSLASH###>';
-        $subject = $this->getAccessibleMock(ImageContentObject::class, ['dummy'], [], '', false);
-        $result = $subject->_call('getImageTagTemplate', $key, $configuration);
+        $result = $this->subject->_call('getImageTagTemplate', $key, $configuration);
         self::assertEquals($result, $defaultImgTagTemplate);
     }
 
@@ -195,9 +201,8 @@ class ImageContentObjectTest extends UnitTestCase
             ->with(self::equalTo('testImageName'))
             ->willReturn([100, 100, null, 'bar']);
 
-        $subject = $this->getAccessibleMock(ImageContentObject::class, ['dummy']);
-        $subject->setContentObjectRenderer($cObj);
-        $result = $subject->_call('getImageSourceCollection', $layoutKey, $configuration, $file);
+        $this->subject->setContentObjectRenderer($cObj);
+        $result = $this->subject->_call('getImageSourceCollection', $layoutKey, $configuration, $file);
 
         self::assertEquals('---bar---', $result);
     }
@@ -266,9 +271,8 @@ class ImageContentObjectTest extends UnitTestCase
             ->method('stdWrap')
             ->willReturnArgument(0);
 
-        $subject = $this->getAccessibleMock(ImageContentObject::class, ['dummy']);
-        $subject->setContentObjectRenderer($cObj);
-        $result = $subject->_call('getImageSourceCollection', $layoutKey, $configuration, $file);
+        $this->subject->setContentObjectRenderer($cObj);
+        $result = $this->subject->_call('getImageSourceCollection', $layoutKey, $configuration, $file);
 
         self::assertEmpty($result);
     }
@@ -399,7 +403,12 @@ class ImageContentObjectTest extends UnitTestCase
             ->with(self::equalTo('testImageName'))
             ->willReturn([100, 100, null, 'bar-file.jpg']);
 
-        $subject = $this->getAccessibleMock(ImageContentObject::class, ['dummy']);
+        $subject = $this->getAccessibleMock(ImageContentObject::class, ['dummy'], [
+            new MarkerBasedTemplateService(
+                new NullFrontend('hash'),
+                new NullFrontend('runtime'),
+            ),
+        ]);
         $subject->setContentObjectRenderer($cObj);
         $result = $subject->_call('getImageSourceCollection', $layoutKey, $configuration, $file);
 
@@ -464,9 +473,8 @@ class ImageContentObjectTest extends UnitTestCase
             ],
         ];
 
-        $subject = $this->getAccessibleMock(ImageContentObject::class, ['dummy']);
-        $subject->setContentObjectRenderer($cObj);
-        $result = $subject->_call('getImageSourceCollection', 'data', $configuration, StringUtility::getUniqueId('testImage-'));
+        $this->subject->setContentObjectRenderer($cObj);
+        $result = $this->subject->_call('getImageSourceCollection', 'data', $configuration, StringUtility::getUniqueId('testImage-'));
 
         self::assertSame($result, 'isGetOneSourceCollectionCalledCallback');
     }
diff --git a/typo3/sysext/frontend/Tests/Unit/ContentObject/Menu/AbstractMenuContentObjectTest.php b/typo3/sysext/frontend/Tests/Unit/ContentObject/Menu/AbstractMenuContentObjectTest.php
index 21171b609a93..ea2d942ec726 100644
--- a/typo3/sysext/frontend/Tests/Unit/ContentObject/Menu/AbstractMenuContentObjectTest.php
+++ b/typo3/sysext/frontend/Tests/Unit/ContentObject/Menu/AbstractMenuContentObjectTest.php
@@ -40,8 +40,10 @@ use TYPO3\CMS\Core\Localization\LocalizationFactory;
 use TYPO3\CMS\Core\Package\PackageManager;
 use TYPO3\CMS\Core\Page\ImportMap;
 use TYPO3\CMS\Core\Page\ImportMapFactory;
+use TYPO3\CMS\Core\Page\PageRenderer;
 use TYPO3\CMS\Core\Routing\PageArguments;
 use TYPO3\CMS\Core\Site\Entity\Site;
+use TYPO3\CMS\Core\Tests\Unit\Page\PageRendererFactoryTrait;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;
 use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
@@ -53,6 +55,7 @@ use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 class AbstractMenuContentObjectTest extends UnitTestCase
 {
     use ProphecyTrait;
+    use PageRendererFactoryTrait;
 
     /**
      * @var AbstractMenuContentObject|MockObject|AccessibleObjectInterface
@@ -95,6 +98,10 @@ class AbstractMenuContentObjectTest extends UnitTestCase
         $importMapFactoryProphecy = $this->prophesize(ImportMapFactory::class);
         $importMapFactoryProphecy->create()->willReturn($importMapProphecy->reveal());
         GeneralUtility::setSingletonInstance(ImportMapFactory::class, $importMapFactoryProphecy->reveal());
+        GeneralUtility::setSingletonInstance(
+            PageRenderer::class,
+            new PageRenderer(...$this->getPageRendererConstructorArgs()),
+        );
         $frontendUserProphecy = $this->prophesize(FrontendUserAuthentication::class);
         $GLOBALS['TSFE'] = $this->getMockBuilder(TypoScriptFrontendController::class)
             ->setConstructorArgs([new Context(), $site, $site->getDefaultLanguage(), new PageArguments(1, '1', []), $frontendUserProphecy->reveal()])
diff --git a/typo3/sysext/frontend/Tests/Unit/Controller/TypoScriptFrontendControllerTest.php b/typo3/sysext/frontend/Tests/Unit/Controller/TypoScriptFrontendControllerTest.php
index b8285caa4bed..4ac4bd94cfd6 100644
--- a/typo3/sysext/frontend/Tests/Unit/Controller/TypoScriptFrontendControllerTest.php
+++ b/typo3/sysext/frontend/Tests/Unit/Controller/TypoScriptFrontendControllerTest.php
@@ -44,6 +44,7 @@ use TYPO3\CMS\Core\PageTitle\PageTitleProviderManager;
 use TYPO3\CMS\Core\Routing\PageArguments;
 use TYPO3\CMS\Core\Site\Entity\Site;
 use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
+use TYPO3\CMS\Core\Tests\Unit\Page\PageRendererFactoryTrait;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\StringUtility;
 use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;
@@ -58,6 +59,7 @@ use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 class TypoScriptFrontendControllerTest extends UnitTestCase
 {
     use ProphecyTrait;
+    use PageRendererFactoryTrait;
 
     /**
      * @var bool Reset singletons created by subject
@@ -77,6 +79,10 @@ class TypoScriptFrontendControllerTest extends UnitTestCase
         $importMapFactoryProphecy = $this->prophesize(ImportMapFactory::class);
         $importMapFactoryProphecy->create()->willReturn($importMapProphecy->reveal());
         GeneralUtility::setSingletonInstance(ImportMapFactory::class, $importMapFactoryProphecy->reveal());
+        GeneralUtility::setSingletonInstance(
+            PageRenderer::class,
+            new PageRenderer(...$this->getPageRendererConstructorArgs()),
+        );
         $site = $this->createSiteWithDefaultLanguage([
             'locale' => 'fr',
             'typo3Language' => 'fr',
@@ -90,7 +96,9 @@ class TypoScriptFrontendControllerTest extends UnitTestCase
         $pageRepository = $this->getMockBuilder(PageRepository::class)->getMock();
         $this->subject->sys_page = $pageRepository;
 
-        $pageRenderer = $this->getMockBuilder(PageRenderer::class)->getMock();
+        $pageRenderer = $this->getMockBuilder(PageRenderer::class)
+            ->setConstructorArgs($this->getPageRendererConstructorArgs())
+            ->getMock();
         $this->subject->_set('pageRenderer', $pageRenderer);
     }
 
diff --git a/typo3/sysext/frontend/Tests/Unit/Plugin/AbstractPluginTest.php b/typo3/sysext/frontend/Tests/Unit/Plugin/AbstractPluginTest.php
index fa3ff9b4c8e3..1d3aa354fa52 100644
--- a/typo3/sysext/frontend/Tests/Unit/Plugin/AbstractPluginTest.php
+++ b/typo3/sysext/frontend/Tests/Unit/Plugin/AbstractPluginTest.php
@@ -21,6 +21,8 @@ use Prophecy\Argument;
 use Prophecy\PhpUnit\ProphecyTrait;
 use Psr\Http\Message\ServerRequestInterface;
 use Symfony\Component\DependencyInjection\Container;
+use TYPO3\CMS\Core\Cache\Frontend\NullFrontend;
+use TYPO3\CMS\Core\Service\MarkerBasedTemplateService;
 use TYPO3\CMS\Core\Site\Entity\Site;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\StringUtility;
@@ -62,6 +64,10 @@ class AbstractPluginTest extends UnitTestCase
             return $args[0] ?? '';
         });
 
+        GeneralUtility::addInstance(MarkerBasedTemplateService::class, new MarkerBasedTemplateService(
+            new NullFrontend('hash'),
+            new NullFrontend('runtime'),
+        ));
         $this->abstractPlugin = new AbstractPlugin(null, $tsfe->reveal());
         $contentObjectRenderer = new ContentObjectRenderer($tsfe->reveal());
         $contentObjectRenderer->setRequest($this->prophesize(ServerRequestInterface::class)->reveal());
-- 
GitLab