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