feature #22734 [Console] Add support for command lazy-loading (chalasr)

This PR was merged into the 3.4 branch.

Discussion
----------

[Console] Add support for command lazy-loading

| Q             | A
| ------------- | ---
| Branch?       | 3.4
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | https://github.com/symfony/symfony/issues/12063 https://github.com/symfony/symfony/pull/16438 https://github.com/symfony/symfony/pull/13946 #21781
| License       | MIT
| Doc PR        | todo

This PR adds command lazy-loading support to the console, based on PSR-11 and DI tags.
(https://github.com/symfony/symfony/issues/12063#issuecomment-57173784)

Commands registered as services which set the `command` attribute on their `console.command` tag are now instantiated when calling `Application::get()` instead of all instantiated at `run()`.

__Usage__

```yaml
app.command.heavy:
    tags:
        - { name: console.command, command: app:heavy }
```

This way private command services can be inlined (no public aliases, remain really private).

With console+PSR11 implem only:

```php
$application = new Application();
$container = new ServiceLocator(['heavy' => function () { return new Heavy(); }]);
$application->setCommandLoader(new ContainerCommandLoader($container, ['app:heavy' => 'heavy']);
```

Implementation is widely inspired from Twig runtime loaders (without the definition/runtime separation which is not needed here).

Commits
-------

7f97519 Add support for command lazy-loading
This commit is contained in:
Nicolas Grekas 2017-07-12 13:56:28 +02:00
commit c8f2c96318
17 changed files with 425 additions and 49 deletions

View File

@ -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
-----

View File

@ -68,15 +68,7 @@ class Application extends BaseApplication
{
$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 @@ class Application extends BaseApplication
{
$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 @@ class Application extends BaseApplication
}
}
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));
}
}
}
}

View File

@ -77,10 +77,15 @@ class RouterDebugCommandTest extends TestCase
$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())

View File

@ -78,8 +78,14 @@ class RouterMatchCommandTest extends TestCase
$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')

View File

@ -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",

View File

@ -10,7 +10,7 @@
<service id="security.console.user_password_encoder_command" class="Symfony\Bundle\SecurityBundle\Command\UserPasswordEncoderCommand">
<argument type="service" id="security.encoder_factory"/>
<argument type="collection" /> <!-- encoders' user classes -->
<tag name="console.command" />
<tag name="console.command" command="security:encode-password" />
</service>
</services>
</container>

View File

@ -10,21 +10,21 @@
<service id="web_server.command.server_run" class="Symfony\Bundle\WebServerBundle\Command\ServerRunCommand">
<argument>%kernel.project_dir%/web</argument>
<argument>%kernel.environment%</argument>
<tag name="console.command" />
<tag name="console.command" command="server:run" />
</service>
<service id="web_server.command.server_start" class="Symfony\Bundle\WebServerBundle\Command\ServerStartCommand">
<argument>%kernel.project_dir%/web</argument>
<argument>%kernel.environment%</argument>
<tag name="console.command" />
<tag name="console.command" command="server:start" />
</service>
<service id="web_server.command.server_stop" class="Symfony\Bundle\WebServerBundle\Command\ServerStopCommand">
<tag name="console.command" />
<tag name="console.command" command="server:stop" />
</service>
<service id="web_server.command.server_status" class="Symfony\Bundle\WebServerBundle\Command\ServerStatusCommand">
<tag name="console.command" />
<tag name="console.command" command="server:status" />
</service>
</services>
</container>

View File

@ -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 @@ class Application
$this->dispatcher = $dispatcher;
}
public function setCommandLoader(CommandLoaderInterface $commandLoader)
{
$this->commandLoader = $commandLoader;
}
/**
* Runs the current application.
*
@ -431,6 +438,10 @@ class Application
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 @@ class Application
*/
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 @@ class Application
*/
public function has($name)
{
return isset($this->commands[$name]);
return isset($this->commands[$name]) || ($this->commandLoader && $this->commandLoader->has($name));
}
/**
@ -555,7 +570,7 @@ class Application
*/
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 @@ class Application
// 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 @@ class Application
$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 @@ class Application
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 @@ class Application
}
}
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;
}

View File

@ -1,6 +1,11 @@
CHANGELOG
=========
3.4.0
-----
* added `CommandLoaderInterface` and PSR-11 `ContainerCommandLoader`
3.3.0
-----

View File

@ -61,10 +61,6 @@ class Command
}
$this->configure();
if (!$this->name) {
throw new LogicException(sprintf('The command defined in "%s" cannot have an empty name.', get_class($this)));
}
}
/**

View File

@ -0,0 +1,37 @@
<?php
namespace Symfony\Component\Console\CommandLoader;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\CommandNotFoundException;
/**
* @author Robin Chalas <robin.chalas@gmail.com>
*/
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();
}

View File

@ -0,0 +1,55 @@
<?php
namespace Symfony\Component\Console\CommandLoader;
use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Exception\CommandNotFoundException;
/**
* Loads commands from a PSR-11 container.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
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);
}
}

View File

@ -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 @@ use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
*/
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 @@ class AddConsoleCommandPass implements CompilerPassInterface
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 (!$definition->isPublic()) {
$container->setAlias($commandId, $id);
$id = $commandId;
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;
}
$serviceIds[$commandId] = $id;
$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;
}
}
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);
}
}

View File

@ -13,6 +13,9 @@ namespace Symfony\Component\Console\Tests;
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\ConsoleErrorEvent;
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 @@ class ApplicationTest extends TestCase
$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 @@ class ApplicationTest extends TestCase
$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 @@ class ApplicationTest extends TestCase
$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 @@ class ApplicationTest extends TestCase
$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 @@ class CustomDefaultCommandApplication extends Application
$this->setDefaultCommand($command->getName());
}
}
class LazyCommand extends Command
{
public function execute(InputInterface $input, OutputInterface $output)
{
$output->writeln('lazy-command called');
}
}

View File

@ -46,7 +46,7 @@ class CommandTest extends TestCase
*/
public function testCommandNameCannotBeEmpty()
{
new Command();
(new Application())->add(new Command());
}
public function testSetApplication()

View File

@ -0,0 +1,61 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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());
}
}

View File

@ -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 @@ class AddConsoleCommandPassTest extends TestCase
$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 @@ class AddConsoleCommandPassTest extends TestCase
$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 @@ class AddConsoleCommandPassTest extends TestCase
$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();