diff --git a/typo3/sysext/backend/Classes/ServiceProvider.php b/typo3/sysext/backend/Classes/ServiceProvider.php index 349910b1d5c920c5afa31b41e94ca980d5f07484..791c2b4fb0aaf09d5515e5a49ae110f87d16b46e 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 643c17ae32856ad0da920bf682c4ad2b22929438..1bdc965c1b8f5831957a36fe8032842abe4bb8a6 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 0000000000000000000000000000000000000000..25300bb4d5339f45ee8266c47c88d372b1c1dd45 --- /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 0000000000000000000000000000000000000000..4f6a09bfb982ce6cbf62f3451676cd9f2c4c2f18 --- /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 340d2fc4d8d291d5627a332a832334fdae4f5dba..40be8d049fc460364fe1c0ec492025fb8ea9538f 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 14690ac5ab3553d149fe6ecfd67a1a5a0ace273b..520a46889ed8fe08e32bbbd7570c38b0d43c24d2 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 0000000000000000000000000000000000000000..c5f5d2f883322d8088a4545d9d831633c02f63a5 --- /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 040f679bce1043c7fc759d57c4ef8169520a5f14..53bea6a9cfdbf576f3a25310add69fe4cd90de68 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 460cef851a13c5b17b554a130e18988fc891c6f9..30a4a830ac11e90acc6eb37c024fbad8580cd0df 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 5823857c361094eff576d80eecfeaa60f1342502..a6277b2919c2ba73e0a055576292dd965e86208e 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 a1b001a0ef2cfaa9f26dab33c8058ae7ebe9824f..a65f9389102f8d3d7006f0d1a295a52cfbe69d20 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 0000000000000000000000000000000000000000..b3360e6057a7a2ffff3422a7f2bedc51bc5a3760 --- /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 990825024fa3ee5ec87167de07d6a8ad8ab532ca..adf8723f520ce73fd3517984bd33ae3846c4e10e 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 23f6af4796ee237c16be933776679a047510912c..6806dac601484a3cb13b8a7e44b770cdfce40453 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 fed41fcfa926251891c28347fa00ee46811d4d69..cbdd1e1203156043181bc177478a928fd4b477ba 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 d9da02b59dd9c2200709011a826655d903c1ecc7..bab3b802b10590663d83f2f9120253b2304999e1 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 0000000000000000000000000000000000000000..83c815ca3e21e7e1d63ee566fe14cd8d624d54cf --- /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 0000000000000000000000000000000000000000..fa64130c0084fb729faceb7e88ac1ad4e837806d --- /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 58ce2bdf8c23bca40673c94e5f0bfbaf33edf46d..5511f35d0d45dbfe0cddb2dd6c8b34cdf36a2382 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 6dbeac970a61ca734ba26810e14e3b0c3a9cccec..2dfb001fc0ada8be5b47de9f12322875ad3a0cf2 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 0c58351973fef8f53d69d573a8c3e79ce07118d6..780da4c052658f565b60e930d53076bf1fbb291c 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; + } }