Skip to content

Commit

Permalink
feature #22734 [Console] Add support for command lazy-loading (chalasr)
Browse files Browse the repository at this point in the history
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 | #12063 #16438 #13946 #21781
| License       | MIT
| Doc PR        | todo

This PR adds command lazy-loading support to the console, based on PSR-11 and DI tags.
(#12063 (comment))

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
  • Loading branch information
nicolas-grekas committed Jul 12, 2017
2 parents afaf299 + 7f97519 commit c8f2c96
Show file tree
Hide file tree
Showing 17 changed files with 424 additions and 48 deletions.
2 changes: 2 additions & 0 deletions src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
Expand Up @@ -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
-----
Expand Down
26 changes: 15 additions & 11 deletions src/Symfony/Bundle/FrameworkBundle/Console/Application.php
Expand Up @@ -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);
}
Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -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));
}
}
}
}
Expand Down
Expand Up @@ -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())
Expand Down
Expand Up @@ -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')
Expand Down
4 changes: 2 additions & 2 deletions src/Symfony/Bundle/FrameworkBundle/composer.json
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
Expand Up @@ -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>
Expand Up @@ -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>
57 changes: 47 additions & 10 deletions src/Symfony/Component/Console/Application.php
Expand Up @@ -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;
Expand Down Expand Up @@ -64,6 +65,7 @@ class Application
private $runningCommand;
private $name;
private $version;
private $commandLoader;
private $catchExceptions = true;
private $autoExit = true;
private $definition;
Expand Down Expand Up @@ -96,6 +98,11 @@ public function setDispatcher(EventDispatcherInterface $dispatcher)
$this->dispatcher = $dispatcher;
}

public function setCommandLoader(CommandLoaderInterface $commandLoader)
{
$this->commandLoader = $commandLoader;
}

/**
* Runs the current application.
*
Expand Down Expand Up @@ -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) {
Expand All @@ -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;

Expand All @@ -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));
}

/**
Expand Down Expand Up @@ -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);

Expand All @@ -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);
Expand All @@ -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;
Expand All @@ -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();
Expand All @@ -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;
}

Expand Down
5 changes: 5 additions & 0 deletions src/Symfony/Component/Console/CHANGELOG.md
@@ -1,6 +1,11 @@
CHANGELOG
=========

3.4.0
-----

* added `CommandLoaderInterface` and PSR-11 `ContainerCommandLoader`

3.3.0
-----

Expand Down
4 changes: 0 additions & 4 deletions src/Symfony/Component/Console/Command/Command.php
Expand Up @@ -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)));
}
}

/**
Expand Down
@@ -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();
}

0 comments on commit c8f2c96

Please sign in to comment.