From 7f97519624cb08c0d9c119eff0ebb911d7f13d97 Mon Sep 17 00:00:00 2001 From: Robin Chalas Date: Wed, 12 Jul 2017 11:59:19 +0200 Subject: [PATCH] Add support for command lazy-loading --- .../Bundle/FrameworkBundle/CHANGELOG.md | 2 + .../FrameworkBundle/Console/Application.php | 26 +++-- .../Tests/Command/RouterDebugCommandTest.php | 11 +- .../Tests/Command/RouterMatchCommandTest.php | 10 +- .../Bundle/FrameworkBundle/composer.json | 4 +- .../Resources/config/console.xml | 2 +- .../Resources/config/webserver.xml | 8 +- src/Symfony/Component/Console/Application.php | 57 ++++++++-- src/Symfony/Component/Console/CHANGELOG.md | 5 + .../Component/Console/Command/Command.php | 4 - .../CommandLoader/CommandLoaderInterface.php | 37 +++++++ .../CommandLoader/ContainerCommandLoader.php | 55 ++++++++++ .../AddConsoleCommandPass.php | 61 +++++++++-- .../Console/Tests/ApplicationTest.php | 100 ++++++++++++++++++ .../Console/Tests/Command/CommandTest.php | 2 +- .../ContainerCommandLoaderTest.php | 61 +++++++++++ .../AddConsoleCommandPassTest.php | 27 ++++- 17 files changed, 424 insertions(+), 48 deletions(-) create mode 100644 src/Symfony/Component/Console/CommandLoader/CommandLoaderInterface.php create mode 100644 src/Symfony/Component/Console/CommandLoader/ContainerCommandLoader.php create mode 100644 src/Symfony/Component/Console/Tests/CommandLoader/ContainerCommandLoaderTest.php diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index bae6cb0a5a3f..206794266505 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -18,6 +18,8 @@ CHANGELOG `Symfony\Component\Translation\DependencyInjection\TranslationExtractorPass` instead * Deprecated `TranslatorPass`, use `Symfony\Component\Translation\DependencyInjection\TranslatorPass` instead + * Added `command` attribute to the `console.command` tag which takes the command + name as value, using it makes the command lazy 3.3.0 ----- diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Application.php b/src/Symfony/Bundle/FrameworkBundle/Console/Application.php index af5eb25c5a2d..b1c893ad0503 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Application.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Application.php @@ -68,15 +68,7 @@ public function doRun(InputInterface $input, OutputInterface $output) { $this->kernel->boot(); - $container = $this->kernel->getContainer(); - - foreach ($this->all() as $command) { - if ($command instanceof ContainerAwareInterface) { - $command->setContainer($container); - } - } - - $this->setDispatcher($container->get('event_dispatcher')); + $this->setDispatcher($this->kernel->getContainer()->get('event_dispatcher')); return parent::doRun($input, $output); } @@ -98,7 +90,13 @@ public function get($name) { $this->registerCommands(); - return parent::get($name); + $command = parent::get($name); + + if ($command instanceof ContainerAwareInterface) { + $command->setContainer($this->kernel->getContainer()); + } + + return $command; } /** @@ -144,9 +142,15 @@ protected function registerCommands() } } + if ($container->has('console.command_loader')) { + $this->setCommandLoader($container->get('console.command_loader')); + } + if ($container->hasParameter('console.command.ids')) { foreach ($container->getParameter('console.command.ids') as $id) { - $this->add($container->get($id)); + if (false !== $id) { + $this->add($container->get($id)); + } } } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterDebugCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterDebugCommandTest.php index 597082485a0b..13901dd1734b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterDebugCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterDebugCommandTest.php @@ -77,10 +77,15 @@ private function getKernel() $container = $this->getMockBuilder('Symfony\Component\DependencyInjection\ContainerInterface')->getMock(); $container - ->expects($this->once()) + ->expects($this->atLeastOnce()) ->method('has') - ->with('router') - ->will($this->returnValue(true)) + ->will($this->returnCallback(function ($id) { + if ('console.command_loader' === $id) { + return false; + } + + return true; + })) ; $container ->expects($this->any()) diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterMatchCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterMatchCommandTest.php index 384bd7ca5307..44749bbd71db 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterMatchCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterMatchCommandTest.php @@ -78,8 +78,14 @@ private function getKernel() $container ->expects($this->atLeastOnce()) ->method('has') - ->with('router') - ->will($this->returnValue(true)); + ->will($this->returnCallback(function ($id) { + if ('console.command_loader' === $id) { + return false; + } + + return true; + })) + ; $container ->expects($this->any()) ->method('get') diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index e17636e15617..20686669ddaa 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -35,7 +35,7 @@ "fig/link-util": "^1.0", "symfony/asset": "~3.3|~4.0", "symfony/browser-kit": "~2.8|~3.0|~4.0", - "symfony/console": "~3.3|~4.0", + "symfony/console": "~3.4|~4.0", "symfony/css-selector": "~2.8|~3.0|~4.0", "symfony/dom-crawler": "~2.8|~3.0|~4.0", "symfony/polyfill-intl-icu": "~1.0", @@ -64,7 +64,7 @@ "phpdocumentor/type-resolver": "<0.2.0", "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0", "symfony/asset": "<3.3", - "symfony/console": "<3.3", + "symfony/console": "<3.4", "symfony/form": "<3.3", "symfony/property-info": "<3.3", "symfony/serializer": "<3.3", diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/console.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/console.xml index d8b818ff6105..c94c00f75488 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/console.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/console.xml @@ -10,7 +10,7 @@ - + diff --git a/src/Symfony/Bundle/WebServerBundle/Resources/config/webserver.xml b/src/Symfony/Bundle/WebServerBundle/Resources/config/webserver.xml index 2815c6d2cfbf..a3c8b5084153 100644 --- a/src/Symfony/Bundle/WebServerBundle/Resources/config/webserver.xml +++ b/src/Symfony/Bundle/WebServerBundle/Resources/config/webserver.xml @@ -10,21 +10,21 @@ %kernel.project_dir%/web %kernel.environment% - + %kernel.project_dir%/web %kernel.environment% - + - + - + diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index fd15fb4cb26a..fff5d0a6e7b8 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console; +use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; use Symfony\Component\Console\Exception\ExceptionInterface; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Helper\DebugFormatterHelper; @@ -64,6 +65,7 @@ class Application private $runningCommand; private $name; private $version; + private $commandLoader; private $catchExceptions = true; private $autoExit = true; private $definition; @@ -96,6 +98,11 @@ public function setDispatcher(EventDispatcherInterface $dispatcher) $this->dispatcher = $dispatcher; } + public function setCommandLoader(CommandLoaderInterface $commandLoader) + { + $this->commandLoader = $commandLoader; + } + /** * Runs the current application. * @@ -431,6 +438,10 @@ public function add(Command $command) throw new LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', get_class($command))); } + if (!$command->getName()) { + throw new LogicException(sprintf('The command defined in "%s" cannot have an empty name.', get_class($command))); + } + $this->commands[$command->getName()] = $command; foreach ($command->getAliases() as $alias) { @@ -451,12 +462,16 @@ public function add(Command $command) */ public function get($name) { - if (!isset($this->commands[$name])) { + if (isset($this->commands[$name])) { + $command = $this->commands[$name]; + } elseif ($this->commandLoader && $this->commandLoader->has($name)) { + $command = $this->commandLoader->get($name); + $command->setName($name); + $this->add($command); + } else { throw new CommandNotFoundException(sprintf('The command "%s" does not exist.', $name)); } - $command = $this->commands[$name]; - if ($this->wantHelps) { $this->wantHelps = false; @@ -478,7 +493,7 @@ public function get($name) */ public function has($name) { - return isset($this->commands[$name]); + return isset($this->commands[$name]) || ($this->commandLoader && $this->commandLoader->has($name)); } /** @@ -555,7 +570,7 @@ public function findNamespace($namespace) */ public function find($name) { - $allCommands = array_keys($this->commands); + $allCommands = $this->commandLoader ? array_merge($this->commandLoader->getNames(), array_keys($this->commands)) : array_keys($this->commands); $expr = preg_replace_callback('{([^:]+|)}', function ($matches) { return preg_quote($matches[1]).'[^:]*'; }, $name); $commands = preg_grep('{^'.$expr.'}', $allCommands); @@ -581,12 +596,12 @@ public function find($name) // filter out aliases for commands which are already on the list if (count($commands) > 1) { - $commandList = $this->commands; - $commands = array_filter($commands, function ($nameOrAlias) use ($commandList, $commands) { - $commandName = $commandList[$nameOrAlias]->getName(); + $commandList = $this->commandLoader ? array_merge(array_flip($this->commandLoader->getNames()), $this->commands) : $this->commands; + $commands = array_unique(array_filter($commands, function ($nameOrAlias) use ($commandList, $commands) { + $commandName = $commandList[$nameOrAlias] instanceof Command ? $commandList[$nameOrAlias]->getName() : $nameOrAlias; return $commandName === $nameOrAlias || !in_array($commandName, $commands); - }); + })); } $exact = in_array($name, $commands, true); @@ -598,6 +613,9 @@ public function find($name) $maxLen = max(Helper::strlen($abbrev), $maxLen); } $abbrevs = array_map(function ($cmd) use ($commandList, $usableWidth, $maxLen) { + if (!$commandList[$cmd] instanceof Command) { + return $cmd; + } $abbrev = str_pad($cmd, $maxLen, ' ').' '.$commandList[$cmd]->getDescription(); return Helper::strlen($abbrev) > $usableWidth ? Helper::substr($abbrev, 0, $usableWidth - 3).'...' : $abbrev; @@ -622,7 +640,18 @@ public function find($name) public function all($namespace = null) { if (null === $namespace) { - return $this->commands; + if (!$this->commandLoader) { + return $this->commands; + } + + $commands = $this->commands; + foreach ($this->commandLoader->getNames() as $name) { + if (!isset($commands[$name])) { + $commands[$name] = $this->commandLoader->get($name); + } + } + + return $commands; } $commands = array(); @@ -632,6 +661,14 @@ public function all($namespace = null) } } + if ($this->commandLoader) { + foreach ($this->commandLoader->getNames() as $name) { + if (!isset($commands[$name]) && $namespace === $this->extractNamespace($name, substr_count($namespace, ':') + 1)) { + $commands[$name] = $this->commandLoader->get($name); + } + } + } + return $commands; } diff --git a/src/Symfony/Component/Console/CHANGELOG.md b/src/Symfony/Component/Console/CHANGELOG.md index f8539491d264..542d00623beb 100644 --- a/src/Symfony/Component/Console/CHANGELOG.md +++ b/src/Symfony/Component/Console/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +3.4.0 +----- + + * added `CommandLoaderInterface` and PSR-11 `ContainerCommandLoader` + 3.3.0 ----- diff --git a/src/Symfony/Component/Console/Command/Command.php b/src/Symfony/Component/Console/Command/Command.php index 08a74c3b69a9..6620ff5718c5 100644 --- a/src/Symfony/Component/Console/Command/Command.php +++ b/src/Symfony/Component/Console/Command/Command.php @@ -61,10 +61,6 @@ public function __construct($name = null) } $this->configure(); - - if (!$this->name) { - throw new LogicException(sprintf('The command defined in "%s" cannot have an empty name.', get_class($this))); - } } /** diff --git a/src/Symfony/Component/Console/CommandLoader/CommandLoaderInterface.php b/src/Symfony/Component/Console/CommandLoader/CommandLoaderInterface.php new file mode 100644 index 000000000000..9462996f6d2a --- /dev/null +++ b/src/Symfony/Component/Console/CommandLoader/CommandLoaderInterface.php @@ -0,0 +1,37 @@ + + */ +interface CommandLoaderInterface +{ + /** + * Loads a command. + * + * @param string $name + * + * @return Command + * + * @throws CommandNotFoundException + */ + public function get($name); + + /** + * Checks if a command exists. + * + * @param string $name + * + * @return bool + */ + public function has($name); + + /** + * @return string[] All registered command names + */ + public function getNames(); +} diff --git a/src/Symfony/Component/Console/CommandLoader/ContainerCommandLoader.php b/src/Symfony/Component/Console/CommandLoader/ContainerCommandLoader.php new file mode 100644 index 000000000000..753ad0fb705c --- /dev/null +++ b/src/Symfony/Component/Console/CommandLoader/ContainerCommandLoader.php @@ -0,0 +1,55 @@ + + */ +class ContainerCommandLoader implements CommandLoaderInterface +{ + private $container; + private $commandMap; + + /** + * @param ContainerInterface $container A container from which to load command services + * @param array $commandMap An array with command names as keys and service ids as values + */ + public function __construct(ContainerInterface $container, array $commandMap) + { + $this->container = $container; + $this->commandMap = $commandMap; + } + + /** + * {@inheritdoc} + */ + public function get($name) + { + if (!$this->has($name)) { + throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name)); + } + + return $this->container->get($this->commandMap[$name]); + } + + /** + * {@inheritdoc} + */ + public function has($name) + { + return isset($this->commandMap[$name]) && $this->container->has($this->commandMap[$name]); + } + + /** + * {@inheritdoc} + */ + public function getNames() + { + return array_keys($this->commandMap); + } +} diff --git a/src/Symfony/Component/Console/DependencyInjection/AddConsoleCommandPass.php b/src/Symfony/Component/Console/DependencyInjection/AddConsoleCommandPass.php index d0626be16b2c..5a323421b13e 100644 --- a/src/Symfony/Component/Console/DependencyInjection/AddConsoleCommandPass.php +++ b/src/Symfony/Component/Console/DependencyInjection/AddConsoleCommandPass.php @@ -12,9 +12,12 @@ namespace Symfony\Component\Console\DependencyInjection; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\CommandLoader\ContainerCommandLoader; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\TypedReference; /** * Registers console commands. @@ -23,9 +26,20 @@ */ class AddConsoleCommandPass implements CompilerPassInterface { + private $commandLoaderServiceId; + private $commandTag; + + public function __construct($commandLoaderServiceId = 'console.command_loader', $commandTag = 'console.command') + { + $this->commandLoaderServiceId = $commandLoaderServiceId; + $this->commandTag = $commandTag; + } + public function process(ContainerBuilder $container) { - $commandServices = $container->findTaggedServiceIds('console.command', true); + $commandServices = $container->findTaggedServiceIds($this->commandTag, true); + $lazyCommandMap = array(); + $lazyCommandRefs = array(); $serviceIds = array(); foreach ($commandServices as $id => $tags) { @@ -36,21 +50,52 @@ public function process(ContainerBuilder $container) throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id)); } if (!$r->isSubclassOf(Command::class)) { - throw new InvalidArgumentException(sprintf('The service "%s" tagged "console.command" must be a subclass of "%s".', $id, Command::class)); + throw new InvalidArgumentException(sprintf('The service "%s" tagged "%s" must be a subclass of "%s".', $id, $this->commandTag, Command::class)); } $commandId = 'console.command.'.strtolower(str_replace('\\', '_', $class)); - if ($container->hasAlias($commandId) || isset($serviceIds[$commandId])) { - $commandId = $commandId.'_'.$id; + + if (!isset($tags[0]['command'])) { + if (isset($serviceIds[$commandId]) || $container->hasAlias($commandId)) { + $commandId = $commandId.'_'.$id; + } + if (!$definition->isPublic()) { + $container->setAlias($commandId, $id); + $id = $commandId; + } + $serviceIds[$commandId] = $id; + + continue; } - if (!$definition->isPublic()) { - $container->setAlias($commandId, $id); - $id = $commandId; + + $serviceIds[$commandId] = false; + $commandName = $tags[0]['command']; + $lazyCommandMap[$commandName] = $id; + $lazyCommandRefs[$id] = new TypedReference($id, $class); + $aliases = array(); + + foreach ($tags as $tag) { + if (!isset($tag['command'])) { + throw new InvalidArgumentException(sprintf('Missing "command" attribute on tag "%s" for service "%s".', $this->commandTag, $id)); + } + if ($commandName !== $tag['command']) { + throw new InvalidArgumentException(sprintf('The "command" attribute must be the same on each "%s" tag for service "%s".', $this->commandTag, $id)); + } + if (isset($tag['alias'])) { + $aliases[] = $tag['alias']; + $lazyCommandMap[$tag['alias']] = $id; + } } - $serviceIds[$commandId] = $id; + if ($aliases) { + $definition->addMethodCall('setAliases', array($aliases)); + } } + $container + ->register($this->commandLoaderServiceId, ContainerCommandLoader::class) + ->setArguments(array(ServiceLocatorTagPass::register($container, $lazyCommandRefs), $lazyCommandMap)); + $container->setParameter('console.command.ids', $serviceIds); } } diff --git a/src/Symfony/Component/Console/Tests/ApplicationTest.php b/src/Symfony/Component/Console/Tests/ApplicationTest.php index 34d9cb0c0d78..7bf76ddc8e83 100644 --- a/src/Symfony/Component/Console/Tests/ApplicationTest.php +++ b/src/Symfony/Component/Console/Tests/ApplicationTest.php @@ -13,6 +13,9 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Application; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\CommandLoader\ContainerCommandLoader; +use Symfony\Component\Console\DependencyInjection\AddConsoleCommandPass; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Helper\FormatterHelper; use Symfony\Component\Console\Input\ArgvInput; @@ -31,6 +34,8 @@ use Symfony\Component\Console\Event\ConsoleExceptionEvent; use Symfony\Component\Console\Event\ConsoleTerminateEvent; use Symfony\Component\Console\Exception\CommandNotFoundException; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\EventDispatcher\EventDispatcher; class ApplicationTest extends TestCase @@ -114,6 +119,26 @@ public function testAll() $this->assertCount(1, $commands, '->all() takes a namespace as its first argument'); } + public function testAllWithCommandLoader() + { + $application = new Application(); + $commands = $application->all(); + $this->assertInstanceOf('Symfony\\Component\\Console\\Command\\HelpCommand', $commands['help'], '->all() returns the registered commands'); + + $application->add(new \FooCommand()); + $commands = $application->all('foo'); + $this->assertCount(1, $commands, '->all() takes a namespace as its first argument'); + + $application->setCommandLoader(new ContainerCommandLoader( + new ServiceLocator(array('foo-bar' => function () { return new \Foo1Command(); })), + array('foo:bar1' => 'foo-bar') + )); + $commands = $application->all('foo'); + $this->assertCount(2, $commands, '->all() takes a namespace as its first argument'); + $this->assertInstanceOf(\FooCommand::class, $commands['foo:bar'], '->all() returns the registered commands'); + $this->assertInstanceOf(\Foo1Command::class, $commands['foo:bar1'], '->all() returns the registered commands'); + } + public function testRegister() { $application = new Application(); @@ -166,6 +191,30 @@ public function testHasGet() $this->assertInstanceOf('Symfony\Component\Console\Command\HelpCommand', $command, '->get() returns the help command if --help is provided as the input'); } + public function testHasGetWithCommandLoader() + { + $application = new Application(); + $this->assertTrue($application->has('list'), '->has() returns true if a named command is registered'); + $this->assertFalse($application->has('afoobar'), '->has() returns false if a named command is not registered'); + + $application->add($foo = new \FooCommand()); + $this->assertTrue($application->has('afoobar'), '->has() returns true if an alias is registered'); + $this->assertEquals($foo, $application->get('foo:bar'), '->get() returns a command by name'); + $this->assertEquals($foo, $application->get('afoobar'), '->get() returns a command by alias'); + + $application->setCommandLoader(new ContainerCommandLoader(new ServiceLocator(array( + 'foo-bar' => function () { return new \Foo1Command(); }, + )), array('foo:bar1' => 'foo-bar', 'afoobar1' => 'foo-bar'))); + + $this->assertTrue($application->has('afoobar'), '->has() returns true if an instance is registered for an alias even with command loader'); + $this->assertEquals($foo, $application->get('foo:bar'), '->get() returns an instance by name even with command loader'); + $this->assertEquals($foo, $application->get('afoobar'), '->get() returns an instance by alias even with command loader'); + $this->assertTrue($application->has('foo:bar1'), '->has() returns true for commands registered in the loader'); + $this->assertInstanceOf(\Foo1Command::class, $foo1 = $application->get('foo:bar1'), '->get() returns a command by name from the command loader'); + $this->assertTrue($application->has('afoobar1'), '->has() returns true for commands registered in the loader'); + $this->assertEquals($foo1, $application->get('afoobar1'), '->get() returns a command by name from the command loader'); + } + public function testSilentHelp() { $application = new Application(); @@ -269,6 +318,20 @@ public function testFind() $this->assertInstanceOf('FooCommand', $application->find('a'), '->find() returns a command if the abbreviation exists for an alias'); } + public function testFindWithCommandLoader() + { + $application = new Application(); + $application->setCommandLoader(new ContainerCommandLoader(new ServiceLocator(array( + 'foo-bar' => $f = function () { return new \FooCommand(); }, + )), array('foo:bar' => 'foo-bar'))); + + $this->assertInstanceOf('FooCommand', $application->find('foo:bar'), '->find() returns a command if its name exists'); + $this->assertInstanceOf('Symfony\Component\Console\Command\HelpCommand', $application->find('h'), '->find() returns a command if its name exists'); + $this->assertInstanceOf('FooCommand', $application->find('f:bar'), '->find() returns a command if the abbreviation for the namespace exists'); + $this->assertInstanceOf('FooCommand', $application->find('f:b'), '->find() returns a command if the abbreviation for the namespace and the command name exist'); + $this->assertInstanceOf('FooCommand', $application->find('a'), '->find() returns a command if the abbreviation exists for an alias'); + } + /** * @dataProvider provideAmbiguousAbbreviations */ @@ -1362,6 +1425,35 @@ public function testCanCheckIfTerminalIsInteractive() $this->assertEquals($tester->getInput()->isInteractive(), @posix_isatty($inputStream)); } + public function testRunLazyCommandService() + { + $container = new ContainerBuilder(); + $container->addCompilerPass(new AddConsoleCommandPass()); + $container + ->register('lazy-command', LazyCommand::class) + ->addTag('console.command', array('command' => 'lazy:command', 'alias' => 'lazy:alias')) + ->addTag('console.command', array('command' => 'lazy:command', 'alias' => 'lazy:alias2')); + $container->compile(); + + $application = new Application(); + $application->setCommandLoader($container->get('console.command_loader')); + $application->setAutoExit(false); + + $tester = new ApplicationTester($application); + + $tester->run(array('command' => 'lazy:command')); + $this->assertSame("lazy-command called\n", $tester->getDisplay(true)); + + $tester->run(array('command' => 'lazy:alias')); + $this->assertSame("lazy-command called\n", $tester->getDisplay(true)); + + $tester->run(array('command' => 'lazy:alias2')); + $this->assertSame("lazy-command called\n", $tester->getDisplay(true)); + + $command = $application->get('lazy:command'); + $this->assertSame(array('lazy:alias', 'lazy:alias2'), $command->getAliases()); + } + protected function getDispatcher($skipCommand = false) { $dispatcher = new EventDispatcher(); @@ -1449,3 +1541,11 @@ public function __construct() $this->setDefaultCommand($command->getName()); } } + +class LazyCommand extends Command +{ + public function execute(InputInterface $input, OutputInterface $output) + { + $output->writeln('lazy-command called'); + } +} diff --git a/src/Symfony/Component/Console/Tests/Command/CommandTest.php b/src/Symfony/Component/Console/Tests/Command/CommandTest.php index 93b4721c393d..4fcbf95753bb 100644 --- a/src/Symfony/Component/Console/Tests/Command/CommandTest.php +++ b/src/Symfony/Component/Console/Tests/Command/CommandTest.php @@ -46,7 +46,7 @@ public function testConstructor() */ public function testCommandNameCannotBeEmpty() { - new Command(); + (new Application())->add(new Command()); } public function testSetApplication() diff --git a/src/Symfony/Component/Console/Tests/CommandLoader/ContainerCommandLoaderTest.php b/src/Symfony/Component/Console/Tests/CommandLoader/ContainerCommandLoaderTest.php new file mode 100644 index 000000000000..78eefd24f1b3 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/CommandLoader/ContainerCommandLoaderTest.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tests\CommandLoader; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\CommandLoader\ContainerCommandLoader; +use Symfony\Component\DependencyInjection\ServiceLocator; + +class ContainerCommandLoaderTest extends TestCase +{ + public function testHas() + { + $loader = new ContainerCommandLoader(new ServiceLocator(array( + 'foo-service' => function () { return new Command('foo'); }, + 'bar-service' => function () { return new Command('bar'); }, + )), array('foo' => 'foo-service', 'bar' => 'bar-service')); + + $this->assertTrue($loader->has('foo')); + $this->assertTrue($loader->has('bar')); + $this->assertFalse($loader->has('baz')); + } + + public function testGet() + { + $loader = new ContainerCommandLoader(new ServiceLocator(array( + 'foo-service' => function () { return new Command('foo'); }, + 'bar-service' => function () { return new Command('bar'); }, + )), array('foo' => 'foo-service', 'bar' => 'bar-service')); + + $this->assertInstanceOf(Command::class, $loader->get('foo')); + $this->assertInstanceOf(Command::class, $loader->get('bar')); + } + + /** + * @expectedException \Symfony\Component\Console\Exception\CommandNotFoundException + */ + public function testGetUnknownCommandThrows() + { + (new ContainerCommandLoader(new ServiceLocator(array()), array()))->get('unknown'); + } + + public function testGetCommandNames() + { + $loader = new ContainerCommandLoader(new ServiceLocator(array( + 'foo-service' => function () { return new Command('foo'); }, + 'bar-service' => function () { return new Command('bar'); }, + )), array('foo' => 'foo-service', 'bar' => 'bar-service')); + + $this->assertSame(array('foo', 'bar'), $loader->getNames()); + } +} diff --git a/src/Symfony/Component/Console/Tests/DependencyInjection/AddConsoleCommandPassTest.php b/src/Symfony/Component/Console/Tests/DependencyInjection/AddConsoleCommandPassTest.php index 0cf463175452..6454dd63df8d 100644 --- a/src/Symfony/Component/Console/Tests/DependencyInjection/AddConsoleCommandPassTest.php +++ b/src/Symfony/Component/Console/Tests/DependencyInjection/AddConsoleCommandPassTest.php @@ -12,10 +12,13 @@ namespace Symfony\Component\Console\Tests\DependencyInjection; use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\CommandLoader\ContainerCommandLoader; use Symfony\Component\Console\DependencyInjection\AddConsoleCommandPass; use Symfony\Component\Console\Command\Command; +use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\TypedReference; use Symfony\Component\HttpKernel\Bundle\Bundle; class AddConsoleCommandPassTest extends TestCase @@ -53,6 +56,26 @@ public function testProcess($public) $this->assertSame(array($alias => $id), $container->getParameter('console.command.ids')); } + public function testProcessRegisterLazyCommands() + { + $container = new ContainerBuilder(); + $container + ->register('my-command', MyCommand::class) + ->setPublic(false) + ->addTag('console.command', array('command' => 'my:command', 'alias' => 'my:alias')) + ; + + (new AddConsoleCommandPass())->process($container); + + $commandLoader = $container->getDefinition('console.command_loader'); + $commandLocator = $container->getDefinition((string) $commandLoader->getArgument(0)); + + $this->assertSame(ContainerCommandLoader::class, $commandLoader->getClass()); + $this->assertSame(array('my:command' => 'my-command', 'my:alias' => 'my-command'), $commandLoader->getArgument(1)); + $this->assertEquals(array(array('my-command' => new ServiceClosureArgument(new TypedReference('my-command', MyCommand::class)))), $commandLocator->getArguments()); + $this->assertSame(array('console.command.symfony_component_console_tests_dependencyinjection_mycommand' => false), $container->getParameter('console.command.ids')); + } + public function visibilityProvider() { return array( @@ -72,7 +95,7 @@ public function testProcessThrowAnExceptionIfTheServiceIsAbstract() $container->addCompilerPass(new AddConsoleCommandPass()); $definition = new Definition('Symfony\Component\Console\Tests\DependencyInjection\MyCommand'); - $definition->addTag('console.command'); + $definition->addTag('console.command', array('command' => 'my:command')); $definition->setAbstract(true); $container->setDefinition('my-command', $definition); @@ -90,7 +113,7 @@ public function testProcessThrowAnExceptionIfTheServiceIsNotASubclassOfCommand() $container->addCompilerPass(new AddConsoleCommandPass()); $definition = new Definition('SplObjectStorage'); - $definition->addTag('console.command'); + $definition->addTag('console.command', array('command' => 'my:command')); $container->setDefinition('my-command', $definition); $container->compile();