From 4e25a5787e1215645df02beaf51878a5c7f4bb07 Mon Sep 17 00:00:00 2001
From: Benjamin Franzke <bfr@qbus.de>
Date: Thu, 5 Sep 2019 21:43:56 +0200
Subject: [PATCH] [FEATURE] Add cache:warmup console command
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

A cache:warmup CLI command is added that is safe to be
executed during deployment or development mode.

It is made extra sure that the command will be executable,
even if all caches are stale or even broken. That means this
command is able to repair broken di, localconf and tca caches.
Technically this became due to preparations for lowlevel
cli commands in #86248.

Caches that will be warmed up by TYPO3 core:
 + DI
 + ext_localconf
 + TCA
 + ext_tables
 + middleware stack
 + PackageManager
 + sites-configuration
 + expressionLanguageProviders
 + BackendRoutes
 + l10n (parses all xlf files, and creates cache for all languages)
 + assets: BackendIcons
 + dashboard: DashboardPresets
 + dashboard: WidgetGroups

Out of scope or impossible to be warmed:
 * Frontend warmup:
   Can be providedby extensions that listen for the
   CacheWarmupEvent
 * RequireJS (assets cache): Often applies with context-conditions
   therefore not reliably warmable.
 * Extbase – better of for a separate patch
   intesting cache entries would be: RequestHandlers, ClassSchemata
   and PersistenceClasses(?)
 * Warmup of Fluid templates:
   It is impossible to auto-detect whether all files in
   Resources/{Templates,Layouts,Partials} are Fluid files.
   Could by added later on with an explicit registry that
   allows to define which templates are suitable for warmup.

Releases: master
Resolves: #93436
Change-Id: I53dd8915a2e06b3d21d778af985c76132e0d4f67
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/61645
Tested-by: Helmut Hummel <typo3@helhum.io>
Tested-by: core-ci <typo3@b13.com>
Tested-by: Benni Mack <benni@typo3.org>
Reviewed-by: Helmut Hummel <typo3@helhum.io>
Reviewed-by: Benni Mack <benni@typo3.org>
---
 .../backend/Classes/ServiceProvider.php       |  23 ++++
 .../core/Classes/Cache/CacheManager.php       |  19 +++
 .../Classes/Cache/Event/CacheWarmupEvent.php  |  52 ++++++++
 .../Classes/Command/CacheWarmupCommand.php    | 113 +++++++++++++++++
 .../Configuration/ExtensionConfiguration.php  |  16 ++-
 .../Configuration/SiteConfiguration.php       |   8 ++
 .../Classes/Core/Event/WarmupBaseTcaCache.php |  43 +++++++
 .../DependencyInjection/ContainerBuilder.php  |  16 ++-
 .../ProviderConfigurationLoader.php           |  11 ++
 .../Classes/Http/MiddlewareStackResolver.php  |  13 ++
 .../core/Classes/Imaging/IconRegistry.php     |  19 +++
 .../core/Classes/Localization/CacheWarmer.php |  63 +++++++++
 .../core/Classes/Package/PackageManager.php   |  18 +++
 typo3/sysext/core/Classes/ServiceProvider.php |  23 ++++
 .../Utility/ExtensionManagementUtility.php    |  18 ++-
 typo3/sysext/core/Configuration/Services.yaml |  16 +++
 ...436-IntroduceCacheWarmupConsoleCommand.rst |  80 ++++++++++++
 .../Command/CacheWarmupCommandTest.php        | 120 ++++++++++++++++++
 ...ensionManagementUtilityAccessibleProxy.php |  11 --
 .../ExtensionManagementUtilityTest.php        |  24 +---
 .../dashboard/Classes/ServiceProvider.php     |  37 +++++-
 21 files changed, 696 insertions(+), 47 deletions(-)
 create mode 100644 typo3/sysext/core/Classes/Cache/Event/CacheWarmupEvent.php
 create mode 100644 typo3/sysext/core/Classes/Command/CacheWarmupCommand.php
 create mode 100644 typo3/sysext/core/Classes/Core/Event/WarmupBaseTcaCache.php
 create mode 100644 typo3/sysext/core/Classes/Localization/CacheWarmer.php
 create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Feature-93436-IntroduceCacheWarmupConsoleCommand.rst
 create mode 100644 typo3/sysext/core/Tests/Functional/Command/CacheWarmupCommandTest.php

diff --git a/typo3/sysext/backend/Classes/ServiceProvider.php b/typo3/sysext/backend/Classes/ServiceProvider.php
index 349910b1d5c9..791c2b4fb0aa 100644
--- a/typo3/sysext/backend/Classes/ServiceProvider.php
+++ b/typo3/sysext/backend/Classes/ServiceProvider.php
@@ -25,10 +25,12 @@ use TYPO3\CMS\Backend\Http\RouteDispatcher;
 use TYPO3\CMS\Backend\Routing\Route;
 use TYPO3\CMS\Backend\Routing\Router;
 use TYPO3\CMS\Backend\Routing\UriBuilder;
+use TYPO3\CMS\Core\Cache\Event\CacheWarmupEvent;
 use TYPO3\CMS\Core\Cache\Exception\InvalidDataException;
 use TYPO3\CMS\Core\Configuration\ConfigurationManager;
 use TYPO3\CMS\Core\Context\Context;
 use TYPO3\CMS\Core\Core\Environment;
+use TYPO3\CMS\Core\EventDispatcher\ListenerProvider;
 use TYPO3\CMS\Core\Exception as CoreException;
 use TYPO3\CMS\Core\Http\MiddlewareDispatcher;
 use TYPO3\CMS\Core\Http\MiddlewareStackResolver;
@@ -54,6 +56,7 @@ class ServiceProvider extends AbstractServiceProvider
             UriBuilder::class => [ static::class, 'getUriBuilder' ],
             'backend.middlewares' => [ static::class, 'getBackendMiddlewares' ],
             'backend.routes' => [ static::class, 'getBackendRoutes' ],
+            'backend.routes.warmer' => [ static::class, 'getBackendRoutesWarmer' ],
         ];
     }
 
@@ -61,6 +64,7 @@ class ServiceProvider extends AbstractServiceProvider
     {
         return [
             Router::class => [ static::class, 'configureBackendRouter' ],
+            ListenerProvider::class => [ static::class, 'addEventListeners' ],
         ] + parent::getExtensions();
     }
 
@@ -140,4 +144,23 @@ class ServiceProvider extends AbstractServiceProvider
     {
         return new ArrayObject();
     }
+
+    public static function getBackendRoutesWarmer(ContainerInterface $container): \Closure
+    {
+        return function (CacheWarmupEvent $event) use ($container) {
+            if ($event->hasGroup('system')) {
+                $cache = $container->get('cache.core');
+                $cacheIdentifier = 'BackendRoutes_' . sha1((string)(new Typo3Version()) . Environment::getProjectPath() . 'BackendRoutes');
+                $routesFromPackages = $container->get('backend.routes')->getArrayCopy();
+                $cache->set($cacheIdentifier, 'return ' . var_export($routesFromPackages, true) . ';');
+            }
+        };
+    }
+
+    public static function addEventListeners(ContainerInterface $container, ListenerProvider $listenerProvider): ListenerProvider
+    {
+        $listenerProvider->addListener(CacheWarmupEvent::class, 'backend.routes.warmer');
+
+        return $listenerProvider;
+    }
 }
diff --git a/typo3/sysext/core/Classes/Cache/CacheManager.php b/typo3/sysext/core/Classes/Cache/CacheManager.php
index 643c17ae3285..1bdc965c1b8f 100644
--- a/typo3/sysext/core/Classes/Cache/CacheManager.php
+++ b/typo3/sysext/core/Classes/Cache/CacheManager.php
@@ -280,6 +280,25 @@ class CacheManager implements SingletonInterface
         }
     }
 
+    /**
+     * @return string[]
+     * @internal
+     */
+    public function getCacheGroups(): array
+    {
+        $groups = array_keys($this->cacheGroups);
+
+        foreach ($this->cacheConfigurations as $config) {
+            foreach ($config['groups'] ?? [] as $group) {
+                if (!in_array($group, $groups, true)) {
+                    $groups[] = $group;
+                }
+            }
+        }
+
+        return $groups;
+    }
+
     /**
      * Instantiates all registered caches.
      */
diff --git a/typo3/sysext/core/Classes/Cache/Event/CacheWarmupEvent.php b/typo3/sysext/core/Classes/Cache/Event/CacheWarmupEvent.php
new file mode 100644
index 000000000000..25300bb4d533
--- /dev/null
+++ b/typo3/sysext/core/Classes/Cache/Event/CacheWarmupEvent.php
@@ -0,0 +1,52 @@
+<?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\Cache\Event;
+
+/**
+ * Event fired when caches are to be warmed up
+ */
+final class CacheWarmupEvent
+{
+    private array $groups = [];
+    private array $errors = [];
+
+    public function __construct(array $groups)
+    {
+        $this->groups = $groups;
+    }
+
+    public function getGroups(): array
+    {
+        return $this->groups;
+    }
+
+    public function hasGroup(string $group): bool
+    {
+        return in_array($group, $this->groups, true);
+    }
+
+    public function getErrors(): array
+    {
+        return $this->errors;
+    }
+
+    public function addError(string $error): void
+    {
+        $this->errors[] = $error;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Command/CacheWarmupCommand.php b/typo3/sysext/core/Classes/Command/CacheWarmupCommand.php
new file mode 100644
index 000000000000..4f6a09bfb982
--- /dev/null
+++ b/typo3/sysext/core/Classes/Command/CacheWarmupCommand.php
@@ -0,0 +1,113 @@
+<?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\Command;
+
+use Psr\EventDispatcher\EventDispatcherInterface;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use TYPO3\CMS\Core\Cache\CacheManager;
+use TYPO3\CMS\Core\Cache\Event\CacheWarmupEvent;
+use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
+use TYPO3\CMS\Core\Configuration\Event\AfterTcaCompilationEvent;
+use TYPO3\CMS\Core\Core\BootService;
+use TYPO3\CMS\Core\Core\Event\WarmupBaseTcaCache;
+use TYPO3\CMS\Core\DependencyInjection\ContainerBuilder;
+use TYPO3\CMS\Core\EventDispatcher\ListenerProvider;
+use TYPO3\CMS\Core\Package\PackageManager;
+use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
+
+class CacheWarmupCommand extends Command
+{
+    protected ContainerBuilder $containerBuilder;
+    protected PackageManager $packageManager;
+    protected BootService $bootService;
+    protected FrontendInterface $dependencyInjectionCache;
+
+    public function __construct(
+        ContainerBuilder $containerBuilder,
+        PackageManager $packageManager,
+        BootService $bootService,
+        FrontendInterface $dependencyInjectionCache
+    ) {
+        $this->containerBuilder = $containerBuilder;
+        $this->packageManager = $packageManager;
+        $this->bootService = $bootService;
+        $this->dependencyInjectionCache = $dependencyInjectionCache;
+        parent::__construct('cache:warmup');
+    }
+
+    /**
+     * Defines the allowed options for this command
+     */
+    protected function configure(): void
+    {
+        $this->setDescription('Warmup TYPO3 caches.');
+        $this->setHelp('This command is useful for deployments to warmup caches during release preparation.');
+        $this->setDefinition([
+            new InputOption('group', 'g', InputOption::VALUE_OPTIONAL, 'The cache group to warmup (system, pages, di or all)', 'all'),
+        ]);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $group = $input->getOption('group') ?? 'all';
+
+        if ($group === 'di' || $group === 'system' || $group === 'all') {
+            $this->containerBuilder->warmupCache($this->packageManager, $this->dependencyInjectionCache);
+            if ($group === 'di') {
+                return 0;
+            }
+        }
+
+        $container = $this->bootService->getContainer();
+
+        $allowExtFileCaches = true;
+        if ($group === 'system' || $group === 'all') {
+            $coreCache = $container->get('cache.core');
+            ExtensionManagementUtility::createExtLocalconfCacheEntry($coreCache);
+            ExtensionManagementUtility::createExtTablesCacheEntry($coreCache);
+
+            // Load TCA uncached…
+            $allowExtFileCaches = false;
+            // …but store the fresh base TCA to cache
+            $listenerProvider = $container->get(ListenerProvider::class);
+            $listenerProvider->addListener(AfterTcaCompilationEvent::class, WarmupBaseTcaCache::class, 'storeBaseTcaCache');
+        }
+
+        // Perform a full boot to load localconf as requirement extensions and for TCA loading.
+        // TCA will be cached during dispatch of AfterTcaCompilationEvent.
+        $this->bootService->loadExtLocalconfDatabaseAndExtTables(false, $allowExtFileCaches);
+
+        $eventDispatcher = $container->get(EventDispatcherInterface::class);
+
+        $groups = $group === 'all' ? $container->get(CacheManager::class)->getCacheGroups() : [$group];
+        $event = new CacheWarmupEvent($groups);
+        $eventDispatcher->dispatch($event);
+
+        if (count($event->getErrors()) > 0) {
+            return 1;
+        }
+
+        return 0;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Configuration/ExtensionConfiguration.php b/typo3/sysext/core/Classes/Configuration/ExtensionConfiguration.php
index 340d2fc4d8d2..40be8d049fc4 100644
--- a/typo3/sysext/core/Classes/Configuration/ExtensionConfiguration.php
+++ b/typo3/sysext/core/Classes/Configuration/ExtensionConfiguration.php
@@ -84,7 +84,7 @@ class ExtensionConfiguration
         $hasBeenSynchronized = false;
         if (!$this->hasConfiguration($extension)) {
             // This if() should not be hit at "casual" runtime, but only in early setup phases
-            $this->synchronizeExtConfTemplateWithLocalConfigurationOfAllExtensions();
+            $this->synchronizeExtConfTemplateWithLocalConfigurationOfAllExtensions(true);
             $hasBeenSynchronized = true;
             if (!$this->hasConfiguration($extension)) {
                 // If there is still no such entry, even after sync -> throw
@@ -102,7 +102,7 @@ class ExtensionConfiguration
         if (!ArrayUtility::isValidPath($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'], $extension . '/' . $path)) {
             // This if() should not be hit at "casual" runtime, but only in early setup phases
             if (!$hasBeenSynchronized) {
-                $this->synchronizeExtConfTemplateWithLocalConfigurationOfAllExtensions();
+                $this->synchronizeExtConfTemplateWithLocalConfigurationOfAllExtensions(true);
             }
             // If there is still no such entry, even after sync -> throw
             if (!ArrayUtility::isValidPath($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'], $extension . '/' . $path)) {
@@ -170,12 +170,15 @@ class ExtensionConfiguration
      * writing and loading LocalConfiguration many times.
      *
      * @param array $configuration Configuration of all extensions
+     * @param bool $skipWriteIfLocalConfiguationDoesNotExist
      * @internal
      */
-    public function setAll(array $configuration): void
+    public function setAll(array $configuration, bool $skipWriteIfLocalConfiguationDoesNotExist = false): void
     {
         $configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
-        $configurationManager->setLocalConfigurationValueByPath('EXTENSIONS', $configuration);
+        if ($skipWriteIfLocalConfiguationDoesNotExist === false || @file_exists($configurationManager->getLocalConfigurationFileLocation())) {
+            $configurationManager->setLocalConfigurationValueByPath('EXTENSIONS', $configuration);
+        }
         $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'] = $configuration;
     }
 
@@ -186,9 +189,10 @@ class ExtensionConfiguration
      * Used when entering the install tool, during installation and if calling ->get()
      * with an extension or path that is not yet found in LocalConfiguration
      *
+     * @param bool $skipWriteIfLocalConfiguationDoesNotExist
      * @internal
      */
-    public function synchronizeExtConfTemplateWithLocalConfigurationOfAllExtensions(): void
+    public function synchronizeExtConfTemplateWithLocalConfigurationOfAllExtensions(bool $skipWriteIfLocalConfiguationDoesNotExist = false): void
     {
         $activePackages = GeneralUtility::makeInstance(PackageManager::class)->getActivePackages();
         $fullConfiguration = [];
@@ -207,7 +211,7 @@ class ExtensionConfiguration
         }
         // Write new config if changed. Loose array comparison to not write if only array key order is different
         if ($fullConfiguration != $currentLocalConfiguration) {
-            $this->setAll($fullConfiguration);
+            $this->setAll($fullConfiguration, $skipWriteIfLocalConfiguationDoesNotExist);
         }
     }
 
diff --git a/typo3/sysext/core/Classes/Configuration/SiteConfiguration.php b/typo3/sysext/core/Classes/Configuration/SiteConfiguration.php
index 14690ac5ab35..520a46889ed8 100644
--- a/typo3/sysext/core/Classes/Configuration/SiteConfiguration.php
+++ b/typo3/sysext/core/Classes/Configuration/SiteConfiguration.php
@@ -19,6 +19,7 @@ namespace TYPO3\CMS\Core\Configuration;
 
 use Symfony\Component\Finder\Finder;
 use Symfony\Component\Yaml\Yaml;
+use TYPO3\CMS\Core\Cache\Event\CacheWarmupEvent;
 use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
 use TYPO3\CMS\Core\Configuration\Loader\YamlFileLoader;
 use TYPO3\CMS\Core\Exception\SiteNotFoundException;
@@ -322,4 +323,11 @@ class SiteConfiguration implements SingletonInterface
 
         return $removed;
     }
+
+    public function warmupCaches(CacheWarmupEvent $event): void
+    {
+        if ($event->hasGroup('system')) {
+            $this->getAllSiteConfigurationFromFiles(false);
+        }
+    }
 }
diff --git a/typo3/sysext/core/Classes/Core/Event/WarmupBaseTcaCache.php b/typo3/sysext/core/Classes/Core/Event/WarmupBaseTcaCache.php
new file mode 100644
index 000000000000..c5f5d2f88332
--- /dev/null
+++ b/typo3/sysext/core/Classes/Core/Event/WarmupBaseTcaCache.php
@@ -0,0 +1,43 @@
+<?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\Core\Event;
+
+use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
+use TYPO3\CMS\Core\Configuration\Event\AfterTcaCompilationEvent;
+use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
+
+final class WarmupBaseTcaCache
+{
+    private FrontendInterface $coreCache;
+
+    public function __construct(FrontendInterface $coreCache)
+    {
+        $this->coreCache = $coreCache;
+    }
+
+    /**
+     * Stores TCA caches during cache warmup.
+     *
+     * This event handler is injected dynamically by TYPO3\CMS\Core\Command\CacheWarmupCommand.
+     */
+    public function storeBaseTcaCache(AfterTcaCompilationEvent $event): void
+    {
+        $GLOBALS['TCA'] = $event->getTca();
+        ExtensionManagementUtility::createBaseTcaCacheFile($this->coreCache);
+    }
+}
diff --git a/typo3/sysext/core/Classes/DependencyInjection/ContainerBuilder.php b/typo3/sysext/core/Classes/DependencyInjection/ContainerBuilder.php
index 040f679bce10..53bea6a9cfdb 100644
--- a/typo3/sysext/core/Classes/DependencyInjection/ContainerBuilder.php
+++ b/typo3/sysext/core/Classes/DependencyInjection/ContainerBuilder.php
@@ -57,6 +57,19 @@ class ContainerBuilder
         $this->defaultServices = $earlyInstances + [ self::class => $this ];
     }
 
+    /**
+     * @param PackageManager $packageManager
+     * @param FrontendInterface $cache
+     * @internal
+     */
+    public function warmupCache(PackageManager $packageManager, FrontendInterface $cache): void
+    {
+        $registry = new ServiceProviderRegistry($packageManager);
+        $containerBuilder = $this->buildContainer($packageManager, $registry);
+        $cacheIdentifier = $this->getCacheIdentifier($packageManager);
+        $this->dumpContainer($containerBuilder, $cache, $cacheIdentifier);
+    }
+
     /**
      * @param PackageManager $packageManager
      * @param FrontendInterface $cache
@@ -165,8 +178,9 @@ class ContainerBuilder
     /**
      * @param PackageManager $packageManager
      * @return string
+     * @internal may only be used in this class or in functional tests
      */
-    protected function getCacheIdentifier(PackageManager $packageManager): string
+    public function getCacheIdentifier(PackageManager $packageManager): string
     {
         $packageManagerCacheIdentifier = $packageManager->getCacheIdentifier() ?? '';
         return $this->cacheIdentifiers[$packageManagerCacheIdentifier] ?? $this->createCacheIdentifier($packageManagerCacheIdentifier);
diff --git a/typo3/sysext/core/Classes/ExpressionLanguage/ProviderConfigurationLoader.php b/typo3/sysext/core/Classes/ExpressionLanguage/ProviderConfigurationLoader.php
index 460cef851a13..30a4a830ac11 100644
--- a/typo3/sysext/core/Classes/ExpressionLanguage/ProviderConfigurationLoader.php
+++ b/typo3/sysext/core/Classes/ExpressionLanguage/ProviderConfigurationLoader.php
@@ -17,6 +17,7 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Core\ExpressionLanguage;
 
+use TYPO3\CMS\Core\Cache\Event\CacheWarmupEvent;
 use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
 use TYPO3\CMS\Core\Package\PackageManager;
 
@@ -68,4 +69,14 @@ class ProviderConfigurationLoader
         $this->cache->set($this->cacheIdentifier, 'return ' . var_export($providers, true) . ';');
         return $providers;
     }
+
+    /**
+     * @internal
+     */
+    public function warmupCaches(CacheWarmupEvent $event): void
+    {
+        if ($event->hasGroup('system')) {
+            $this->createCache();
+        }
+    }
 }
diff --git a/typo3/sysext/core/Classes/Http/MiddlewareStackResolver.php b/typo3/sysext/core/Classes/Http/MiddlewareStackResolver.php
index 5823857c3610..a6277b2919c2 100644
--- a/typo3/sysext/core/Classes/Http/MiddlewareStackResolver.php
+++ b/typo3/sysext/core/Classes/Http/MiddlewareStackResolver.php
@@ -19,6 +19,7 @@ namespace TYPO3\CMS\Core\Http;
 
 use ArrayObject;
 use Psr\Container\ContainerInterface;
+use TYPO3\CMS\Core\Cache\Event\CacheWarmupEvent;
 use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend as PhpFrontendCache;
 use TYPO3\CMS\Core\Core\Environment;
 use TYPO3\CMS\Core\Information\Typo3Version;
@@ -135,4 +136,16 @@ class MiddlewareStackResolver
     {
         return 'middlewares_' . $stackName . '_' . sha1((string)(new Typo3Version()) . Environment::getProjectPath());
     }
+
+    public function warmupCaches(CacheWarmupEvent $event): void
+    {
+        if ($event->hasGroup('system')) {
+            $allMiddlewares = $this->loadConfiguration();
+            $middlewares = $this->sanitizeMiddlewares($allMiddlewares);
+
+            foreach ($middlewares as $stack => $middlewaresOfStack) {
+                $this->cache->set($this->getCacheIdentifier($stack), 'return ' . var_export($middlewaresOfStack, true) . ';');
+            }
+        }
+    }
 }
diff --git a/typo3/sysext/core/Classes/Imaging/IconRegistry.php b/typo3/sysext/core/Classes/Imaging/IconRegistry.php
index a1b001a0ef2c..a65f9389102f 100644
--- a/typo3/sysext/core/Classes/Imaging/IconRegistry.php
+++ b/typo3/sysext/core/Classes/Imaging/IconRegistry.php
@@ -15,6 +15,7 @@
 
 namespace TYPO3\CMS\Core\Imaging;
 
+use TYPO3\CMS\Core\Cache\Event\CacheWarmupEvent;
 use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
 use TYPO3\CMS\Core\Core\Environment;
 use TYPO3\CMS\Core\Exception;
@@ -810,4 +811,22 @@ class IconRegistry implements SingletonInterface
         }
         return BitmapIconProvider::class;
     }
+
+    public function warmupCaches(CacheWarmupEvent $event): void
+    {
+        if ($event->hasGroup('system')) {
+            $backupIcons = $this->icons;
+            $backupAliases = $this->iconAliases;
+            $this->icons = [];
+            $this->iconAliases = [];
+
+            $this->registerBackendIcons();
+            // all found icons should now be present, for historic reasons now merge w/ the statically declared icons
+            $this->icons = array_merge($this->icons, $this->iconAliases, $this->staticIcons);
+            $this->cache->set($this->getBackendIconsCacheIdentifier(), $this->icons);
+
+            $this->icons = $backupIcons;
+            $this->iconAliases = $backupAliases;
+        }
+    }
 }
diff --git a/typo3/sysext/core/Classes/Localization/CacheWarmer.php b/typo3/sysext/core/Classes/Localization/CacheWarmer.php
new file mode 100644
index 000000000000..b3360e6057a7
--- /dev/null
+++ b/typo3/sysext/core/Classes/Localization/CacheWarmer.php
@@ -0,0 +1,63 @@
+<?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\Localization;
+
+use TYPO3\CMS\Core\Cache\Event\CacheWarmupEvent;
+use TYPO3\CMS\Core\Package\PackageManager;
+
+/**
+ * @internal
+ */
+class CacheWarmer
+{
+    protected PackageManager $packageManager;
+    protected LocalizationFactory $localizationFactory;
+
+    public function __construct(
+        PackageManager $packageManager,
+        LocalizationFactory $localizationFactory
+    ) {
+        $this->packageManager = $packageManager;
+        $this->localizationFactory = $localizationFactory;
+    }
+
+    public function warmupCaches(CacheWarmupEvent $event): void
+    {
+        if ($event->hasGroup('system')) {
+            $languages = array_merge(['default'], $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['lang']['availableLanguages'] ?? []);
+            $packages = $this->packageManager->getActivePackages();
+            foreach ($packages as $package) {
+                $dir = $package->getPackagePath() . 'Resources/Private/Language';
+                if (!is_dir($dir)) {
+                    continue;
+                }
+                $recursiveIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir));
+                // Search for all files with suffix *.xlf and  without a dot in the file basename
+                $fileIterator = new \RegexIterator($recursiveIterator, '#^.+/[^.]+\.xlf$#', \RegexIterator::GET_MATCH);
+                $shorthand = 'EXT:' . $package->getPackageKey() . '/Resources/Private/Language';
+                foreach ($fileIterator as $match) {
+                    $fileReference = str_replace($dir, $shorthand, $match[0]);
+                    foreach ($languages as $language) {
+                        // @todo: Force cache renewal
+                        $this->localizationFactory->getParsedData($fileReference, $language);
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/typo3/sysext/core/Classes/Package/PackageManager.php b/typo3/sysext/core/Classes/Package/PackageManager.php
index 990825024fa3..adf8723f520c 100644
--- a/typo3/sysext/core/Classes/Package/PackageManager.php
+++ b/typo3/sysext/core/Classes/Package/PackageManager.php
@@ -17,6 +17,7 @@ namespace TYPO3\CMS\Core\Package;
 
 use Symfony\Component\Finder\Finder;
 use Symfony\Component\Finder\SplFileInfo;
+use TYPO3\CMS\Core\Cache\Event\CacheWarmupEvent;
 use TYPO3\CMS\Core\Core\ClassLoadingInformation;
 use TYPO3\CMS\Core\Core\Environment;
 use TYPO3\CMS\Core\Package\Cache\PackageCacheEntry;
@@ -1146,4 +1147,21 @@ class PackageManager implements SingletonInterface
 
         return $frameworkPackageKeys;
     }
+
+    /**
+     * @internal
+     */
+    public function warmupCaches(CacheWarmupEvent $event): void
+    {
+        if (Environment::isComposerMode()) {
+            return;
+        }
+        if ($event->hasGroup('system')) {
+            if (count($this->packageStatesConfiguration) === 0) {
+                $this->loadPackageStates();
+                $this->initializePackageObjects();
+            }
+            $this->saveToPackageCache();
+        }
+    }
 }
diff --git a/typo3/sysext/core/Classes/ServiceProvider.php b/typo3/sysext/core/Classes/ServiceProvider.php
index 23f6af4796ee..6806dac60148 100644
--- a/typo3/sysext/core/Classes/ServiceProvider.php
+++ b/typo3/sysext/core/Classes/ServiceProvider.php
@@ -49,6 +49,7 @@ class ServiceProvider extends AbstractServiceProvider
             Configuration\SiteConfiguration::class => [ static::class, 'getSiteConfiguration' ],
             Command\ListCommand::class => [ static::class, 'getListCommand' ],
             HelpCommand::class => [ static::class, 'getHelpCommand' ],
+            Command\CacheWarmupCommand::class => [ static::class, 'getCacheWarmupCommand' ],
             Command\DumpAutoloadCommand::class => [ static::class, 'getDumpAutoloadCommand' ],
             Console\CommandApplication::class => [ static::class, 'getConsoleCommandApplication' ],
             Console\CommandRegistry::class => [ static::class, 'getConsoleCommandRegistry' ],
@@ -171,6 +172,16 @@ class ServiceProvider extends AbstractServiceProvider
         return new HelpCommand();
     }
 
+    public static function getCacheWarmupCommand(ContainerInterface $container): Command\CacheWarmupCommand
+    {
+        return new Command\CacheWarmupCommand(
+            $container->get(ContainerBuilder::class),
+            $container->get(Package\PackageManager::class),
+            $container->get(Core\BootService::class),
+            $container->get('cache.di')
+        );
+    }
+
     public static function getDumpAutoloadCommand(ContainerInterface $container): Command\DumpAutoloadCommand
     {
         return new Command\DumpAutoloadCommand();
@@ -213,6 +224,16 @@ class ServiceProvider extends AbstractServiceProvider
             Package\PackageManager::class,
             'packagesMayHaveChanged'
         );
+
+        $cacheWarmers = [
+            Configuration\SiteConfiguration::class,
+            Http\MiddlewareStackResolver::class,
+            Imaging\IconRegistry::class,
+            Package\PackageManager::class,
+        ];
+        foreach ($cacheWarmers as $service) {
+            $listenerProvider->addListener(Cache\Event\CacheWarmupEvent::class, $service, 'warmupCaches');
+        }
         return $listenerProvider;
     }
 
@@ -452,6 +473,8 @@ class ServiceProvider extends AbstractServiceProvider
 
         $commandRegistry->addLazyCommand('help', HelpCommand::class, 'Displays help for a command');
 
+        $commandRegistry->addLazyCommand('cache:warmup', Command\CacheWarmupCommand::class, 'Cache warmup for all, system or frontend caches.');
+
         $commandRegistry->addLazyCommand('dumpautoload', Command\DumpAutoloadCommand::class, 'Updates class loading information in non-composer mode.', Environment::isComposerMode());
         $commandRegistry->addLazyCommand('extensionmanager:extension:dumpclassloadinginformation', Command\DumpAutoloadCommand::class, null, Environment::isComposerMode(), false, 'dumpautoload');
         $commandRegistry->addLazyCommand('extension:dumpclassloadinginformation', Command\DumpAutoloadCommand::class, null, Environment::isComposerMode(), false, 'dumpautoload');
diff --git a/typo3/sysext/core/Classes/Utility/ExtensionManagementUtility.php b/typo3/sysext/core/Classes/Utility/ExtensionManagementUtility.php
index fed41fcfa926..cbdd1e120315 100644
--- a/typo3/sysext/core/Classes/Utility/ExtensionManagementUtility.php
+++ b/typo3/sysext/core/Classes/Utility/ExtensionManagementUtility.php
@@ -1519,8 +1519,9 @@ tt_content.' . $key . $suffix . ' {
      * Create cache entry for concatenated ext_localconf.php files
      *
      * @param FrontendInterface $codeCache
+     * @internal
      */
-    protected static function createExtLocalconfCacheEntry(FrontendInterface $codeCache)
+    public static function createExtLocalconfCacheEntry(FrontendInterface $codeCache)
     {
         $phpCodeToCache = [];
         // Set same globals as in loadSingleExtLocalconfFiles()
@@ -1608,8 +1609,9 @@ tt_content.' . $key . $suffix . ' {
      * the file should return an array with content of a specific table.
      *
      * @see Extension core, extensionmanager and others for examples.
+     * @internal
      */
-    protected static function buildBaseTcaFromSingleFiles()
+    public static function buildBaseTcaFromSingleFiles()
     {
         $GLOBALS['TCA'] = [];
 
@@ -1683,8 +1685,9 @@ tt_content.' . $key . $suffix . ' {
      * file for next access instead of cycling through all extensions again.
      *
      * @param FrontendInterface $codeCache
+     * @internal
      */
-    protected static function createBaseTcaCacheFile(FrontendInterface $codeCache)
+    public static function createBaseTcaCacheFile(FrontendInterface $codeCache)
     {
         // @deprecated Remove 'categoryRegistry' in v12
         $codeCache->set(
@@ -1726,7 +1729,7 @@ tt_content.' . $key . $suffix . ' {
             $hasCache = $codeCache->require($cacheIdentifier) !== false;
             if (!$hasCache) {
                 self::loadSingleExtTablesFiles();
-                self::createExtTablesCacheEntry();
+                self::createExtTablesCacheEntry($codeCache);
             }
         } else {
             self::loadSingleExtTablesFiles();
@@ -1749,8 +1752,11 @@ tt_content.' . $key . $suffix . ' {
 
     /**
      * Create concatenated ext_tables.php cache file
+     *
+     * @param FrontendInterface $codeCache
+     * @internal
      */
-    protected static function createExtTablesCacheEntry()
+    public static function createExtTablesCacheEntry(FrontendInterface $codeCache)
     {
         $phpCodeToCache = [];
         // Set same globals as in loadSingleExtTablesFiles()
@@ -1780,7 +1786,7 @@ tt_content.' . $key . $suffix . ' {
         // Remove all start and ending php tags from content
         $phpCodeToCache = preg_replace('/<\\?php|\\?>/is', '', $phpCodeToCache);
         $phpCodeToCache = preg_replace('/declare\\s?+\\(\\s?+strict_types\\s?+=\\s?+1\\s?+\\);/is', '', (string)$phpCodeToCache);
-        self::getCacheManager()->getCache('core')->set(self::getExtTablesCacheIdentifier(), $phpCodeToCache);
+        $codeCache->set(self::getExtTablesCacheIdentifier(), $phpCodeToCache);
     }
 
     /**
diff --git a/typo3/sysext/core/Configuration/Services.yaml b/typo3/sysext/core/Configuration/Services.yaml
index d9da02b59dd9..bab3b802b105 100644
--- a/typo3/sysext/core/Configuration/Services.yaml
+++ b/typo3/sysext/core/Configuration/Services.yaml
@@ -176,6 +176,12 @@ services:
       - name: softreference.parser
         parserKey: url
 
+
+  TYPO3\CMS\Core\Core\Event\WarmupBaseTcaCache:
+    public: true
+    arguments:
+      $coreCache: '@cache.core'
+
   # @internal
   # This service entry is provided for legacy code that instantiates LanguageService
   # using GeneralUtility::makeInstance instead of the factory methods which itself
@@ -190,10 +196,20 @@ services:
       version: '11.3'
       message: 'Injection/Instantiation of "%service_id%" is deprecated. Please use TYPO3\CMS\Core\Localization\LanguageServiceFactory->create().'
 
+  TYPO3\CMS\Core\Localization\CacheWarmer:
+    tags:
+      - name: event.listener
+        method: 'warmupCaches'
+        event: TYPO3\CMS\Core\Cache\Event\CacheWarmupEvent
+
   TYPO3\CMS\Core\ExpressionLanguage\ProviderConfigurationLoader:
     public: true
     arguments:
       $coreCache: '@cache.core'
+    tags:
+      - name: event.listener
+        method: 'warmupCaches'
+        event: TYPO3\CMS\Core\Cache\Event\CacheWarmupEvent
 
   TYPO3\CMS\Core\Page\AssetRenderer:
     public: true
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-93436-IntroduceCacheWarmupConsoleCommand.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-93436-IntroduceCacheWarmupConsoleCommand.rst
new file mode 100644
index 000000000000..83c815ca3e21
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/master/Feature-93436-IntroduceCacheWarmupConsoleCommand.rst
@@ -0,0 +1,80 @@
+.. include:: ../../Includes.txt
+
+========================================================
+Feature: #93436 - Introduce cache:warmup console command
+========================================================
+
+See :issue:`93436`
+
+Description
+===========
+
+It is now possible to warmup TYPO3 caches using the command line.
+
+The administrator can use the following CLI command:
+
+.. code-block:: bash
+
+   ./typo3/sysext/core/bin/typo3 cache:warmup
+
+Specific cache groups can be defined via the group option.
+The usage is described as this:
+
+ .. code-block:: bash
+
+    cache:warmup [--group <all|system|di|pages|…>]
+
+All available cache groups can be supplied as option. The command defaults to
+warm all available cache groups.
+
+Extensions that register custom caches are encouraged to implement cache warmers
+via :php:`TYPO3\CMS\Core\Cache\Event\CacheWarmupEvent`.
+
+Note: TYPO3 frontend caches will not be warmed by TYPO3 core, such functionality
+could be added by third party extensions with the help of
+:php:`TYPO3\CMS\Core\Cache\Event\CacheWarmupEvent`.
+
+Impact
+======
+
+It is common practice to clear all caches during deployment of TYPO3 instance
+updates. This means that the first request after a deployment usually takes
+a major amount of time and blocks other requests due to cache-locks.
+
+TYPO3 caches can now be warmed during deployment in release preparatory steps in
+symlink based deployment/release proceducres. The enables fast first requests
+with all (or at least system) caches being prepared and warmed.
+
+Caches are often filesystem relevant (filepaths are calculated into cache
+hashes), therefore cache warmup should only be performed on the the live system,
+in the *final* folder of a new release, and ideally before switching
+to that new release (via symlink switch). Note that caches that have be
+pre-created in CI will likely be useless as cache hashes will not match.
+
+To summarize: Cache warmup is to be used during deployment, on the live system
+server, inside the new release folder and before switching the new release live.
+
+Deployment steps are:
+
+ * Release preparation:
+   * git-checkout/rsync your codebase (on CI or on live)
+   * `composer install` (on CI or on live)
+   * `vendor/bin/typo3 cache:warmup --group system` (*only* on the live system)
+ * Change release symlink to the new release folder
+ * Release postparation
+   * Clear only the page related caches (e.g. via database truncate or an
+     upcoming `cache:flush` command)
+
+The conceptional idea is to warmup all file-related caches *before* (symlink)
+switching to a new release and to *only* flush database and frontend (shared)
+caches after the symlink switch. Database warmup could be implemented with
+the help of the :php:`TYPO3\CMS\Core\Cache\Event\CacheWarmupEvent` as an
+additionally functionality by third party extensions.
+
+Note that file-related caches (summarized into the group "system") can safely be
+cleared before doing a release switch, as it is recommended to keep file caches
+per release. In other words, share :file:`var/session`, :file:`var/log`,
+:file:`var/lock` and :file:`var/charset` between releases, but keep
+:file:`var/cache` be associated only with one release.
+
+.. index:: CLI, ext:core
diff --git a/typo3/sysext/core/Tests/Functional/Command/CacheWarmupCommandTest.php b/typo3/sysext/core/Tests/Functional/Command/CacheWarmupCommandTest.php
new file mode 100644
index 000000000000..fa64130c0084
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Command/CacheWarmupCommandTest.php
@@ -0,0 +1,120 @@
+<?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\Functional\Command;
+
+use TYPO3\CMS\Core\Core\Environment;
+use TYPO3\CMS\Core\DependencyInjection\ContainerBuilder;
+use TYPO3\CMS\Core\Package\PackageManager;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
+
+/**
+ * Test case
+ */
+class CacheWarmupCommandTest extends FunctionalTestCase
+{
+    /**
+     * @test
+     */
+    public function cachesCanBeWarmed()
+    {
+        $containerBuilder = $this->getContainer()->get(ContainerBuilder::class);
+        $packageManager = $this->getContainer()->get(PackageManager::class);
+        $diCacheIdentifier = $containerBuilder->getCacheIdentifier($packageManager);
+
+        GeneralUtility::rmdir(Environment::getVarPath() . '/cache/', true);
+        $result = $this->executeConsoleCommand('cache:warmup');
+
+        self::assertEquals(0, $result['status']);
+        self::assertFileExists(Environment::getVarPath() . '/cache/code/di/' . $diCacheIdentifier . '.php');
+        self::assertFileExists(Environment::getVarPath() . '/cache/code/core/sites-configuration.php');
+    }
+
+    /**
+     * @test
+     */
+    public function systemCachesCanBeWarmed()
+    {
+        $containerBuilder = $this->getContainer()->get(ContainerBuilder::class);
+        $packageManager = $this->getContainer()->get(PackageManager::class);
+        $diCacheIdentifier = $containerBuilder->getCacheIdentifier($packageManager);
+
+        GeneralUtility::rmdir(Environment::getVarPath() . '/cache/', true);
+        $result = $this->executeConsoleCommand('cache:warmup --group %s', 'system');
+
+        self::assertEquals(0, $result['status']);
+        self::assertFileExists(Environment::getVarPath() . '/cache/code/di/' . $diCacheIdentifier . '.php');
+        self::assertFileExists(Environment::getVarPath() . '/cache/code/core/sites-configuration.php');
+    }
+
+    /**
+     * @test
+     */
+    public function diCachesDoesNotWarmSystemCaches()
+    {
+        $containerBuilder = $this->getContainer()->get(ContainerBuilder::class);
+        $packageManager = $this->getContainer()->get(PackageManager::class);
+        $diCacheIdentifier = $containerBuilder->getCacheIdentifier($packageManager);
+
+        GeneralUtility::rmdir(Environment::getVarPath() . '/cache/', true);
+        $result = $this->executeConsoleCommand('cache:warmup -g %s', 'di');
+
+        self::assertEquals(0, $result['status']);
+        self::assertFileExists(Environment::getVarPath() . '/cache/code/di/' . $diCacheIdentifier . '.php');
+        self::assertFileDoesNotExist(Environment::getVarPath() . '/cache/code/core/sites-configuration.php');
+    }
+
+    /**
+     * @test
+     */
+    public function systemCachesCanBeWarmedIfCacheIsBroken()
+    {
+        $containerBuilder = $this->getContainer()->get(ContainerBuilder::class);
+        $packageManager = $this->getContainer()->get(PackageManager::class);
+        $diCacheIdentifier = $containerBuilder->getCacheIdentifier($packageManager);
+
+        GeneralUtility::mkdir_deep(Environment::getVarPath() . '/cache/code/di');
+        file_put_contents(
+            Environment::getVarPath() . '/cache/code/di/' . $diCacheIdentifier . '.php',
+            'invalid php code'
+        );
+
+        $result = $this->executeConsoleCommand('cache:warmup --group %s', 'system');
+
+        self::assertEquals(0, $result['status']);
+        self::assertFileExists(Environment::getVarPath() . '/cache/code/di/' . $diCacheIdentifier . '.php');
+    }
+
+    private function executeConsoleCommand(string $cmdline, ...$args): array
+    {
+        $cmd = vsprintf(PHP_BINARY . ' ' . GeneralUtility::getFileAbsFileName('EXT:core/bin/typo3') . ' ' . $cmdline, array_map('escapeshellarg', $args));
+
+        $output = '';
+
+        $handle = popen($cmd, 'r');
+        while (!feof($handle)) {
+            $output .= fgets($handle, 4096);
+        }
+        $status = pclose($handle);
+
+        return [
+            'status' => $status,
+            'output' => $output
+        ];
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Utility/AccessibleProxies/ExtensionManagementUtilityAccessibleProxy.php b/typo3/sysext/core/Tests/Unit/Utility/AccessibleProxies/ExtensionManagementUtilityAccessibleProxy.php
index 58ce2bdf8c23..5511f35d0d45 100644
--- a/typo3/sysext/core/Tests/Unit/Utility/AccessibleProxies/ExtensionManagementUtilityAccessibleProxy.php
+++ b/typo3/sysext/core/Tests/Unit/Utility/AccessibleProxies/ExtensionManagementUtilityAccessibleProxy.php
@@ -18,7 +18,6 @@ declare(strict_types=1);
 namespace TYPO3\CMS\Core\Tests\Unit\Utility\AccessibleProxies;
 
 use TYPO3\CMS\Core\Cache\CacheManager;
-use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 
 /**
@@ -56,16 +55,6 @@ class ExtensionManagementUtilityAccessibleProxy extends ExtensionManagementUtili
         self::$extTablesWasReadFromCacheOnce = false;
     }
 
-    public static function createExtLocalconfCacheEntry(FrontendInterface $cache)
-    {
-        parent::createExtLocalconfCacheEntry($cache);
-    }
-
-    public static function createExtTablesCacheEntry()
-    {
-        parent::createExtTablesCacheEntry();
-    }
-
     public static function getExtTablesCacheIdentifier()
     {
         return parent::getExtTablesCacheIdentifier();
diff --git a/typo3/sysext/core/Tests/Unit/Utility/ExtensionManagementUtilityTest.php b/typo3/sysext/core/Tests/Unit/Utility/ExtensionManagementUtilityTest.php
index 6dbeac970a61..2dfb001fc0ad 100644
--- a/typo3/sysext/core/Tests/Unit/Utility/ExtensionManagementUtilityTest.php
+++ b/typo3/sysext/core/Tests/Unit/Utility/ExtensionManagementUtilityTest.php
@@ -1600,14 +1600,8 @@ class ExtensionManagementUtilityTest extends UnitTestCase
             ->getMock();
         $packageManager->setPackageCache(new PackageStatesPackageCache('vfs://Test/Configuration/PackageStates.php', $mockCache));
 
-        /** @var CacheManager|\PHPUnit\Framework\MockObject\MockObject $mockCacheManager */
-        $mockCacheManager = $this->getMockBuilder(CacheManager::class)
-            ->onlyMethods(['getCache'])
-            ->getMock();
-        $mockCacheManager->expects(self::any())->method('getCache')->willReturn($mockCache);
-        ExtensionManagementUtilityAccessibleProxy::setCacheManager($mockCacheManager);
         $mockCache->expects(self::once())->method('set')->with(self::anything(), self::stringContains($uniqueStringInTables), self::anything());
-        ExtensionManagementUtilityAccessibleProxy::createExtTablesCacheEntry();
+        ExtensionManagementUtilityAccessibleProxy::createExtTablesCacheEntry($mockCache);
     }
 
     /**
@@ -1624,16 +1618,10 @@ class ExtensionManagementUtilityTest extends UnitTestCase
             ->getMock();
         $packageManager->setPackageCache(new PackageStatesPackageCache('vfs://Test/Configuration/PackageStates.php', $mockCache));
 
-        /** @var CacheManager|\PHPUnit\Framework\MockObject\MockObject $mockCacheManager */
-        $mockCacheManager = $this->getMockBuilder(CacheManager::class)
-            ->onlyMethods(['getCache'])
-            ->getMock();
-        $mockCacheManager->expects(self::any())->method('getCache')->willReturn($mockCache);
-        ExtensionManagementUtilityAccessibleProxy::setCacheManager($mockCacheManager);
         $mockCache->expects(self::once())
             ->method('set')
             ->with(self::anything(), self::logicalNot(self::stringContains($extensionName)), self::anything());
-        ExtensionManagementUtilityAccessibleProxy::createExtTablesCacheEntry();
+        ExtensionManagementUtilityAccessibleProxy::createExtTablesCacheEntry($mockCache);
     }
 
     /**
@@ -1646,17 +1634,11 @@ class ExtensionManagementUtilityTest extends UnitTestCase
             ->disableOriginalConstructor()
             ->getMock();
 
-        /** @var CacheManager|\PHPUnit\Framework\MockObject\MockObject $mockCacheManager */
-        $mockCacheManager = $this->getMockBuilder(CacheManager::class)
-            ->onlyMethods(['getCache'])
-            ->getMock();
-        $mockCacheManager->expects(self::any())->method('getCache')->willReturn($mockCache);
-        ExtensionManagementUtilityAccessibleProxy::setCacheManager($mockCacheManager);
         $mockCache->expects(self::once())->method('set')->with(self::anything(), self::anything(), self::equalTo([]));
         $packageManager = $this->createMockPackageManagerWithMockPackage(StringUtility::getUniqueId());
         $packageManager->setPackageCache(new PackageStatesPackageCache('vfs://Test/Configuration/PackageStates.php', $mockCache));
         ExtensionManagementUtility::setPackageManager($packageManager);
-        ExtensionManagementUtilityAccessibleProxy::createExtTablesCacheEntry();
+        ExtensionManagementUtilityAccessibleProxy::createExtTablesCacheEntry($mockCache);
     }
 
     /////////////////////////////////////////
diff --git a/typo3/sysext/dashboard/Classes/ServiceProvider.php b/typo3/sysext/dashboard/Classes/ServiceProvider.php
index 0c58351973fe..780da4c05265 100644
--- a/typo3/sysext/dashboard/Classes/ServiceProvider.php
+++ b/typo3/sysext/dashboard/Classes/ServiceProvider.php
@@ -19,7 +19,9 @@ namespace TYPO3\CMS\Dashboard;
 
 use ArrayObject;
 use Psr\Container\ContainerInterface;
+use TYPO3\CMS\Core\Cache\Event\CacheWarmupEvent;
 use TYPO3\CMS\Core\Core\Environment;
+use TYPO3\CMS\Core\EventDispatcher\ListenerProvider;
 use TYPO3\CMS\Core\Information\Typo3Version;
 use TYPO3\CMS\Core\Package\AbstractServiceProvider;
 use TYPO3\CMS\Core\Package\PackageManager;
@@ -48,6 +50,7 @@ class ServiceProvider extends AbstractServiceProvider
             'dashboard.presets' => [ static::class, 'getDashboardPresets' ],
             'dashboard.widgetGroups' => [ static::class, 'getWidgetGroups' ],
             'dashboard.widgets' => [ static::class, 'getWidgets' ],
+            'dashboard.configuration.warmer' => [ static::class, 'getConfigurationWarmer' ],
         ];
     }
 
@@ -55,6 +58,7 @@ class ServiceProvider extends AbstractServiceProvider
     {
         return [
             DashboardPresetRegistry::class => [ static::class, 'configureDashboardPresetRegistry' ],
+            ListenerProvider::class => [ static::class, 'addEventListeners' ],
             WidgetGroupRegistry::class => [ static::class, 'configureWidgetGroupRegistry' ],
             'dashboard.presets' => [ static::class, 'configureDashboardPresets' ],
             'dashboard.widgetGroups' => [ static::class, 'configureWidgetGroups' ],
@@ -77,6 +81,11 @@ class ServiceProvider extends AbstractServiceProvider
         return new ArrayObject();
     }
 
+    private static function getCacheIdentifier($type): string
+    {
+        return 'Dashboard_' . $type . '_' . sha1((string)(new Typo3Version()) . Environment::getProjectPath() . $type);
+    }
+
     public static function configureDashboardPresetRegistry(
         ContainerInterface $container,
         DashboardPresetRegistry $dashboardPresetRegistry = null
@@ -84,7 +93,7 @@ class ServiceProvider extends AbstractServiceProvider
         $dashboardPresetRegistry = $dashboardPresetRegistry ?? self::new($container, DashboardPresetRegistry::class);
         $cache = $container->get('cache.core');
 
-        $cacheIdentifier = 'Dashboard_' . sha1((string)(new Typo3Version()) . Environment::getProjectPath() . 'DashboardPresets');
+        $cacheIdentifier = self::getCacheIdentifier('Presets');
         if ($cache->has($cacheIdentifier)) {
             $dashboardPresetsFromPackages = $cache->require($cacheIdentifier);
         } else {
@@ -114,7 +123,7 @@ class ServiceProvider extends AbstractServiceProvider
         $widgetGroupRegistry = $widgetGroupRegistry ?? self::new($container, WidgetGroupRegistry::class);
         $cache = $container->get('cache.core');
 
-        $cacheIdentifier = 'Dashboard_' . sha1((string)(new Typo3Version()) . Environment::getProjectPath() . 'WidgetGroups');
+        $cacheIdentifier = self::getCacheIdentifier('WidgetGroups');
         if ($cache->has($cacheIdentifier)) {
             $widgetGroupsFromPackages = $cache->require($cacheIdentifier);
         } else {
@@ -210,4 +219,28 @@ class ServiceProvider extends AbstractServiceProvider
 
         return $paths;
     }
+
+    public static function getConfigurationWarmer(ContainerInterface $container): \Closure
+    {
+        $presetsCacheIdentifier = self::getCacheIdentifier('Presets');
+        $widgetGroupsCacheIdentifier = self::getCacheIdentifier('WidgetGroups');
+        return function (CacheWarmupEvent $event) use ($container, $presetsCacheIdentifier, $widgetGroupsCacheIdentifier) {
+            if ($event->hasGroup('system')) {
+                $cache = $container->get('cache.core');
+
+                $dashboardPresetsFromPackages = $container->get('dashboard.presets')->getArrayCopy();
+                $cache->set($presetsCacheIdentifier, 'return ' . var_export($dashboardPresetsFromPackages, true) . ';');
+
+                $widgetGroupsFromPackages = $container->get('dashboard.widgetGroups')->getArrayCopy();
+                $cache->set($widgetGroupsCacheIdentifier, 'return ' . var_export($widgetGroupsFromPackages, true) . ';');
+            }
+        };
+    }
+
+    public static function addEventListeners(ContainerInterface $container, ListenerProvider $listenerProvider): ListenerProvider
+    {
+        $listenerProvider->addListener(CacheWarmupEvent::class, 'dashboard.configuration.warmer');
+
+        return $listenerProvider;
+    }
 }
-- 
GitLab