Skip to content
Snippets Groups Projects
Commit cef9d8c2 authored by Alexander Schnitzler's avatar Alexander Schnitzler Committed by Helmut Hummel
Browse files

[TASK] Introduce command registry to aggregate console commands

This introduces an iterable command registry that
aggregates commands from Configuration/Commands.php files.
To speed things up for subsequent usage, a first level cache is used.

Resolves: #82455
Releases: master
Change-Id: Ibd123ef947d06939bc84f5ea609996fec85de6e8
Reviewed-on: https://review.typo3.org/54120


Reviewed-by: default avatarHelmut Hummel <typo3@helhum.io>
Tested-by: default avatarHelmut Hummel <typo3@helhum.io>
Reviewed-by: default avatarMathias Brodala <mbrodala@pagemachine.de>
Tested-by: default avatarMathias Brodala <mbrodala@pagemachine.de>
Tested-by: default avatarTYPO3com <no-reply@typo3.com>
Reviewed-by: default avatarAlexander Schnitzler <review.typo3.org@alexanderschnitzler.de>
Tested-by: default avatarAlexander Schnitzler <review.typo3.org@alexanderschnitzler.de>
parent 938a5107
Branches
Tags
No related merge requests found
<?php
declare(strict_types=1);
namespace TYPO3\CMS\Core\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 Symfony\Component\Console\Command\Command;
use TYPO3\CMS\Core\Package\PackageManager;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Registry for Symfony commands, populated from extensions
*/
class CommandRegistry implements \IteratorAggregate, SingletonInterface
{
/**
* @var PackageManager
*/
protected $packageManager;
/**
* Map of commands
*
* @var Command[]
*/
protected $commands = [];
/**
* @param PackageManager $packageManager
*/
public function __construct(PackageManager $packageManager = null)
{
$this->packageManager = $packageManager ?: GeneralUtility::makeInstance(PackageManager::class);
}
/**
* @return \Generator
*/
public function getIterator(): \Generator
{
$this->populateCommandsFromPackages();
foreach ($this->commands as $commandName => $command) {
yield $commandName => $command;
}
}
/**
* Find all Configuration/Commands.php files of extensions and create a registry from it.
* The file should return an array with a command key as key and the command description
* as value. The command description must be an array and have a class key that defines
* the class name of the command. Example:
*
* <?php
* return [
* 'backend:lock' => [
* 'class' => \TYPO3\CMS\Backend\Command\LockBackendCommand::class
* ],
* ];
*
* @throws CommandNameAlreadyInUseException
*/
protected function populateCommandsFromPackages()
{
if ($this->commands) {
return;
}
foreach ($this->packageManager->getActivePackages() as $package) {
$commandsOfExtension = $package->getPackagePath() . 'Configuration/Commands.php';
if (@is_file($commandsOfExtension)) {
$commands = require_once $commandsOfExtension;
if (is_array($commands)) {
foreach ($commands as $commandName => $commandConfig) {
if (array_key_exists($commandName, $this->commands)) {
throw new CommandNameAlreadyInUseException(
'Command "' . $commandName . '" registered by "' . $package->getPackageKey() . '" is already in use',
1484486383
);
}
$this->commands[$commandName] = GeneralUtility::makeInstance($commandConfig['class'], $commandName);
}
}
}
}
}
}
......@@ -20,7 +20,6 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\ConsoleOutput;
use TYPO3\CMS\Core\Authentication\CommandLineUserAuthentication;
use TYPO3\CMS\Core\Core\Bootstrap;
use TYPO3\CMS\Core\Package\PackageManager;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
......@@ -97,28 +96,15 @@ class CommandRequestHandler implements RequestHandlerInterface
/**
* Put all available commands inside the application
* @throws \TYPO3\CMS\Core\Console\CommandNameAlreadyInUseException
*/
protected function populateAvailableCommands()
{
/** @var PackageManager $packageManager */
$packageManager = Bootstrap::getInstance()->getEarlyInstance(PackageManager::class);
$commands = GeneralUtility::makeInstance(CommandRegistry::class);
foreach ($packageManager->getActivePackages() as $package) {
$commandsOfExtension = $package->getPackagePath() . 'Configuration/Commands.php';
if (@is_file($commandsOfExtension)) {
$commands = require_once $commandsOfExtension;
if (is_array($commands)) {
foreach ($commands as $commandName => $commandDescription) {
/** @var Command $cmd */
$cmd = GeneralUtility::makeInstance($commandDescription['class'], $commandName);
// Check if the command name is already in use
if ($this->application->has($commandName)) {
throw new CommandNameAlreadyInUseException('Command "' . $commandName . '" registered by "' . $package->getPackageKey() . '" is already in use', 1484486383);
}
$this->application->add($cmd);
}
}
}
foreach ($commands as $commandName => $command) {
/** @var Command $command */
$this->application->add($command);
}
}
}
<?php
declare(strict_types=1);
namespace TYPO3\CMS\Core\Tests\Unit\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 Symfony\Component\Console\Command\Command;
use TYPO3\CMS\Core\Console\CommandNameAlreadyInUseException;
use TYPO3\CMS\Core\Console\CommandRegistry;
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;
/**
* Set up this testcase
*/
protected function 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);
}
/**
* @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());
$commands = iterator_to_array($commandRegistry);
$this->assertCount(2, $commands);
$this->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());
iterator_to_array($commandRegistry);
}
}
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment