From 0360ef3b4a9f34e4441b95f6d9071bb49ae1f432 Mon Sep 17 00:00:00 2001 From: Benjamin Franzke <bfr@qbus.de> Date: Thu, 5 Sep 2019 13:44:09 +0200 Subject: [PATCH] [FEATURE] Add dependency injection support for console commands Transform CommandRegistry into a symfony CommandLoader which allows console commands to be created on demand. That means commands are lazy loaded in order to avoid creating all commands with all their dependencies in every console invocation. Command will now be loaded when they are either executed or when command metadata is required (e.g. for the command listing) The `site:list` command is adapted to make use of dependency injection. Releases: master Resolves: #89139 Change-Id: I64256bf2dc21f0f3fe434aa5dff6176f0fe22233 Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/61630 Tested-by: TYPO3com <noreply@typo3.com> Tested-by: Achim Fritz <af@achimfritz.de> Tested-by: Benni Mack <benni@typo3.org> Reviewed-by: Achim Fritz <af@achimfritz.de> Reviewed-by: Benni Mack <benni@typo3.org> --- .../sysext/backend/Configuration/Commands.php | 20 -- .../backend/Configuration/Services.yaml | 12 ++ .../core/Classes/Command/SiteListCommand.php | 14 +- .../Classes/Console/CommandApplication.php | 15 +- .../core/Classes/Console/CommandRegistry.php | 133 ++++++++++++- .../ConsoleCommandPass.php | 67 +++++++ typo3/sysext/core/Classes/ServiceProvider.php | 11 +- typo3/sysext/core/Configuration/Commands.php | 23 --- typo3/sysext/core/Configuration/Services.php | 1 + typo3/sysext/core/Configuration/Services.yaml | 42 ++++ ...figurationMigratedToSymfonyServiceTags.rst | 73 +++++++ ...encyInjectionSupportForConsoleCommands.rst | 73 +++++++ .../Unit/Console/CommandRegistryTest.php | 126 ++++++------ .../Console/CommandRegistryTest.php | 181 ++++++++++++++++++ .../Configuration/Commands.php | 17 -- .../Configuration/Services.yaml | 28 +++ .../sysext/impexp/Configuration/Commands.php | 12 -- .../sysext/impexp/Configuration/Services.yaml | 5 + .../install/Classes/ServiceProvider.php | 34 ++++ .../sysext/install/Configuration/Commands.php | 20 -- .../Php/MethodCallMatcher.php | 8 + .../lowlevel/Configuration/Commands.php | 33 ---- .../lowlevel/Configuration/Services.yaml | 40 ++++ .../redirects/Configuration/Commands.php | 13 -- .../redirects/Configuration/Services.yaml | 5 + .../scheduler/Configuration/Commands.php | 14 -- .../scheduler/Configuration/Services.yaml | 6 + .../workspaces/Configuration/Commands.php | 18 -- .../workspaces/Configuration/Services.yaml | 15 ++ 29 files changed, 806 insertions(+), 253 deletions(-) delete mode 100644 typo3/sysext/backend/Configuration/Commands.php create mode 100644 typo3/sysext/core/Classes/DependencyInjection/ConsoleCommandPass.php delete mode 100644 typo3/sysext/core/Configuration/Commands.php create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Deprecation-89139-ConsoleCommandsConfigurationMigratedToSymfonyServiceTags.rst create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Feature-89139-AddDependencyInjectionSupportForConsoleCommands.rst create mode 100644 typo3/sysext/core/Tests/UnitDeprecated/Console/CommandRegistryTest.php delete mode 100644 typo3/sysext/extensionmanager/Configuration/Commands.php delete mode 100644 typo3/sysext/impexp/Configuration/Commands.php delete mode 100644 typo3/sysext/install/Configuration/Commands.php delete mode 100644 typo3/sysext/lowlevel/Configuration/Commands.php delete mode 100644 typo3/sysext/redirects/Configuration/Commands.php delete mode 100644 typo3/sysext/scheduler/Configuration/Commands.php delete mode 100644 typo3/sysext/workspaces/Configuration/Commands.php diff --git a/typo3/sysext/backend/Configuration/Commands.php b/typo3/sysext/backend/Configuration/Commands.php deleted file mode 100644 index 7a7a9420e93b..000000000000 --- a/typo3/sysext/backend/Configuration/Commands.php +++ /dev/null @@ -1,20 +0,0 @@ -<?php -/** - * Commands to be executed by typo3, where the key of the array - * is the name of the command (to be called as the first argument after typo3). - * Required parameter is the "class" of the command which needs to be a subclass - * of Symfony/Console/Command. - * - * example: bin/typo3 backend:lock - */ -return [ - 'backend:lock' => [ - 'class' => \TYPO3\CMS\Backend\Command\LockBackendCommand::class - ], - 'backend:unlock' => [ - 'class' => \TYPO3\CMS\Backend\Command\UnlockBackendCommand::class - ], - 'referenceindex:update' => [ - 'class' => \TYPO3\CMS\Backend\Command\ReferenceIndexUpdateCommand::class - ] -]; diff --git a/typo3/sysext/backend/Configuration/Services.yaml b/typo3/sysext/backend/Configuration/Services.yaml index 3dd913461758..14d7049a1805 100644 --- a/typo3/sysext/backend/Configuration/Services.yaml +++ b/typo3/sysext/backend/Configuration/Services.yaml @@ -7,6 +7,18 @@ services: TYPO3\CMS\Backend\: resource: '../Classes/*' + TYPO3\CMS\Backend\Command\LockBackendCommand: + tags: + - { name: 'console.command', command: 'backend:lock' } + + TYPO3\CMS\Backend\Command\UnlockBackendCommand: + tags: + - { name: 'console.command', command: 'backend:unlock' } + + TYPO3\CMS\Backend\Command\ReferenceIndexUpdateCommand: + tags: + - { name: 'console.command', command: 'referenceindex:update' } + # Temporary workaround until testing framework loads EXT:fluid in functional tests # @todo: Fix typo3/testing-framework and remove this TYPO3\CMS\Backend\View\BackendTemplateView: diff --git a/typo3/sysext/core/Classes/Command/SiteListCommand.php b/typo3/sysext/core/Classes/Command/SiteListCommand.php index c0a51d4156e0..c2fd02ea21b8 100644 --- a/typo3/sysext/core/Classes/Command/SiteListCommand.php +++ b/typo3/sysext/core/Classes/Command/SiteListCommand.php @@ -28,6 +28,17 @@ use TYPO3\CMS\Core\Site\SiteFinder; */ class SiteListCommand extends Command { + /** + * @var SiteFinder + */ + protected $siteFinder; + + public function __construct(SiteFinder $siteFinder) + { + $this->siteFinder = $siteFinder; + parent::__construct(); + } + /** * Defines the allowed options for this command */ @@ -44,8 +55,7 @@ class SiteListCommand extends Command protected function execute(InputInterface $input, OutputInterface $output) { $io = new SymfonyStyle($input, $output); - $siteFinder = new SiteFinder(); - $sites = $siteFinder->getAllSites(); + $sites = $this->siteFinder->getAllSites(); if (empty($sites)) { $io->title('No sites configured'); diff --git a/typo3/sysext/core/Classes/Console/CommandApplication.php b/typo3/sysext/core/Classes/Console/CommandApplication.php index 3492572060ba..8b8a86c0dcb1 100644 --- a/typo3/sysext/core/Classes/Console/CommandApplication.php +++ b/typo3/sysext/core/Classes/Console/CommandApplication.php @@ -29,7 +29,6 @@ use TYPO3\CMS\Core\Core\Bootstrap; use TYPO3\CMS\Core\Core\Environment; use TYPO3\CMS\Core\Information\Typo3Version; use TYPO3\CMS\Core\Localization\LanguageService; -use TYPO3\CMS\Core\Utility\GeneralUtility; /** * Entry point for the TYPO3 Command Line for Commands @@ -42,15 +41,21 @@ class CommandApplication implements ApplicationInterface */ protected $context; + /** + * @var CommandRegistry + */ + protected $commandRegistry; + /** * Instance of the symfony application * @var Application */ protected $application; - public function __construct(Context $context) + public function __construct(Context $context, CommandRegistry $commandRegistry) { $this->context = $context; + $this->commandRegistry = $commandRegistry; $this->checkEnvironmentOrDie(); $this->application = new Application('TYPO3 CMS', sprintf( '%s (Application Context: <comment>%s</comment>)', @@ -58,6 +63,7 @@ class CommandApplication implements ApplicationInterface Environment::getContext() )); $this->application->setAutoExit(false); + $this->application->setCommandLoader($commandRegistry); } /** @@ -113,11 +119,12 @@ class CommandApplication implements ApplicationInterface /** * Put all available commands inside the application + * + * Note: This method will be removed in TYPO3 v11 when support for Configuration/Commands.php is dropped. */ protected function populateAvailableCommands(): void { - $commands = GeneralUtility::makeInstance(CommandRegistry::class); - foreach ($commands as $commandName => $command) { + foreach ($this->commandRegistry->getLegacyCommands() as $commandName => $command) { /** @var Command $command */ $this->application->add($command); } diff --git a/typo3/sysext/core/Classes/Console/CommandRegistry.php b/typo3/sysext/core/Classes/Console/CommandRegistry.php index ff60a1561c8c..969a1be4f09e 100644 --- a/typo3/sysext/core/Classes/Console/CommandRegistry.php +++ b/typo3/sysext/core/Classes/Console/CommandRegistry.php @@ -15,7 +15,10 @@ namespace TYPO3\CMS\Core\Console; * The TYPO3 project - inspiring people to share! */ +use Psr\Container\ContainerInterface; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; +use Symfony\Component\Console\Exception\CommandNotFoundException; use TYPO3\CMS\Core\Package\PackageManager; use TYPO3\CMS\Core\SingletonInterface; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -23,17 +26,22 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; /** * Registry for Symfony commands, populated from extensions */ -class CommandRegistry implements \IteratorAggregate, SingletonInterface +class CommandRegistry implements CommandLoaderInterface, \IteratorAggregate, SingletonInterface { /** * @var PackageManager */ protected $packageManager; + /** + * @var ContainerInterface + */ + protected $container; + /** * Map of commands * - * @var Command[] + * @var array */ protected $commands = []; @@ -44,21 +52,68 @@ class CommandRegistry implements \IteratorAggregate, SingletonInterface */ protected $commandConfigurations = []; + /** + * Map of lazy (DI-managed) command configurations with the command name as key + * + * @var array + */ + protected $lazyCommandConfigurations = []; + /** * @param PackageManager $packageManager + * @param ContainerInterface $container + */ + public function __construct(PackageManager $packageManager, ContainerInterface $container) + { + $this->packageManager = $packageManager; + $this->container = $container; + } + + /** + * {@inheritdoc} + */ + public function has($name) + { + $this->populateCommandsFromPackages(); + + return array_key_exists($name, $this->commands); + } + + /** + * {@inheritdoc} */ - public function __construct(PackageManager $packageManager = null) + public function get($name) { - $this->packageManager = $packageManager ?: GeneralUtility::makeInstance(PackageManager::class); + try { + return $this->getCommandByIdentifier($name); + } catch (UnknownCommandException $e) { + throw new CommandNotFoundException($e->getMessage(), [], 1567969355, $e); + } + } + + /** + * {@inheritdoc} + */ + public function getNames() + { + $this->populateCommandsFromPackages(); + + return array_keys($this->commands); } /** * @return \Generator + * @deprecated will be removed in TYPO3 v11.0 when support for Configuration/Commands.php is dropped. */ public function getIterator(): \Generator { + trigger_error('Using ' . self::class . ' as iterable has been deprecated and will stop working in TYPO3 11.0.', E_USER_DEPRECATED); + $this->populateCommandsFromPackages(); foreach ($this->commands as $commandName => $command) { + if (is_string($command)) { + $command = $this->getInstance($command, $commandName); + } yield $commandName => $command; } } @@ -73,11 +128,30 @@ class CommandRegistry implements \IteratorAggregate, SingletonInterface $this->populateCommandsFromPackages(); foreach ($this->commands as $commandName => $command) { if ($this->commandConfigurations[$commandName]['schedulable'] ?? true) { + if (is_string($command)) { + $command = $this->getInstance($command, $commandName); + } yield $commandName => $command; } } } + /** + * @return \Generator + * @internal This method will be removed in TYPO3 v11 when support for Configuration/Commands.php is dropped. + */ + public function getLegacyCommands(): \Generator + { + $this->populateCommandsFromPackages(); + foreach ($this->commands as $commandName => $command) { + // Type string indicates lazy loading + if (is_string($command)) { + continue; + } + yield $commandName => $command; + } + } + /** * @param string $identifier * @throws CommandNameAlreadyInUseException @@ -95,7 +169,12 @@ class CommandRegistry implements \IteratorAggregate, SingletonInterface ); } - return $this->commands[$identifier] ?? null; + $command = $this->commands[$identifier] ?? null; + if (is_string($command)) { + $command = $this->getInstance($command, $identifier); + } + + return $command; } /** @@ -118,6 +197,13 @@ class CommandRegistry implements \IteratorAggregate, SingletonInterface if ($this->commands) { return; } + + foreach ($this->lazyCommandConfigurations as $commandName => $commandConfig) { + // Lazy commands shall be loaded from the Container on demand, store the command as string to indicate lazy loading + $this->commands[$commandName] = $commandConfig['class']; + $this->commandConfigurations[$commandName] = $commandConfig; + } + foreach ($this->packageManager->getActivePackages() as $package) { $commandsOfExtension = $package->getPackagePath() . 'Configuration/Commands.php'; if (@is_file($commandsOfExtension)) { @@ -129,6 +215,14 @@ class CommandRegistry implements \IteratorAggregate, SingletonInterface $commands = require $commandsOfExtension; if (is_array($commands)) { foreach ($commands as $commandName => $commandConfig) { + if (array_key_exists($commandName, $this->lazyCommandConfigurations)) { + // Lazy (DI managed) commands override classic commands from Configuration/Commands.php + // Skip this case to allow extensions to provide commands via DI config and to allow + // TYPO3 v9 backwards compatibile confguration via Configuration/Commands.php. + // Note: Also the deprecation error is skipped on-demand as the extension has been + // adapted and the configuration will be ignored as of TYPO3 v11. + continue; + } if (array_key_exists($commandName, $this->commands)) { throw new CommandNameAlreadyInUseException( 'Command "' . $commandName . '" registered by "' . $package->getPackageKey() . '" is already in use', @@ -137,9 +231,38 @@ class CommandRegistry implements \IteratorAggregate, SingletonInterface } $this->commands[$commandName] = GeneralUtility::makeInstance($commandConfig['class'], $commandName); $this->commandConfigurations[$commandName] = $commandConfig; + + trigger_error( + 'Registering console commands in Configuration/Commands.php has been deprecated and will stop working in TYPO3 v11.0.', + E_USER_DEPRECATED + ); } } } } } + + protected function getInstance(string $class, string $commandName): Command + { + $command = $this->container->get($class); + + if ($command instanceof Command) { + $command->setName($commandName); + return $command; + } + + throw new \InvalidArgumentException('Registered console command class ' . get_class($command) . ' does not inherit from ' . Command::class, 1567966448); + } + + /** + * @internal + */ + public function addLazyCommand(string $commandName, string $serviceName, bool $alias = false, bool $schedulable = true): void + { + $this->lazyCommandConfigurations[$commandName] = [ + 'class' => $serviceName, + 'alias' => $alias, + 'schedulable' => $schedulable, + ]; + } } diff --git a/typo3/sysext/core/Classes/DependencyInjection/ConsoleCommandPass.php b/typo3/sysext/core/Classes/DependencyInjection/ConsoleCommandPass.php new file mode 100644 index 000000000000..f35cc5cd9d4c --- /dev/null +++ b/typo3/sysext/core/Classes/DependencyInjection/ConsoleCommandPass.php @@ -0,0 +1,67 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Core\DependencyInjection; + +/* + * 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! + */ + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use TYPO3\CMS\Core\Console\CommandRegistry; + +/** + * @internal + */ +final class ConsoleCommandPass implements CompilerPassInterface +{ + /** + * @var string + */ + private $tagName; + + /** + * @param string $tagName + */ + public function __construct(string $tagName) + { + $this->tagName = $tagName; + } + + /** + * @param ContainerBuilder $container + */ + public function process(ContainerBuilder $container) + { + $commandRegistryDefinition = $container->findDefinition(CommandRegistry::class); + if (!$commandRegistryDefinition) { + return; + } + + $unorderedEventListeners = []; + foreach ($container->findTaggedServiceIds($this->tagName) as $serviceName => $tags) { + $container->findDefinition($serviceName)->setPublic(true); + foreach ($tags as $attributes) { + if (!isset($attributes['command'])) { + continue; + } + + $commandRegistryDefinition->addMethodCall('addLazyCommand', [ + $attributes['command'], + $serviceName, + (bool)($attributes['alias'] ?? false), + (bool)($attributes['schedulable'] ?? true) + ]); + } + } + } +} diff --git a/typo3/sysext/core/Classes/ServiceProvider.php b/typo3/sysext/core/Classes/ServiceProvider.php index 481067386562..959b20feaa44 100644 --- a/typo3/sysext/core/Classes/ServiceProvider.php +++ b/typo3/sysext/core/Classes/ServiceProvider.php @@ -35,6 +35,7 @@ class ServiceProvider extends AbstractServiceProvider return [ Cache\CacheManager::class => [ static::class, 'getCacheManager' ], Console\CommandApplication::class => [ static::class, 'getConsoleCommandApplication' ], + Console\CommandRegistry::class => [ static::class, 'getConsoleCommandRegistry' ], Context\Context::class => [ static::class, 'getContext' ], EventDispatcher\EventDispatcher::class => [ static::class, 'getEventDispatcher' ], EventDispatcher\ListenerProvider::class => [ static::class, 'getEventListenerProvider' ], @@ -77,7 +78,15 @@ class ServiceProvider extends AbstractServiceProvider public static function getConsoleCommandApplication(ContainerInterface $container): Console\CommandApplication { - return new Console\CommandApplication($container->get(Context\Context::class)); + return new Console\CommandApplication( + $container->get(Context\Context::class), + $container->get(Console\CommandRegistry::class) + ); + } + + public static function getConsoleCommandRegistry(ContainerInterface $container): Console\CommandRegistry + { + return new Console\CommandRegistry($container->get(Package\PackageManager::class), $container); } public static function getEventDispatcher(ContainerInterface $container): EventDispatcher\EventDispatcher diff --git a/typo3/sysext/core/Configuration/Commands.php b/typo3/sysext/core/Configuration/Commands.php deleted file mode 100644 index 15028c8ccf4e..000000000000 --- a/typo3/sysext/core/Configuration/Commands.php +++ /dev/null @@ -1,23 +0,0 @@ -<?php - -return [ - 'dumpautoload' => [ - 'class' => \TYPO3\CMS\Core\Command\DumpAutoloadCommand::class, - 'schedulable' => false, - ], - 'mailer:spool:send' => [ - 'class' => \TYPO3\CMS\Core\Command\SendEmailCommand::class, - ], - 'extension:list' => [ - 'class' => \TYPO3\CMS\Core\Command\ExtensionListCommand::class, - 'schedulable' => false - ], - 'site:list' => [ - 'class' => \TYPO3\CMS\Core\Command\SiteListCommand::class, - 'schedulable' => false - ], - 'site:show' => [ - 'class' => \TYPO3\CMS\Core\Command\SiteShowCommand::class, - 'schedulable' => false - ] -]; diff --git a/typo3/sysext/core/Configuration/Services.php b/typo3/sysext/core/Configuration/Services.php index dc3ef1b92d43..0c5420a9d092 100644 --- a/typo3/sysext/core/Configuration/Services.php +++ b/typo3/sysext/core/Configuration/Services.php @@ -21,5 +21,6 @@ return function (ContainerConfigurator $container, ContainerBuilder $containerBu $containerBuilder->addCompilerPass(new DependencyInjection\ListenerProviderPass('event.listener')); $containerBuilder->addCompilerPass(new DependencyInjection\PublicServicePass('typo3.middleware')); $containerBuilder->addCompilerPass(new DependencyInjection\PublicServicePass('typo3.request_handler')); + $containerBuilder->addCompilerPass(new DependencyInjection\ConsoleCommandPass('console.command')); $containerBuilder->addCompilerPass(new DependencyInjection\AutowireInjectMethodsPass()); }; diff --git a/typo3/sysext/core/Configuration/Services.yaml b/typo3/sysext/core/Configuration/Services.yaml index bd6e57342068..5ac89f353670 100644 --- a/typo3/sysext/core/Configuration/Services.yaml +++ b/typo3/sysext/core/Configuration/Services.yaml @@ -10,6 +10,48 @@ services: TYPO3\CMS\Core\DependencyInjection\EnvVarProcessor: tags: ['container.env_var_processor'] + TYPO3\CMS\Core\Command\DumpAutoloadCommand: + tags: + - name: 'console.command' + command: 'dumpautoload' + schedulable: false + - name: 'console.command' + command: 'extensionmanager:extension:dumpclassloadinginformation' + alias: true + schedulable: false + - name: 'console.command' + command: 'extension:dumpclassloadinginformation' + alias: true + schedulable: false + + TYPO3\CMS\Core\Command\ExtensionListCommand: + tags: + - name: 'console.command' + command: 'extension:list' + schedulable: false + + TYPO3\CMS\Core\Command\SendEmailCommand: + tags: + - name: 'console.command' + command: 'mailer:spool:send' + - name: 'console.command' + command: 'swiftmailer:spool:send' + alias: true + schedulable: false + + + TYPO3\CMS\Core\Command\SiteListCommand: + tags: + - name: 'console.command' + command: 'site:list' + schedulable: false + + TYPO3\CMS\Core\Command\SiteShowCommand: + tags: + - name: 'console.command' + command: 'site:show' + schedulable: false + TYPO3\CMS\Core\Configuration\SiteConfiguration: arguments: $configPath: "%env(TYPO3:configPath)%/sites" diff --git a/typo3/sysext/core/Documentation/Changelog/master/Deprecation-89139-ConsoleCommandsConfigurationMigratedToSymfonyServiceTags.rst b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-89139-ConsoleCommandsConfigurationMigratedToSymfonyServiceTags.rst new file mode 100644 index 000000000000..4e373e7d45d6 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-89139-ConsoleCommandsConfigurationMigratedToSymfonyServiceTags.rst @@ -0,0 +1,73 @@ +.. include:: ../../Includes.txt + +===================================================================================== +Deprecation: #89139 - Console Commands configuration migrated to Symfony service tags +===================================================================================== + +See :issue:`89139` + +Description +=========== + +The console command configuration file format :php:`Configuration/Commands.php` +has been deprecated in favor of the dependency injection service tag +`console.command`. The tag allows to configure dependency injection and +command registration in one single location. + + +Impact +====== + +Providing a command configuration in :php:`Configuration/Commands.php` will +trigger a deprecation warning when the respective commands have not already +been defined via dependency injection service tags. + +Extensions that provide both, the deprecated configuration file and service +tags, will not trigger a deprecation message in order to allow extensions to +support multiple TYPO3 major versions. + + +Affected Installations +====================== + +TYPO3 installations with custom extensions that configure symfony console commands +via :php:`Configuration/Commands.php` and have not been migrated to add symfony +service tags. + + +Migration +========= + +Add the `console.command` tag to command classes. Use the tag attribute `command` +to specify the command name. The optional tag attribute `schedulable` may be set +to false to exclude the command from the TYPO3 scheduler. + +.. code-block:: yaml + + services: + _defaults: + autowire: true + autoconfigure: true + public: false + + MyVendor\MyExt\Commands\FooCommand + tags: + - name: 'console.command', + command: 'my:command' + schedulable: false + +Command aliases are to be configured as separate tags. +The optonal tag attribute `alias` should be set to true for alias commands. + +.. code-block:: yaml + + MyVendor\MyExt\Commands\BarCommand + tags: + - name: 'console.command' + command: 'my:bar' } + - name: 'console.command' + command: 'my:old-bar-command' + alias: true + schedulable: false + +.. index:: CLI, PHP-API, PartiallyScanned, ext:core diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-89139-AddDependencyInjectionSupportForConsoleCommands.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-89139-AddDependencyInjectionSupportForConsoleCommands.rst new file mode 100644 index 000000000000..3a981cd0cf2b --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Feature-89139-AddDependencyInjectionSupportForConsoleCommands.rst @@ -0,0 +1,73 @@ +.. include:: ../../Includes.txt + +======================================================================= +Feature: #89139 - Add dependency injection support for console commands +======================================================================= + +See :issue:`89139` + +Description +=========== + +Support for dependency injection in console commands has been added. + +Command dependencies can now be injected via constructor or other injection techniques. +Therefore a new dependency injection tag `console.command` has been added. +Commands tagged with `console.command` are lazy loaded. That means they will only be +instantiated when they are actually executed, when the `help` subcommand is executed, +or when available schedulable commands are iterated. + +The legacy command definition format :php:`Confguration/Commands.php` has been deprecated. + + +Impact +====== + +It is recommended to configure dependency injection tags for all commands, as the legacy command +definition format :php:`Confguration/Commands.php` has been deprecated. + +Commands that have been configured via `console.command` tag override legacy commands from +:php:`Confguration/Commands.php` without throwing a deprecation error for those commands. +Backwards compatibility with older TYPO3 version can be achieved by specifying both variants, +legacy configuration in :php:`Confguration/Commands.php` and new configuration via +`console.command` tag. + + +Usage +===== + +Add the `console.command` tag to command classes. +Use the tag attribute `command` to specify the command name. +The optional tag attribute `schedulable` may be set to false +to exclude the command from the TYPO3 scheduler. + +.. code-block:: yaml + + services: + _defaults: + autowire: true + autoconfigure: true + public: false + + MyVendor\MyExt\Commands\FooCommand + tags: + - name: 'console.command' + command: 'my:command' + schedulable: false + +Command aliases are to be configured as separate tags. +The optonal tag attribute `alias` should be set to true for alias commands. + +.. code-block:: yaml + + MyVendor\MyExt\Commands\BarCommand + tags: + - name: 'console.command' + command: 'my:bar' + - name: 'console.command' + command: 'my:old-bar-command' + alias: true + schedulable: false + + +.. index:: CLI, PHP-API, ext:core diff --git a/typo3/sysext/core/Tests/Unit/Console/CommandRegistryTest.php b/typo3/sysext/core/Tests/Unit/Console/CommandRegistryTest.php index 0d57be61dcaf..aa301cb9b658 100644 --- a/typo3/sysext/core/Tests/Unit/Console/CommandRegistryTest.php +++ b/typo3/sysext/core/Tests/Unit/Console/CommandRegistryTest.php @@ -16,11 +16,10 @@ namespace TYPO3\CMS\Core\Tests\Unit\Console; */ use org\bovigo\vfs\vfsStream; -use Prophecy\Prophecy\ObjectProphecy; +use Psr\Container\ContainerInterface; use Symfony\Component\Console\Command\Command; -use TYPO3\CMS\Core\Console\CommandNameAlreadyInUseException; +use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; use TYPO3\CMS\Core\Console\CommandRegistry; -use TYPO3\CMS\Core\Console\UnknownCommandException; use TYPO3\CMS\Core\Package\PackageInterface; use TYPO3\CMS\Core\Package\PackageManager; use TYPO3\TestingFramework\Core\Unit\UnitTestCase; @@ -40,112 +39,97 @@ class CommandRegistryTest extends UnitTestCase */ protected $packageManagerProphecy; + /** + * @var ContainerInterface|\Prophecy\Prophecy\ObjectProphecy + */ + protected $containerProphecy; + /** * Set up this testcase */ protected function setUp(): void { parent::setUp(); - $commandMockClass = $this->getMockClass(Command::class, ['dummy']); - $this->rootDirectory = vfsStream::setup('root', null, [ - 'package1' => [ - 'Configuration' => [ - 'Commands.php' => '<?php return ["first:command" => [ "class" => "' . $commandMockClass . '" ]];', - ], - ], - 'package2' => [ - 'Configuration' => [ - 'Commands.php' => '<?php return ["second:command" => [ "class" => "' . $commandMockClass . '" ]];', - ], - ], - 'package3' => [ - 'Configuration' => [ - 'Commands.php' => '<?php return ["third:command" => [ "class" => "' . $commandMockClass . '" ]];', - ], - ], - 'package4' => [ - 'Configuration' => [ - 'Commands.php' => '<?php return ["third:command" => [ "class" => "' . $commandMockClass . '" ]];', - ], - ], - ]); /** @var PackageManager */ $this->packageManagerProphecy = $this->prophesize(PackageManager::class); + $this->packageManagerProphecy->getActivePackages()->willReturn([]); + + /** @var ContainerInterface */ + $this->containerProphecy = $this->prophesize(ContainerInterface::class); } /** * @test */ - public function iteratesCommandsOfActivePackages() + public function implementsCommandLoaderInterface() { - /** @var PackageInterface */ - $package1 = $this->prophesize(PackageInterface::class); - $package1->getPackagePath()->willReturn($this->rootDirectory->getChild('package1')->url() . '/'); - /** @var PackageInterface */ - $package2 = $this->prophesize(PackageInterface::class); - $package2->getPackagePath()->willReturn($this->rootDirectory->getChild('package2')->url() . '/'); - - $this->packageManagerProphecy->getActivePackages()->willReturn([$package1->reveal(), $package2->reveal()]); - - $commandRegistry = new CommandRegistry($this->packageManagerProphecy->reveal()); - $commands = iterator_to_array($commandRegistry); - - self::assertCount(2, $commands); - self::assertContainsOnlyInstancesOf(Command::class, $commands); + $commandRegistry = new CommandRegistry($this->packageManagerProphecy->reveal(), $this->containerProphecy->reveal()); + self::assertInstanceof(CommandLoaderInterface::class, $commandRegistry); } /** * @test */ - public function throwsExceptionOnDuplicateCommand() + public function iteratesLazyCommandsOfActivePackages() { - /** @var PackageInterface */ - $package3 = $this->prophesize(PackageInterface::class); - $package3->getPackagePath()->willReturn($this->rootDirectory->getChild('package3')->url() . '/'); - /** @var PackageInterface */ - $package4 = $this->prophesize(PackageInterface::class); - $package4->getPackagePath()->willReturn($this->rootDirectory->getChild('package4')->url() . '/'); - $package4->getPackageKey()->willReturn('package4'); + $command1MockClass = $this->getMockClass(Command::class, ['dummy']); + $command2MockClass = $this->getMockClass(Command::class, ['dummy']); + + $this->containerProphecy->get('command1')->willReturn(new $command1MockClass); + $this->containerProphecy->get('command2')->willReturn(new $command2MockClass); - $this->packageManagerProphecy->getActivePackages()->willReturn([$package3->reveal(), $package4->reveal()]); + $commandRegistry = new CommandRegistry($this->packageManagerProphecy->reveal(), $this->containerProphecy->reveal()); + $commandRegistry->addLazyCommand('test:command', 'command1'); + $commandRegistry->addLazyCommand('test:command2', 'command2'); - $this->expectException(CommandNameAlreadyInUseException::class); - $this->expectExceptionCode(1484486383); + $commandNames = $commandRegistry->getNames(); - $commandRegistry = new CommandRegistry($this->packageManagerProphecy->reveal()); - iterator_to_array($commandRegistry); + self::assertCount(2, $commandNames); + self::assertInstanceOf($command1MockClass, $commandRegistry->get('test:command')); + self::assertInstanceOf($command1MockClass, $commandRegistry->get('test:command2')); } /** * @test */ - public function getCommandByIdentifierReturnsRegisteredCommand() + public function iteratesLegacyCommandsOfActivePackages() { - /** @var PackageInterface|ObjectProphecy $package */ - $package = $this->prophesize(PackageInterface::class); - $package->getPackagePath()->willReturn($this->rootDirectory->getChild('package1')->url() . '/'); - $package->getPackageKey()->willReturn('package1'); + $commandRegistry = new CommandRegistry($this->packageManagerProphecy->reveal(), $this->containerProphecy->reveal()); + $commands = iterator_to_array($commandRegistry->getLegacyCommands()); - $this->packageManagerProphecy->getActivePackages()->willReturn([$package->reveal()]); - - $commandRegistry = new CommandRegistry($this->packageManagerProphecy->reveal()); - $command = $commandRegistry->getCommandByIdentifier('first:command'); - - self::assertInstanceOf(Command::class, $command); + self::assertCount(0, $commands); + self::assertContainsOnlyInstancesOf(Command::class, $commands); } /** * @test */ - public function throwsUnknowCommandExceptionIfUnregisteredCommandIsRequested() + public function lazyCommandOverridesLegacyCommandsWithoutDeprecationError() { - $this->packageManagerProphecy->getActivePackages()->willReturn([]); + $commandMockClass = $this->getMockClass(Command::class, ['dummy']); + $this->rootDirectory = vfsStream::setup('root', null, [ + 'package1' => [ + 'Configuration' => [ + 'Commands.php' => '<?php return ["first:command" => [ "class" => "' . $commandMockClass . '" ]];', + ], + ], + ]); + /** @var PackageInterface */ + $package = $this->prophesize(PackageInterface::class); + $package->getPackagePath()->willReturn($this->rootDirectory->getChild('package1')->url() . '/'); + + $this->packageManagerProphecy->getActivePackages()->willReturn([$package->reveal()]); + + $this->containerProphecy->get($commandMockClass)->willReturn(new $commandMockClass); + + $commandRegistry = new CommandRegistry($this->packageManagerProphecy->reveal(), $this->containerProphecy->reveal()); + $commandRegistry->addLazyCommand('first:command', $commandMockClass); - $this->expectException(UnknownCommandException::class); - $this->expectExceptionCode(1510906768); + $nonLazyCommands = iterator_to_array($commandRegistry->getLegacyCommands()); + $lazyCommands = $commandRegistry->getNames(); - $commandRegistry = new CommandRegistry($this->packageManagerProphecy->reveal()); - $commandRegistry->getCommandByIdentifier('foo'); + self::assertCount(0, $nonLazyCommands); + self::assertCount(1, $lazyCommands); } } diff --git a/typo3/sysext/core/Tests/UnitDeprecated/Console/CommandRegistryTest.php b/typo3/sysext/core/Tests/UnitDeprecated/Console/CommandRegistryTest.php new file mode 100644 index 000000000000..777ea211d055 --- /dev/null +++ b/typo3/sysext/core/Tests/UnitDeprecated/Console/CommandRegistryTest.php @@ -0,0 +1,181 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Core\Tests\UnitDeprecated\Console; + +/* + * 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! + */ + +use org\bovigo\vfs\vfsStream; +use Prophecy\Prophecy\ObjectProphecy; +use Psr\Container\ContainerInterface; +use Symfony\Component\Console\Command\Command; +use TYPO3\CMS\Core\Console\CommandNameAlreadyInUseException; +use TYPO3\CMS\Core\Console\CommandRegistry; +use TYPO3\CMS\Core\Console\UnknownCommandException; +use TYPO3\CMS\Core\Package\PackageInterface; +use TYPO3\CMS\Core\Package\PackageManager; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; + +/** + * Testcase for CommandRegistry + */ +class CommandRegistryTest extends UnitTestCase +{ + /** + * @var \org\bovigo\vfs\vfsStreamDirectory + */ + protected $rootDirectory; + + /** + * @var PackageManager|\Prophecy\Prophecy\ObjectProphecy + */ + protected $packageManagerProphecy; + + /** + * @var ContainerInterface|\Prophecy\Prophecy\ObjectProphecy + */ + protected $containerProphecy; + + /** + * Set up this testcase + */ + protected function setUp(): void + { + parent::setUp(); + $commandMockClass = $this->getMockClass(Command::class, ['dummy']); + $this->rootDirectory = vfsStream::setup('root', null, [ + 'package1' => [ + 'Configuration' => [ + 'Commands.php' => '<?php return ["first:command" => [ "class" => "' . $commandMockClass . '" ]];', + ], + ], + 'package2' => [ + 'Configuration' => [ + 'Commands.php' => '<?php return ["second:command" => [ "class" => "' . $commandMockClass . '" ]];', + ], + ], + 'package3' => [ + 'Configuration' => [ + 'Commands.php' => '<?php return ["third:command" => [ "class" => "' . $commandMockClass . '" ]];', + ], + ], + 'package4' => [ + 'Configuration' => [ + 'Commands.php' => '<?php return ["third:command" => [ "class" => "' . $commandMockClass . '" ]];', + ], + ], + ]); + + /** @var PackageManager */ + $this->packageManagerProphecy = $this->prophesize(PackageManager::class); + + /** @var ContainerInterface */ + $this->containerProphecy = $this->prophesize(ContainerInterface::class); + } + + /** + * @test + */ + public function iteratesCommandsOfActivePackages() + { + /** @var PackageInterface */ + $package1 = $this->prophesize(PackageInterface::class); + $package1->getPackagePath()->willReturn($this->rootDirectory->getChild('package1')->url() . '/'); + /** @var PackageInterface */ + $package2 = $this->prophesize(PackageInterface::class); + $package2->getPackagePath()->willReturn($this->rootDirectory->getChild('package2')->url() . '/'); + + $this->packageManagerProphecy->getActivePackages()->willReturn([$package1->reveal(), $package2->reveal()]); + + $commandRegistry = new CommandRegistry($this->packageManagerProphecy->reveal(), $this->containerProphecy->reveal()); + $commands = iterator_to_array($commandRegistry); + + self::assertCount(2, $commands); + self::assertContainsOnlyInstancesOf(Command::class, $commands); + } + + /** + * @test + */ + public function iteratesLegacyCommandsOfActivePackages() + { + /** @var PackageInterface */ + $package1 = $this->prophesize(PackageInterface::class); + $package1->getPackagePath()->willReturn($this->rootDirectory->getChild('package1')->url() . '/'); + /** @var PackageInterface */ + $package2 = $this->prophesize(PackageInterface::class); + $package2->getPackagePath()->willReturn($this->rootDirectory->getChild('package2')->url() . '/'); + + $this->packageManagerProphecy->getActivePackages()->willReturn([$package1->reveal(), $package2->reveal()]); + + $commandRegistry = new CommandRegistry($this->packageManagerProphecy->reveal(), $this->containerProphecy->reveal()); + $commands = iterator_to_array($commandRegistry->getLegacyCommands()); + + self::assertCount(2, $commands); + self::assertContainsOnlyInstancesOf(Command::class, $commands); + } + + /** + * @test + */ + public function throwsExceptionOnDuplicateCommand() + { + /** @var PackageInterface */ + $package3 = $this->prophesize(PackageInterface::class); + $package3->getPackagePath()->willReturn($this->rootDirectory->getChild('package3')->url() . '/'); + /** @var PackageInterface */ + $package4 = $this->prophesize(PackageInterface::class); + $package4->getPackagePath()->willReturn($this->rootDirectory->getChild('package4')->url() . '/'); + $package4->getPackageKey()->willReturn('package4'); + + $this->packageManagerProphecy->getActivePackages()->willReturn([$package3->reveal(), $package4->reveal()]); + + $this->expectException(CommandNameAlreadyInUseException::class); + $this->expectExceptionCode(1484486383); + + $commandRegistry = new CommandRegistry($this->packageManagerProphecy->reveal(), $this->containerProphecy->reveal()); + iterator_to_array($commandRegistry); + } + + /** + * @test + */ + public function getCommandByIdentifierReturnsRegisteredCommand() + { + /** @var PackageInterface|ObjectProphecy $package */ + $package = $this->prophesize(PackageInterface::class); + $package->getPackagePath()->willReturn($this->rootDirectory->getChild('package1')->url() . '/'); + $package->getPackageKey()->willReturn('package1'); + + $this->packageManagerProphecy->getActivePackages()->willReturn([$package->reveal()]); + + $commandRegistry = new CommandRegistry($this->packageManagerProphecy->reveal(), $this->containerProphecy->reveal()); + $command = $commandRegistry->getCommandByIdentifier('first:command'); + + self::assertInstanceOf(Command::class, $command); + } + + /** + * @test + */ + public function throwsUnknowCommandExceptionIfUnregisteredCommandIsRequested() + { + $this->packageManagerProphecy->getActivePackages()->willReturn([]); + + $this->expectException(UnknownCommandException::class); + $this->expectExceptionCode(1510906768); + + $commandRegistry = new CommandRegistry($this->packageManagerProphecy->reveal(), $this->containerProphecy->reveal()); + $commandRegistry->getCommandByIdentifier('foo'); + } +} diff --git a/typo3/sysext/extensionmanager/Configuration/Commands.php b/typo3/sysext/extensionmanager/Configuration/Commands.php deleted file mode 100644 index 22dd86b0cf21..000000000000 --- a/typo3/sysext/extensionmanager/Configuration/Commands.php +++ /dev/null @@ -1,17 +0,0 @@ -<?php -/** - * Commands to be executed by typo3, where the key of the array - * is the name of the command (to be called as the first argument after typo3). - * Required parameter is the "class" of the command which needs to be a subclass - * of Symfony/Console/Command. - */ -return [ - 'extension:activate' => [ - 'class' => \TYPO3\CMS\Extensionmanager\Command\ActivateExtensionCommand::class, - 'schedulable' => false, - ], - 'extension:deactivate' => [ - 'class' => \TYPO3\CMS\Extensionmanager\Command\DeactivateExtensionCommand::class, - 'schedulable' => false, - ], -]; diff --git a/typo3/sysext/extensionmanager/Configuration/Services.yaml b/typo3/sysext/extensionmanager/Configuration/Services.yaml index c94629a9cd35..9c547ab8306a 100644 --- a/typo3/sysext/extensionmanager/Configuration/Services.yaml +++ b/typo3/sysext/extensionmanager/Configuration/Services.yaml @@ -40,3 +40,31 @@ services: identifier: 'legacy-slot' method: 'emitProcessActionsSignal' event: TYPO3\CMS\Extensionmanager\Event\AvailableActionsForExtensionEvent + + TYPO3\CMS\Extensionmanager\Command\ActivateExtensionCommand: + tags: + - name: 'console.command' + command: 'extension:activate' + schedulable: false + - name: 'console.command' + command: 'extensionmanager:extension:install' + alias: true + schedulable: false + - name: 'console.command' + command: 'extension:install' + alias: true + schedulable: false + + TYPO3\CMS\Extensionmanager\Command\DeactivateExtensionCommand: + tags: + - name: 'console.command' + command: 'extension:deactivate' + schedulable: false + - name: 'console.command' + command: 'extensionmanager:extension:uninstall' + alias: true + schedulable: false + - name: 'console.command' + command: 'extension:uninstall' + alias: true + schedulable: false diff --git a/typo3/sysext/impexp/Configuration/Commands.php b/typo3/sysext/impexp/Configuration/Commands.php deleted file mode 100644 index cecbaea9dec4..000000000000 --- a/typo3/sysext/impexp/Configuration/Commands.php +++ /dev/null @@ -1,12 +0,0 @@ -<?php -/** - * Commands to be executed by typo3, where the key of the array - * is the name of the command (to be called as the first argument after typo3). - * Required parameter is the "class" of the command which needs to be a subclass - * of Symfony/Console/Command. - */ -return [ - 'impexp:import' => [ - 'class' => \TYPO3\CMS\Impexp\Command\ImportCommand::class - ] -]; diff --git a/typo3/sysext/impexp/Configuration/Services.yaml b/typo3/sysext/impexp/Configuration/Services.yaml index 4099126b0450..321ccd27139c 100644 --- a/typo3/sysext/impexp/Configuration/Services.yaml +++ b/typo3/sysext/impexp/Configuration/Services.yaml @@ -10,6 +10,11 @@ services: TYPO3\CMS\Impexp\Utility\ImportExportUtility: public: true + TYPO3\CMS\Impexp\Command\ImportCommand: + tags: + - name: 'console.command' + command: 'impexp:import' + # Listener for old Signal Slots TYPO3\CMS\Impexp\Compatibility\SlotReplacement: tags: diff --git a/typo3/sysext/install/Classes/ServiceProvider.php b/typo3/sysext/install/Classes/ServiceProvider.php index 0f9475d2248b..a1874db01e6f 100644 --- a/typo3/sysext/install/Classes/ServiceProvider.php +++ b/typo3/sysext/install/Classes/ServiceProvider.php @@ -17,6 +17,7 @@ namespace TYPO3\CMS\Install; use Psr\Container\ContainerInterface; use TYPO3\CMS\Core\Configuration\ConfigurationManager; +use TYPO3\CMS\Core\Console\CommandRegistry; use TYPO3\CMS\Core\Context\Context; use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory; use TYPO3\CMS\Core\DependencyInjection\ContainerBuilder; @@ -45,6 +46,16 @@ class ServiceProvider extends AbstractServiceProvider Service\LoadTcaService::class => [ static::class, 'getLoadTcaService' ], Middleware\Maintenance::class => [ static::class, 'getMaintenanceMiddleware' ], Controller\UpgradeController::class => [ static::class, 'getUpgradeController' ], + Command\LanguagePackCommand::class => [ static::class, 'getLanguagePackCommand' ], + Command\UpgradeWizardRunCommand::class => [ static::class, 'getUpgradeWizardRunCommand' ], + Command\UpgradeWizardListCommand::class => [ static::class, 'getUpgradeWizardListCommand' ], + ]; + } + + public function getExtensions(): array + { + return [ + CommandRegistry::class => [ static::class, 'configureCommands' ], ]; } @@ -101,4 +112,27 @@ class ServiceProvider extends AbstractServiceProvider $container->get(Service\LateBootService::class) ); } + + public static function getLanguagePackCommand(ContainerInterface $container): Command\LanguagePackCommand + { + return new Command\LanguagePackCommand; + } + + public static function getUpgradeWizardRunCommand(ContainerInterface $container): Command\UpgradeWizardRunCommand + { + return new Command\UpgradeWizardRunCommand; + } + + public static function getUpgradeWizardListCommand(ContainerInterface $container): Command\UpgradeWizardListCommand + { + return new Command\UpgradeWizardListCommand; + } + + public static function configureCommands(ContainerInterface $container, CommandRegistry $commandRegistry): CommandRegistry + { + $commandRegistry->addLazyCommand('language:update', Command\LanguagePackCommand::class); + $commandRegistry->addLazyCommand('upgrade:run', Command\UpgradeWizardRunCommand::class); + $commandRegistry->addLazyCommand('upgrade:list', Command\UpgradeWizardListCommand::class); + return $commandRegistry; + } } diff --git a/typo3/sysext/install/Configuration/Commands.php b/typo3/sysext/install/Configuration/Commands.php deleted file mode 100644 index 872a35613cee..000000000000 --- a/typo3/sysext/install/Configuration/Commands.php +++ /dev/null @@ -1,20 +0,0 @@ -<?php -/** - * Commands to be executed by typo3, where the key of the array - * is the name of the command (to be called as the first argument after typo3). - * Required parameter is the "class" of the command which needs to be a subclass - * of Symfony/Console/Command. - * - * example: bin/typo3 language:update - */ -return [ - 'language:update' => [ - 'class' => \TYPO3\CMS\Install\Command\LanguagePackCommand::class - ], - 'upgrade:run' => [ - 'class' => \TYPO3\CMS\Install\Command\UpgradeWizardRunCommand::class - ], - 'upgrade:list' => [ - 'class' => \TYPO3\CMS\Install\Command\UpgradeWizardListCommand::class - ] -]; diff --git a/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php b/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php index f909a31af43f..1f0cf4973a1a 100644 --- a/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php +++ b/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php @@ -4420,4 +4420,12 @@ return [ 'Deprecation-90258-SimplifiedRTEParserAPI.rst', ], ], + 'TYPO3\CMS\Core\Console\CommandRegistry->getIterator' => [ + 'numberOfMandatoryArguments' => 0, + 'maximumNumberOfArguments' => 0, + 'restFiles' => [ + 'Feature-89139-AddDependencyInjectionSupportForConsoleCommands.rst', + 'Deprecation-89139-ConsoleCommandsConfigurationMigratedToSymfonyServiceTags.rst', + ], + ], ]; diff --git a/typo3/sysext/lowlevel/Configuration/Commands.php b/typo3/sysext/lowlevel/Configuration/Commands.php deleted file mode 100644 index 3b468e21a263..000000000000 --- a/typo3/sysext/lowlevel/Configuration/Commands.php +++ /dev/null @@ -1,33 +0,0 @@ -<?php -/** - * Commands to be executed by the typo3 CLI binary, where the key of the array - * is the name of the command (to be called as the first argument after "bin/typo3"). - * Required parameter is the "class" of the command which needs to be a subclass - * of Symfony/Console/Command. - */ -return [ - 'syslog:list' => [ - 'class' => \TYPO3\CMS\Lowlevel\Command\ListSysLogCommand::class - ], - 'cleanup:missingfiles' => [ - 'class' => \TYPO3\CMS\Lowlevel\Command\MissingFilesCommand::class - ], - 'cleanup:lostfiles' => [ - 'class' => \TYPO3\CMS\Lowlevel\Command\LostFilesCommand::class - ], - 'cleanup:multiplereferencedfiles' => [ - 'class' => \TYPO3\CMS\Lowlevel\Command\FilesWithMultipleReferencesCommand::class - ], - 'cleanup:missingrelations' => [ - 'class' => \TYPO3\CMS\Lowlevel\Command\MissingRelationsCommand::class - ], - 'cleanup:deletedrecords' => [ - 'class' => \TYPO3\CMS\Lowlevel\Command\DeletedRecordsCommand::class - ], - 'cleanup:orphanrecords' => [ - 'class' => \TYPO3\CMS\Lowlevel\Command\OrphanRecordsCommand::class - ], - 'cleanup:flexforms' => [ - 'class' => \TYPO3\CMS\Lowlevel\Command\CleanFlexFormsCommand::class, - ] -]; diff --git a/typo3/sysext/lowlevel/Configuration/Services.yaml b/typo3/sysext/lowlevel/Configuration/Services.yaml index 6ded070b54e9..a3f390874b98 100644 --- a/typo3/sysext/lowlevel/Configuration/Services.yaml +++ b/typo3/sysext/lowlevel/Configuration/Services.yaml @@ -6,3 +6,43 @@ services: TYPO3\CMS\Lowlevel\: resource: '../Classes/*' + + TYPO3\CMS\Lowlevel\Command\ListSysLogCommand: + tags: + - name: 'console.command' + command: 'syslog:list' + + TYPO3\CMS\Lowlevel\Command\MissingFilesCommand: + tags: + - name: 'console.command' + command: 'cleanup:missingfiles' + + TYPO3\CMS\Lowlevel\Command\LostFilesCommand: + tags: + - name: 'console.command' + command: 'cleanup:lostfiles' + + TYPO3\CMS\Lowlevel\Command\FilesWithMultipleReferencesCommand: + tags: + - name: 'console.command' + command: 'cleanup:multiplereferencedfiles' + + TYPO3\CMS\Lowlevel\Command\MissingRelationsCommand: + tags: + - name: 'console.command' + command: 'cleanup:missingrelations' + + TYPO3\CMS\Lowlevel\Command\DeletedRecordsCommand: + tags: + - name: 'console.command' + command: 'cleanup:deletedrecords' + + TYPO3\CMS\Lowlevel\Command\OrphanRecordsCommand: + tags: + - name: 'console.command' + command: 'cleanup:orphanrecords' + + TYPO3\CMS\Lowlevel\Command\CleanFlexFormsCommand: + tags: + - name: 'console.command' + command: 'cleanup:flexforms' diff --git a/typo3/sysext/redirects/Configuration/Commands.php b/typo3/sysext/redirects/Configuration/Commands.php deleted file mode 100644 index b9687f07e55f..000000000000 --- a/typo3/sysext/redirects/Configuration/Commands.php +++ /dev/null @@ -1,13 +0,0 @@ -<?php -/** - * Commands to be executed by typo3, where the key of the array - * is the name of the command (to be called as the first argument after typo3). - * Required parameter is the "class" of the command which needs to be a subclass - * of Symfony/Console/Command. - */ -return [ - 'redirects:checkintegrity' => [ - 'class' => \TYPO3\CMS\Redirects\Command\CheckIntegrityCommand::class, - 'schedulable' => true, - ], -]; diff --git a/typo3/sysext/redirects/Configuration/Services.yaml b/typo3/sysext/redirects/Configuration/Services.yaml index 17372701f400..eb29c25540e0 100644 --- a/typo3/sysext/redirects/Configuration/Services.yaml +++ b/typo3/sysext/redirects/Configuration/Services.yaml @@ -13,6 +13,11 @@ services: TYPO3\CMS\Redirects\Hooks\DataHandlerSlugUpdateHook: public: true + TYPO3\CMS\Redirects\Command\CheckIntegrityCommand: + tags: + - name: 'console.command' + command: 'redirects:checkintegrity' + TYPO3\CMS\Redirects\EventListener\RecordHistoryRollbackEventsListener: tags: - name: event.listener diff --git a/typo3/sysext/scheduler/Configuration/Commands.php b/typo3/sysext/scheduler/Configuration/Commands.php deleted file mode 100644 index d6d3aa2fe051..000000000000 --- a/typo3/sysext/scheduler/Configuration/Commands.php +++ /dev/null @@ -1,14 +0,0 @@ -<?php -/** - * Commands to be executed by typo3, where the key of the array - * is the name of the command (to be called as the first argument after typo3). - * Required parameter is the "class" of the command which needs to be a subclass - * of Symfony/Console/Command. - */ -return [ - 'scheduler:run' => [ - 'class' => \TYPO3\CMS\Scheduler\Command\SchedulerCommand::class, - // command must not be schedulable, otherwise we'll get an endless recursion - 'schedulable' => false, - ] -]; diff --git a/typo3/sysext/scheduler/Configuration/Services.yaml b/typo3/sysext/scheduler/Configuration/Services.yaml index c719171541e8..b7456c24a3d6 100644 --- a/typo3/sysext/scheduler/Configuration/Services.yaml +++ b/typo3/sysext/scheduler/Configuration/Services.yaml @@ -7,6 +7,12 @@ services: TYPO3\CMS\Scheduler\: resource: '../Classes/*' + TYPO3\CMS\Scheduler\Command\SchedulerCommand: + tags: + - name: 'console.command' + command: 'scheduler:run' + schedulable: false + TYPO3\CMS\Scheduler\SystemInformation\ToolbarItemProvider: tags: - name: event.listener diff --git a/typo3/sysext/workspaces/Configuration/Commands.php b/typo3/sysext/workspaces/Configuration/Commands.php deleted file mode 100644 index e156d33fa66c..000000000000 --- a/typo3/sysext/workspaces/Configuration/Commands.php +++ /dev/null @@ -1,18 +0,0 @@ -<?php -/** - * Commands to be executed by the typo3 CLI binary, where the key of the array - * is the name of the command (to be called as the first argument after "bin/typo3"). - * Required parameter is the "class" of the command which needs to be a subclass - * of Symfony/Console/Command. - */ -return [ - 'cleanup:versions' => [ - 'class' => \TYPO3\CMS\Workspaces\Command\WorkspaceVersionRecordsCommand::class, - ], - 'cleanup:previewlinks' => [ - 'class' => \TYPO3\CMS\Workspaces\Command\CleanupPreviewLinksCommand::class, - ], - 'workspace:autopublish' => [ - 'class' => \TYPO3\CMS\Workspaces\Command\AutoPublishCommand::class, - ], -]; diff --git a/typo3/sysext/workspaces/Configuration/Services.yaml b/typo3/sysext/workspaces/Configuration/Services.yaml index 9c62f633de7c..2649612811de 100644 --- a/typo3/sysext/workspaces/Configuration/Services.yaml +++ b/typo3/sysext/workspaces/Configuration/Services.yaml @@ -15,6 +15,21 @@ services: TYPO3\CMS\Workspaces\Service\GridDataService: public: true + TYPO3\CMS\Workspaces\Command\WorkspaceVersionRecordsCommand: + tags: + - name: 'console.command' + command: 'cleanup:versions' + + TYPO3\CMS\Workspaces\Command\CleanupPreviewLinksCommand: + tags: + - name: 'console.command' + command: 'cleanup:previewlinks' + + TYPO3\CMS\Workspaces\Command\AutoPublishCommand: + tags: + - name: 'console.command' + command: 'workspace:autopublish' + TYPO3\CMS\Workspaces\Compatibility\SlotReplacement: tags: - name: event.listener -- GitLab