[TASK] Introduce command registry to aggregate console commands 20/54120/22
authorAlexander Schnitzler <git@alexanderschnitzler.de>
Mon, 11 Sep 2017 16:52:08 +0000 (18:52 +0200)
committerHelmut Hummel <typo3@helhum.io>
Mon, 18 Sep 2017 12:14:23 +0000 (14:14 +0200)
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: Helmut Hummel <typo3@helhum.io>
Tested-by: Helmut Hummel <typo3@helhum.io>
Reviewed-by: Mathias Brodala <mbrodala@pagemachine.de>
Tested-by: Mathias Brodala <mbrodala@pagemachine.de>
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Alexander Schnitzler <review.typo3.org@alexanderschnitzler.de>
Tested-by: Alexander Schnitzler <review.typo3.org@alexanderschnitzler.de>
typo3/sysext/core/Classes/Console/CommandRegistry.php [new file with mode: 0644]
typo3/sysext/core/Classes/Console/CommandRequestHandler.php
typo3/sysext/core/Tests/Unit/Console/CommandRegistryTest.php [new file with mode: 0644]

diff --git a/typo3/sysext/core/Classes/Console/CommandRegistry.php b/typo3/sysext/core/Classes/Console/CommandRegistry.php
new file mode 100644 (file)
index 0000000..ce79263
--- /dev/null
@@ -0,0 +1,97 @@
+<?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);
+                    }
+                }
+            }
+        }
+    }
+}
index f583be9..0655cea 100644 (file)
@@ -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);
         }
     }
 }
diff --git a/typo3/sysext/core/Tests/Unit/Console/CommandRegistryTest.php b/typo3/sysext/core/Tests/Unit/Console/CommandRegistryTest.php
new file mode 100644 (file)
index 0000000..e348d30
--- /dev/null
@@ -0,0 +1,116 @@
+<?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);
+    }
+}