From 3500a5cac1ec3b5e15033ce22b055d3cbce4dd34 Mon Sep 17 00:00:00 2001 From: Benjamin Franzke <bfr@qbus.de> Date: Tue, 22 Dec 2020 14:04:31 +0100 Subject: [PATCH] [TASK] Refactor low level console commands to avoid full boot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This will allow for low level commands like cache:warmup (or flush) to be implemented. Commands that're registered in (internal!) service providers are now defined to be lowlevel. That allows to change behaviour for internal commands only, without being breaking. Low level commands… * will only be defined by TYPO3 core, not by third party extensions or libraries (despite by using internal interfaces) * will not use any $GLOBALS in constructor * will only inject dependencies defined by service providers * must not require a database connection during construction * will only be defined in internal service providers * may use the internal BootService to bootstrap to a certain boot level Regular commands… * are allowed to access TYPO3_CONF_VARS in constructor * are allowed to inject any service via constructor * are allowed to access TCA in constructor * may perform database calls in constructors (although this is not recommended) * may fail during construction, e.g. because of a stale DI container (an upcoming low level cache:flush/warmup command will be provided for such scenarios) The command list `bin/typo3 list`… * will show all low level commands * will degrade to low level only commands if the DI container is not loadable The upgrade command therefore now runs fully uncached and does not require a full boot upfront. The symfony command `help` is treated as non low level command, as command constructors need to be executed in order for arguments to be configured and available for help output. Therefore a full boot is required. This causes the symfony DI container, ext_localconf and ext_tables to be loaded. That means: An invalid container cache will cause help to fail, `list` will degrade do lowlevel commands, but an (upcoming) low level console command cache:flush/warmup will still be invokable and is intended for scenarios where 3rd party code is changed and therefore DI/localconf/TCA cache became stale. `typo3/cms-cli`, which provides vendor/bin/typo3 in composer mode, is adapted in: https://github.com/TYPO3/cms-cli/pull/5 Commands used for updating typo3/cms-cli: composer require typo3/cms-cli:^3.0 composer require typo3/cms-cli:^3.0 \ --no-update --working-dir=typo3/sysext/core Resolves: #86248 Related: #93174 Releases: master Change-Id: Ie7cfb73983d96ed67532570be4099a25d106db28 Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/67241 Tested-by: TYPO3com <noreply@typo3.com> Tested-by: core-ci <typo3@b13.com> Tested-by: Helmut Hummel <typo3@helhum.io> Tested-by: Benni Mack <benni@typo3.org> Reviewed-by: Helmut Hummel <typo3@helhum.io> Reviewed-by: Benni Mack <benni@typo3.org> --- composer.json | 2 +- composer.lock | 22 +-- .../Command/Descriptor/TextDescriptor.php | 12 +- .../core/Classes/Command/ListCommand.php | 22 ++- .../Classes/Console/CommandApplication.php | 89 ++++++++-- .../sysext/core/Classes/Core/BootService.php | 155 ++++++++++++++++++ .../DependencyInjection/ContainerBuilder.php | 3 + typo3/sysext/core/Classes/ServiceProvider.php | 20 ++- .../sysext/core/Resources/Private/Php/cli.php | 2 +- typo3/sysext/core/composer.json | 2 +- .../Command/UpgradeWizardListCommand.php | 2 +- .../Command/UpgradeWizardRunCommand.php | 2 +- .../Classes/Service/LateBootService.php | 130 +-------------- 13 files changed, 298 insertions(+), 165 deletions(-) create mode 100644 typo3/sysext/core/Classes/Core/BootService.php diff --git a/composer.json b/composer.json index 3f791a9740c7..2e8cec9d3205 100644 --- a/composer.json +++ b/composer.json @@ -75,7 +75,7 @@ "symfony/var-dumper": "^5.2", "symfony/yaml": "^5.2", "typo3/class-alias-loader": "^1.0", - "typo3/cms-cli": "^2.0", + "typo3/cms-cli": "^3.0", "typo3/cms-composer-installers": "^2.0 || ^3.0", "typo3/phar-stream-wrapper": "^3.1.6", "typo3/symfony-psr-event-dispatcher-adapter": "^1.0 || ^2.0", diff --git a/composer.lock b/composer.lock index ecdaeff89dac..22157f65aa37 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b4c3074ccc16c2e62a19f8ccba3f0998", + "content-hash": "ecf8365aa1212f8dec9609b56adab0f2", "packages": [ { "name": "bacon/bacon-qr-code", @@ -4373,16 +4373,16 @@ }, { "name": "typo3/cms-cli", - "version": "2.0.0", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/TYPO3/cms-cli.git", - "reference": "215a0bf5c446b4e0b20f4562bbaf3d6215a5ee82" + "reference": "bfb13f4ab6a505104662b79c18108f41c48e9288" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3/cms-cli/zipball/215a0bf5c446b4e0b20f4562bbaf3d6215a5ee82", - "reference": "215a0bf5c446b4e0b20f4562bbaf3d6215a5ee82", + "url": "https://api.github.com/repos/TYPO3/cms-cli/zipball/bfb13f4ab6a505104662b79c18108f41c48e9288", + "reference": "bfb13f4ab6a505104662b79c18108f41c48e9288", "shasum": "" }, "require": { @@ -4400,9 +4400,9 @@ "homepage": "https://typo3.org", "support": { "issues": "https://github.com/TYPO3/cms-cli/issues", - "source": "https://github.com/TYPO3/cms-cli/tree/master" + "source": "https://github.com/TYPO3/cms-cli/tree/3.0.0" }, - "time": "2018-03-08T20:16:43+00:00" + "time": "2021-02-09T12:45:27+00:00" }, { "name": "typo3/cms-composer-installers", @@ -4622,12 +4622,12 @@ "version": "1.9.1", "source": { "type": "git", - "url": "https://github.com/webmozart/assert.git", + "url": "https://github.com/webmozarts/assert.git", "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389", "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389", "shasum": "" }, @@ -4665,8 +4665,8 @@ "validate" ], "support": { - "issues": "https://github.com/webmozart/assert/issues", - "source": "https://github.com/webmozart/assert/tree/master" + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.9.1" }, "time": "2020-07-08T17:02:28+00:00" } diff --git a/typo3/sysext/core/Classes/Command/Descriptor/TextDescriptor.php b/typo3/sysext/core/Classes/Command/Descriptor/TextDescriptor.php index 8d56392e76e3..62b9d8ed2c73 100644 --- a/typo3/sysext/core/Classes/Command/Descriptor/TextDescriptor.php +++ b/typo3/sysext/core/Classes/Command/Descriptor/TextDescriptor.php @@ -32,10 +32,12 @@ use TYPO3\CMS\Core\Console\CommandRegistry; class TextDescriptor extends SymfonyTextDescriptor { private CommandRegistry $commandRegistry; + private bool $degraded; - public function __construct(CommandRegistry $commandRegistry) + public function __construct(CommandRegistry $commandRegistry, bool $degraded) { $this->commandRegistry = $commandRegistry; + $this->degraded = $degraded; } /** @@ -57,6 +59,10 @@ class TextDescriptor extends SymfonyTextDescriptor return; } + if ($this->degraded) { + $this->write("<error>Failed to boot dependency injection, only lowlevel commands are available.</error>\n\n", true); + } + $namespaces = $this->commandRegistry->getNamespaces(); $help = $application->getHelp(); if ($help !== '') { @@ -89,6 +95,10 @@ class TextDescriptor extends SymfonyTextDescriptor } $this->write("\n"); + + if ($this->degraded) { + $this->write("\n<error>Failed to boot dependency injection, only lowlevel commands are available.</error>\n", true); + } } private function describeNamespace(array $namespace, array $commands, int $width): void diff --git a/typo3/sysext/core/Classes/Command/ListCommand.php b/typo3/sysext/core/Classes/Command/ListCommand.php index 193afc6ca7bf..887305c5f1f2 100644 --- a/typo3/sysext/core/Classes/Command/ListCommand.php +++ b/typo3/sysext/core/Classes/Command/ListCommand.php @@ -17,23 +17,27 @@ declare(strict_types=1); namespace TYPO3\CMS\Core\Command; +use Psr\Container\ContainerInterface; use Symfony\Component\Console\Command\ListCommand as SymfonyListCommand; use Symfony\Component\Console\Helper\DescriptorHelper; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use TYPO3\CMS\Core\Command\Descriptor\TextDescriptor; use TYPO3\CMS\Core\Console\CommandRegistry; +use TYPO3\CMS\Core\Core\BootService; /** * ListCommand displays the list of all available commands for the application. */ class ListCommand extends SymfonyListCommand { - protected CommandRegistry $commandRegistry; + protected ContainerInterface $failsafeContainer; + protected BootService $bootService; - public function __construct(CommandRegistry $commandRegistry) + public function __construct(ContainerInterface $failsafeContainer, BootService $bootService) { - $this->commandRegistry = $commandRegistry; + $this->failsafeContainer = $failsafeContainer; + $this->bootService = $bootService; parent::__construct(); } @@ -42,8 +46,18 @@ class ListCommand extends SymfonyListCommand */ protected function execute(InputInterface $input, OutputInterface $output) { + $degraded = false; + try { + $container = $this->bootService->getContainer(); + } catch (\Throwable $e) { + $container = $this->failsafeContainer; + $degraded = true; + } + + $commandRegistry = $container->get(CommandRegistry::class); + $helper = new DescriptorHelper(); - $helper->register('txt', new TextDescriptor($this->commandRegistry)); + $helper->register('txt', new TextDescriptor($commandRegistry, $degraded)); $helper->describe($output, $this->getApplication(), [ 'format' => $input->getOption('format'), 'raw_text' => $input->getOption('raw'), diff --git a/typo3/sysext/core/Classes/Console/CommandApplication.php b/typo3/sysext/core/Classes/Console/CommandApplication.php index 3c93507f10b5..0a7ea3f43e10 100644 --- a/typo3/sysext/core/Classes/Console/CommandApplication.php +++ b/typo3/sysext/core/Classes/Console/CommandApplication.php @@ -17,15 +17,18 @@ namespace TYPO3\CMS\Core\Console; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Exception\ExceptionInterface; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Output\ConsoleOutput; use TYPO3\CMS\Core\Authentication\CommandLineUserAuthentication; +use TYPO3\CMS\Core\Configuration\ConfigurationManager; use TYPO3\CMS\Core\Context\Context; use TYPO3\CMS\Core\Context\DateTimeAspect; use TYPO3\CMS\Core\Context\UserAspect; use TYPO3\CMS\Core\Context\VisibilityAspect; use TYPO3\CMS\Core\Context\WorkspaceAspect; use TYPO3\CMS\Core\Core\ApplicationInterface; +use TYPO3\CMS\Core\Core\BootService; use TYPO3\CMS\Core\Core\Bootstrap; use TYPO3\CMS\Core\Core\Environment; use TYPO3\CMS\Core\Information\Typo3Version; @@ -37,26 +40,27 @@ use TYPO3\CMS\Core\Localization\LanguageService; */ class CommandApplication implements ApplicationInterface { - /** - * @var Context - */ - protected $context; + protected Context $context; - /** - * @var CommandRegistry - */ - protected $commandRegistry; + protected CommandRegistry $commandRegistry; - /** - * Instance of the symfony application - * @var Application - */ - protected $application; + protected ConfigurationManager $configurationManager; - public function __construct(Context $context, CommandRegistry $commandRegistry) - { + protected BootService $bootService; + + protected Application $application; + + public function __construct( + Context $context, + CommandRegistry $commandRegistry, + ConfigurationManager $configurationMananger, + BootService $bootService + ) { $this->context = $context; $this->commandRegistry = $commandRegistry; + $this->configurationManager = $configurationMananger; + $this->bootService = $bootService; + $this->checkEnvironmentOrDie(); $this->application = new Application('TYPO3 CMS', sprintf( '%s (Application Context: <comment>%s</comment>)', @@ -76,12 +80,31 @@ class CommandApplication implements ApplicationInterface */ public function run(callable $execute = null) { - $this->initializeContext(); - $input = new ArgvInput(); $output = new ConsoleOutput(); - Bootstrap::loadExtTables(); + $commandName = $this->getCommandName($input); + if ($this->wantsFullBoot($commandName)) { + // Do a full boot if command is not a low-level command + $container = $this->bootService->getContainer(); + $this->application->setCommandLoader($container->get(CommandRegistry::class)); + $this->context = $container->get(Context::class); + + $isLowLevelCommandShortcut = false; + try { + $realName = $this->application->find($commandName)->getName(); + // Do not load ext_localconf if a low level command was found + // due to using a shortcut + $isLowLevelCommandShortcut = !$this->wantsFullBoot($realName); + } catch (ExceptionInterface $e) { + // Errors must be ignored, full binding/validation happens later when the console application runs. + } + if (!$isLowLevelCommandShortcut && $this->essentialConfigurationExists()) { + $this->bootService->loadExtLocalconfDatabaseAndExtTables(); + } + } + + $this->initializeContext(); // create the BE_USER object (not logged in yet) Bootstrap::initializeBackendUser(CommandLineUserAuthentication::class); $GLOBALS['LANG'] = LanguageService::createFromUserPreferences($GLOBALS['BE_USER']); @@ -97,6 +120,36 @@ class CommandApplication implements ApplicationInterface exit($exitCode); } + protected function wantsFullBoot(string $commandName): bool + { + if ($commandName === 'help') { + return true; + } + return !$this->commandRegistry->has($commandName); + } + + protected function getCommandName(ArgvInput $input): string + { + try { + $input->bind($this->application->getDefinition()); + } catch (ExceptionInterface $e) { + // Errors must be ignored, full binding/validation happens later when the console application runs. + } + + return $input->getFirstArgument() ?? 'list'; + } + + /** + * Check if LocalConfiguration.php and PackageStates.php exist + * + * @return bool TRUE when the essential configuration is available, otherwise FALSE + */ + protected function essentialConfigurationExists(): bool + { + return file_exists($this->configurationManager->getLocalConfigurationFileLocation()) + && file_exists(Environment::getLegacyConfigPath() . '/PackageStates.php'); + } + /** * Check the script is called from a cli environment. */ diff --git a/typo3/sysext/core/Classes/Core/BootService.php b/typo3/sysext/core/Classes/Core/BootService.php new file mode 100644 index 000000000000..2501940781e1 --- /dev/null +++ b/typo3/sysext/core/Classes/Core/BootService.php @@ -0,0 +1,155 @@ +<?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; + +use Psr\Container\ContainerInterface; +use Psr\EventDispatcher\EventDispatcherInterface; +use TYPO3\CMS\Core\DependencyInjection\ContainerBuilder; +use TYPO3\CMS\Core\Imaging\IconRegistry; +use TYPO3\CMS\Core\Package\PackageManager; +use TYPO3\CMS\Core\Page\PageRenderer; +use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; +use TYPO3\CMS\Core\Utility\GeneralUtility; + +/** + * @internal This is NOT an API class, it is for internal use in TYPO3 core only. + */ +class BootService +{ + private ContainerBuilder $containerBuilder; + + private ContainerInterface $failsafeContainer; + + private ?ContainerInterface $container = null; + + public function __construct(ContainerBuilder $containerBuilder, ContainerInterface $failsafeContainer) + { + $this->containerBuilder = $containerBuilder; + $this->failsafeContainer = $failsafeContainer; + } + + public function getContainer(bool $allowCaching = true): ContainerInterface + { + return $this->container ?? $this->prepareContainer($allowCaching); + } + + private function prepareContainer(bool $allowCaching = true): ContainerInterface + { + $packageManager = $this->failsafeContainer->get(PackageManager::class); + $dependencyInjectionContainerCache = $this->failsafeContainer->get('cache.di'); + + $failsafe = false; + + // Build a non-failsafe container which is required for loading ext_localconf + $this->container = $this->containerBuilder->createDependencyInjectionContainer($packageManager, $dependencyInjectionContainerCache, $failsafe); + $this->container->set('_early.boot-service', $this); + if ($allowCaching) { + $this->container->get('boot.state')->cacheDisabled = false; + $coreCache = Bootstrap::createCache('core'); + // Core cache is initialized with a NullBackend in failsafe mode. + // Replace it with a new cache that uses the real backend. + $this->container->set('_early.cache.core', $coreCache); + $this->container->set('_early.cache.assets', Bootstrap::createCache('assets')); + $this->container->get(PackageManager::class)->injectCoreCache($coreCache); + } + + return $this->container; + } + + /** + * Switch global context to a new context, or revert + * to the original booting container if no container + * is specified + * + * @param ContainerInterface $container + * @param array $backup + * @return array + */ + public function makeCurrent(ContainerInterface $container = null, array $backup = []): array + { + $container = $container ?? $backup['container'] ?? $this->failsafeContainer; + + $newBackup = [ + 'singletonInstances' => GeneralUtility::getSingletonInstances(), + 'container' => GeneralUtility::getContainer(), + ]; + + GeneralUtility::purgeInstances(); + + // Set global state to the non-failsafe container and it's instances + GeneralUtility::setContainer($container); + ExtensionManagementUtility::setPackageManager($container->get(PackageManager::class)); + + $backupSingletonInstances = $backup['singletonInstances'] ?? []; + foreach ($backupSingletonInstances as $className => $instance) { + GeneralUtility::setSingletonInstance($className, $instance); + } + + return $newBackup; + } + + /** + * Bootstrap a non-failsafe container and load ext_localconf + * + * Use by actions like the database analyzer and the upgrade wizards which + * need additional bootstrap actions performed. + * + * Those actions can potentially fatal if some old extension is loaded that triggers + * a fatal in ext_localconf or ext_tables code! Use only if really needed. + * + * @param bool $resetContainer + * @param bool $allowCaching + * @return ContainerInterface + */ + public function loadExtLocalconfDatabaseAndExtTables(bool $resetContainer = false, bool $allowCaching = true): ContainerInterface + { + $container = $this->getContainer($allowCaching); + + $backup = $this->makeCurrent($container); + $beUserBackup = $GLOBALS['BE_USER'] ?? null; + + $container->get('boot.state')->done = false; + $assetsCache = $container->get('cache.assets'); + IconRegistry::setCache($assetsCache); + PageRenderer::setCache($assetsCache); + $eventDispatcher = $container->get(EventDispatcherInterface::class); + ExtensionManagementUtility::setEventDispatcher($eventDispatcher); + Bootstrap::loadTypo3LoadedExtAndExtLocalconf($allowCaching, $container->get('cache.core')); + Bootstrap::unsetReservedGlobalVariables(); + $GLOBALS['BE_USER'] = $beUserBackup; + $container->get('boot.state')->done = true; + Bootstrap::loadBaseTca($allowCaching); + Bootstrap::loadExtTables($allowCaching); + + if ($resetContainer) { + $this->makeCurrent(null, $backup); + } + + return $container; + } + + public function resetGlobalContainer(): void + { + $this->makeCurrent(null, []); + } + + public function getFailsafeContainer(): ContainerInterface + { + return $this->failsafeContainer; + } +} diff --git a/typo3/sysext/core/Classes/DependencyInjection/ContainerBuilder.php b/typo3/sysext/core/Classes/DependencyInjection/ContainerBuilder.php index 8df0e0ae494d..040f679bce10 100644 --- a/typo3/sysext/core/Classes/DependencyInjection/ContainerBuilder.php +++ b/typo3/sysext/core/Classes/DependencyInjection/ContainerBuilder.php @@ -127,6 +127,9 @@ class ContainerBuilder $containerBuilder->setAlias($id, $syntheticId)->setPublic(true); } + // Optional service, set by BootService as back reference to the original bootService + $containerBuilder->register('_early.boot-service')->setSynthetic(true)->setPublic(true); + $containerBuilder->compile(); return $containerBuilder; diff --git a/typo3/sysext/core/Classes/ServiceProvider.php b/typo3/sysext/core/Classes/ServiceProvider.php index 08fafac75df1..6c8e53c76ff0 100644 --- a/typo3/sysext/core/Classes/ServiceProvider.php +++ b/typo3/sysext/core/Classes/ServiceProvider.php @@ -23,6 +23,7 @@ use Psr\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Console\Command\HelpCommand; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface as SymfonyEventDispatcherInterface; use TYPO3\CMS\Core\Core\Environment; +use TYPO3\CMS\Core\DependencyInjection\ContainerBuilder; use TYPO3\CMS\Core\Package\AbstractServiceProvider; use TYPO3\SymfonyPsrEventDispatcherAdapter\EventDispatcherAdapter as SymfonyEventDispatcher; @@ -49,6 +50,7 @@ class ServiceProvider extends AbstractServiceProvider Console\CommandApplication::class => [ static::class, 'getConsoleCommandApplication' ], Console\CommandRegistry::class => [ static::class, 'getConsoleCommandRegistry' ], Context\Context::class => [ static::class, 'getContext' ], + Core\BootService::class => [ static::class, 'getBootService' ], Crypto\PasswordHashing\PasswordHashFactory::class => [ static::class, 'getPasswordHashFactory' ], EventDispatcher\EventDispatcher::class => [ static::class, 'getEventDispatcher' ], EventDispatcher\ListenerProvider::class => [ static::class, 'getEventListenerProvider' ], @@ -137,7 +139,8 @@ class ServiceProvider extends AbstractServiceProvider public static function getListCommand(ContainerInterface $container): Command\ListCommand { return new Command\ListCommand( - $container->get(Console\CommandRegistry::class) + $container, + $container->get(Core\BootService::class) ); } @@ -155,7 +158,9 @@ class ServiceProvider extends AbstractServiceProvider { return new Console\CommandApplication( $container->get(Context\Context::class), - $container->get(Console\CommandRegistry::class) + $container->get(Console\CommandRegistry::class), + $container->get(Configuration\ConfigurationManager::class), + $container->get(Core\BootService::class) ); } @@ -193,6 +198,17 @@ class ServiceProvider extends AbstractServiceProvider return new Context\Context(); } + public static function getBootService(ContainerInterface $container): Core\BootService + { + if ($container->has('_early.boot-service')) { + return $container->get('_early.boot-service'); + } + return new Core\BootService( + $container->get(ContainerBuilder::class), + $container + ); + } + public static function getPasswordHashFactory(ContainerInterface $container): Crypto\PasswordHashing\PasswordHashFactory { return new Crypto\PasswordHashing\PasswordHashFactory(); diff --git a/typo3/sysext/core/Resources/Private/Php/cli.php b/typo3/sysext/core/Resources/Private/Php/cli.php index 7b9b55ee2e6c..d96be4e832a4 100644 --- a/typo3/sysext/core/Resources/Private/Php/cli.php +++ b/typo3/sysext/core/Resources/Private/Php/cli.php @@ -20,5 +20,5 @@ call_user_func(function () { $classLoader = require __DIR__ . '/../../../../../../vendor/autoload.php'; \TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::run(4, \TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::REQUESTTYPE_CLI); - \TYPO3\CMS\Core\Core\Bootstrap::init($classLoader)->get(\TYPO3\CMS\Core\Console\CommandApplication::class)->run(); + \TYPO3\CMS\Core\Core\Bootstrap::init($classLoader, true)->get(\TYPO3\CMS\Core\Console\CommandApplication::class)->run(); }); diff --git a/typo3/sysext/core/composer.json b/typo3/sysext/core/composer.json index ae5f12cb3d89..bee5276c5284 100644 --- a/typo3/sysext/core/composer.json +++ b/typo3/sysext/core/composer.json @@ -59,7 +59,7 @@ "symfony/routing": "^5.2", "symfony/yaml": "^5.2", "typo3/class-alias-loader": "^1.0", - "typo3/cms-cli": "^2.0", + "typo3/cms-cli": "^3.0", "typo3/cms-composer-installers": "^2.0 || ^3.0", "typo3/phar-stream-wrapper": "^3.1.6", "typo3/symfony-psr-event-dispatcher-adapter": "^1.0 || ^2.0", diff --git a/typo3/sysext/install/Classes/Command/UpgradeWizardListCommand.php b/typo3/sysext/install/Classes/Command/UpgradeWizardListCommand.php index f3e1089accad..6bc57dfe62c2 100644 --- a/typo3/sysext/install/Classes/Command/UpgradeWizardListCommand.php +++ b/typo3/sysext/install/Classes/Command/UpgradeWizardListCommand.php @@ -72,7 +72,7 @@ class UpgradeWizardListCommand extends Command */ protected function bootstrap(): void { - $this->lateBootService->loadExtLocalconfDatabaseAndExtTables(); + $this->lateBootService->loadExtLocalconfDatabaseAndExtTables(false); Bootstrap::initializeBackendUser(CommandLineUserAuthentication::class); Bootstrap::initializeBackendAuthentication(); } diff --git a/typo3/sysext/install/Classes/Command/UpgradeWizardRunCommand.php b/typo3/sysext/install/Classes/Command/UpgradeWizardRunCommand.php index 93ceba4d9393..9e6280637dc5 100644 --- a/typo3/sysext/install/Classes/Command/UpgradeWizardRunCommand.php +++ b/typo3/sysext/install/Classes/Command/UpgradeWizardRunCommand.php @@ -77,7 +77,7 @@ class UpgradeWizardRunCommand extends Command */ protected function bootstrap(): void { - $this->lateBootService->loadExtLocalconfDatabaseAndExtTables(); + $this->lateBootService->loadExtLocalconfDatabaseAndExtTables(false); Bootstrap::initializeBackendUser(CommandLineUserAuthentication::class); Bootstrap::initializeBackendAuthentication(); $this->upgradeWizardsService->isDatabaseCharsetUtf8() ?: $this->upgradeWizardsService->setDatabaseCharsetUtf8(); diff --git a/typo3/sysext/install/Classes/Service/LateBootService.php b/typo3/sysext/install/Classes/Service/LateBootService.php index ac779dfa7ff8..dd44754b6f5c 100644 --- a/typo3/sysext/install/Classes/Service/LateBootService.php +++ b/typo3/sysext/install/Classes/Service/LateBootService.php @@ -18,138 +18,20 @@ declare(strict_types=1); namespace TYPO3\CMS\Install\Service; use Psr\Container\ContainerInterface; -use Psr\EventDispatcher\EventDispatcherInterface; -use TYPO3\CMS\Core\Core\Bootstrap; -use TYPO3\CMS\Core\DependencyInjection\ContainerBuilder; -use TYPO3\CMS\Core\Imaging\IconRegistry; -use TYPO3\CMS\Core\Package\PackageManager; -use TYPO3\CMS\Core\Page\PageRenderer; -use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; -use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Core\Core\BootService; /** * @internal This is NOT an API class, it is for internal use in the install tool only. */ -class LateBootService +class LateBootService extends BootService { - /** - * @var ContainerBuilder - */ - private $containerBuilder; - - /** - * @var ContainerInterface - */ - private $failsafeContainer; - - /** - * @var ContainerInterface - */ - private $container; - - /** - * @param ContainerBuilder $containerBuilder - * @param ContainerInterface $failsafeContainer - */ - public function __construct(ContainerBuilder $containerBuilder, ContainerInterface $failsafeContainer) - { - $this->containerBuilder = $containerBuilder; - $this->failsafeContainer = $failsafeContainer; - } - - /** - * @return ContainerInterface - */ - public function getContainer(): ContainerInterface - { - return $this->container ?? $this->prepareContainer(); - } - - /** - * @return ContainerInterface - */ - private function prepareContainer(): ContainerInterface - { - $packageManager = $this->failsafeContainer->get(PackageManager::class); - $dependencyInjectionContainerCache = $this->failsafeContainer->get('cache.di'); - - $failsafe = false; - - // Build a non-failsafe container which is required for loading ext_localconf - return $this->container = $this->containerBuilder->createDependencyInjectionContainer($packageManager, $dependencyInjectionContainerCache, $failsafe); - } - - /** - * Switch global context to a new context, or revert - * to the original booting container if no container - * is specified - * - * @param ContainerInterface $container - * @param array $backup - * @return array - */ - public function makeCurrent(ContainerInterface $container = null, array $backup = []): array + public function getContainer(bool $allowCaching = false): ContainerInterface { - $container = $container ?? $backup['container'] ?? $this->failsafeContainer; - - $newBackup = [ - 'singletonInstances' => GeneralUtility::getSingletonInstances(), - 'container' => GeneralUtility::getContainer(), - ]; - - GeneralUtility::purgeInstances(); - - // Set global state to the non-failsafe container and it's instances - GeneralUtility::setContainer($container); - ExtensionManagementUtility::setPackageManager($container->get(PackageManager::class)); - - $backupSingletonInstances = $backup['singletonInstances'] ?? []; - foreach ($backupSingletonInstances as $className => $instance) { - GeneralUtility::setSingletonInstance($className, $instance); - } - - return $newBackup; - } - - /** - * Bootstrap a non-failsafe container and load ext_localconf - * - * Use by actions like the database analyzer and the upgrade wizards which - * need additional bootstrap actions performed. - * - * Those actions can potentially fatal if some old extension is loaded that triggers - * a fatal in ext_localconf or ext_tables code! Use only if really needed. - * - * @param bool $resetContainer - * @return ContainerInterface - */ - public function loadExtLocalconfDatabaseAndExtTables(bool $resetContainer = true): ContainerInterface - { - $container = $this->getContainer(); - - $backup = $this->makeCurrent($container); - - $container->get('boot.state')->done = false; - $assetsCache = $container->get('cache.assets'); - IconRegistry::setCache($assetsCache); - PageRenderer::setCache($assetsCache); - $eventDispatcher = $container->get(EventDispatcherInterface::class); - ExtensionManagementUtility::setEventDispatcher($eventDispatcher); - Bootstrap::loadTypo3LoadedExtAndExtLocalconf(false); - Bootstrap::unsetReservedGlobalVariables(); - $container->get('boot.state')->done = true; - Bootstrap::loadBaseTca(false); - Bootstrap::loadExtTables(false); - - if ($resetContainer) { - $this->makeCurrent(null, $backup); - } - - return $container; + return parent::getContainer($allowCaching); } - public function resetGlobalContainer(): void + public function loadExtLocalconfDatabaseAndExtTables(bool $resetContainer = true, bool $allowCaching = false): ContainerInterface { - $this->makeCurrent(null, []); + return parent::loadExtLocalconfDatabaseAndExtTables($resetContainer, $allowCaching); } } -- GitLab