Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Console] Refactor the Console component for lazy-loading of commands #12063

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
mnapoli opened this issue Sep 27, 2014 · 30 comments
Closed

[Console] Refactor the Console component for lazy-loading of commands #12063

mnapoli opened this issue Sep 27, 2014 · 30 comments

Comments

@mnapoli
Copy link
Contributor

mnapoli commented Sep 27, 2014

Commands have to be instantiated to be registered in the console application:

$application = new Application();
$application->add(new GreetCommand);

That's a problem, for example if you have commands with big dependency graphs: it loads all the dependencies in memory. It's also not very practical when using Symfony\Console outside of the framework: integrating it with a DI container is not practical.

I would like to discuss a new way to register commands which would allow them to be lazy loaded. That requires to have the definition of their names outside of the command itself (else the lazy loading is useless).

To do this, there are several options.

1. Closures

The first one would be to leave the opportunity to provide a callable that returns the command:

$commandFactory = function () {
    return new GreetCommand();
}

$application = new Application();
$application->add($commandFactory, 'greet');

That option is BC compatible, but not that good for DX, not very practical.

2. CommandResolver

This is inspired by how HttpKernel works.

$application = new Application();
// Classic command
$application->addCommand('greet', array('Acme\AcmeDemoBundle\Command\GreetCommand', 'execute'));
// Invokable class (i.e. implements __invoke())
$application->addCommand('greet', 'Acme\AcmeDemoBundle\Command\GreetCommand');

// first parameter: the command name
// second parameter: a way to identify the command (string or array)

The HttpKernel uses a ControllerResolver to create controllers. The Console component could have a CommandResolverInterface:

interface CommandResolverInterface
{
    /**
     * @param string|array $command
     * @return callable|false A PHP callable representing the Command,
     *                        or false if this resolver is not able to determine the command
     */
    public function getCommand($command);
}

That would leave the option to implement this interface using any DI container.

That option could be made BC compatible. However it requires more work and it's a big change in the component.


I would really favor the second solution (or something similar). I've read the discussions about that topic, I know opinions are diverse.

I just want to emphasize that I don't want to make Symfony\Console too similar to HttpKernel. Some have suggested that in the past (me included), and I know this won't happen. I am not suggesting to merge Request and Input for example. I am just suggesting to separate the command routing from the command creation, just like it is in HttpKernel.

@unkind
Copy link
Contributor

unkind commented Sep 27, 2014

Described problem looks very similar to #11974 (that one has BC breaks, though).

@lyrixx
Copy link
Member

lyrixx commented Sep 27, 2014

That's a problem, for example if you have commands with big dependency graphs: it loads all the dependencies in memory

Why don't you give your DIC to your command? Like that, all your dependencies are lazy loaded? A command should but the glue between the end user and your model, like a Controller. So it's OK to give the DIC to a command.

Anyway, I agree the command component should be rework to avoid the instantiation of all commands.
Another option could be to expose a CommandConfiguration via a static method.

@mnapoli
Copy link
Contributor Author

mnapoli commented Sep 27, 2014

@unkind Yes #11974 contains big architectural changes and BC breaks. I discussed the idea with him, so the concept is similar, but this one is maybe more realistic for happening in Symfony.

@lyrixx I don't want to inject the DIC because I don't necessarily want to couple my commands to any container, and because injecting the whole container is also less practical. Remember that this component is used a lot outside of Symfony framework.

Anyway, the only solution today is to inject the whole container, which is not good at all. There needs to be an alternative for full dependency injection.

Another option could be to expose a CommandConfiguration via a static method.

It has been discussed in #10639: you might want to provide a default value for options that can be fetched from the DIC (or from any dependency). So it's better to leave the configuration inside the command (as non-static method).

@linaori
Copy link
Contributor

linaori commented Sep 29, 2014

You could actually make the tags leading, where the tag defines the name of the service. Symfony "locates" commands, thus should be able to do this during container compilation and tag those commands in the container.

This way the current default Console behavior will not change, but running commands saves you a lot of memory usage.

I think the following would speak for itself.

services:
    class: Vendor\MyBundle\Command\MyCommand
    arguments:
        - "@mailer" #example 
    tags:
        -  { name: console.command, alias: "my:command" }

For BC compatibility a compiler pass can handle adding commands that are not tagged:

foreach($located_commands as $command) {
    $definition = new Definition(get_class($command));
    // add tag
    // optionally use a factory perhaps?
    // set a unique name
}

I think this would increase performance, usability and decoupling.

@stof
Copy link
Member

stof commented Sep 29, 2014

@mnapoli be careful that the name is not enough. You need the aliases too.

Why don't you give your DIC to your command? Like that, all your dependencies are lazy loaded? A command should but the glue between the end user and your model, like a Controller. So it's OK to give the DIC to a command.

The goal is to support commands as a service in a better way, to avoid loading the whole object graph each time. And commands as a service are very handy when the commands are not defined in the bundle, but in the integrated library (security:check being a perfect example of such command btw, even if it does not have lots of dependencies).

IMO, a first step to support lazy-registration could be done in a similar way than the ContainerAwareEventDispatcher: registering the service id, the name and the aliases in a property (through the code handling tagged commands currently, when the names are provided as attributes on the tags). This way, we don't need to introduce any new concept

@stof
Copy link
Member

stof commented Sep 29, 2014

I am just suggesting to separate the command routing from the command creation, just like it is in HttpKernel.

The command creation is not in the Application class. It is in the Bundle class. The console component does not instantiate commands for you

@lyrixx
Copy link
Member

lyrixx commented Sep 29, 2014

You need the alias and also the isEnable flag.

@stof
Copy link
Member

stof commented Sep 29, 2014

well, the isEnabled is less critical. You could instantiate the service to check it eventually. This would still avoid loading all services. There are only 2 cases where isEnabled will have an impact:

  • you are trying to call a disabled command => load the command, and then fail saying the command does not exist like today
  • ambiguous shortcuts => load the difference candidates (generally only a few commands, unless you are trying to make shortcuts useless in your app and confusing everyone by naming all your command with a single letter change at the end of the name...) and then filter out disabled ones before complaining.

Displaying the command list will still require loading everything, because it also needs the description, and enforcing making it lazy would start making it too complex to lazy-load things.
The common case for the console is still running a command, so this is the case where we should try to keep services lazy

@alexandresalome
Copy link

What about instanciating them on first call to fetch name+isEnabled and cache the result?

@linaori
Copy link
Contributor

linaori commented Sep 30, 2014

@alexandresalome That's what I suggested but at compile-time. This means you can index exactly which commands come from where and it's backwards compatible. Why compile-time? Because you can do that during deployment.

@lyrixx
Copy link
Member

lyrixx commented Sep 30, 2014

This does not work because in isEnable you can have logic that depends of context, like date / time / db status ...

@linaori
Copy link
Contributor

linaori commented Sep 30, 2014

I have only added the name to my example for that reason.

@lyrixx
Copy link
Member

lyrixx commented Sep 30, 2014

But IMHO, it's a bit weird to list the name of a not enabled comman, and then you try to run and it failed

@linaori
Copy link
Contributor

linaori commented Sep 30, 2014

It's a lot nicer to exit a command gracefully if you don't have a connection or when it doesn't meet specific requirements. It should still be known to the container (especially for debugging purposes).

However, configuration (even options/arguments) should not be done inside the command imo. You can optimize a lot more if you know things about a command before initializing it.

Just "because", you can also add enable using the expression language if you are in a funky mood, this enables you to disable it based on logic

@mnapoli
Copy link
Contributor Author

mnapoli commented Oct 1, 2014

Many of you are talking about the container compilation or caching, but remember that this component is used outside of the Symfony framework. It has become a standard on CLI interfaces, thinking about it only from the framework point of view is definitely not doing it justice.


be careful that the name is not enough. You need the aliases too.

You are right @stof, I missed this detail. We seem to all agree though that except the name and the alias, we don't need anymore info. isEnabled can be obtained non-lazily but it doesn't seem to be a problem.

The command creation is not in the Application class. It is in the Bundle class. The console component does not instantiate commands for you

I stand corrected, thanks. I will update the issue description.


@alexandresalome & @iltar: caching (at compile-time or other) is a solution for the Symfony framework, but is not when the component is used as standalone.


But IMHO, it's a bit weird to list the name of a not enabled comman, and then you try to run and it failed

@lyrixx the list will load all the commands, so isEnabled should be available and up to date when listing the commands.


IMO, a first step to support lazy-registration could be done in a similar way than the ContainerAwareEventDispatcher: registering the service id, the name and the aliases in a property (through the code handling tagged commands currently, when the names are provided as attributes on the tags). This way, we don't need to introduce any new concept

How would it work when the component is used standalone.

@stof
Copy link
Member

stof commented Oct 2, 2014

How would it work when the component is used standalone.

When you use the component standalone, most projects are not written in a way allowing to lazy-load command dependencies anyway, because there are not using a DI container lazy-loading the creation of the object graph, and so they create everything upfront (or they don't use DI for their commands, in which case the lazy-loading is a non-issue)

@mnapoli
Copy link
Contributor Author

mnapoli commented Oct 2, 2014

I opened this issue for this very reason: using the Console with a DI container outside of Symfony is a pain. I'd like to find a solution that would make it possible both within and without the framework.

Symfony components are a reference of high quality, reliable and powerful components. I think it's important not to loose sight of it, especially for this component that is used a lot, even within other frameworks.

@fabpot
Copy link
Member

fabpot commented Oct 3, 2014

@mnapoli I will have a look at this issue and the best way to improve the current situation for 2.7.

@mnapoli
Copy link
Contributor Author

mnapoli commented Oct 3, 2014

@fabpot fantastic!

@linaori
Copy link
Contributor

linaori commented Oct 3, 2014

I think that one small step can be taken that will make a nice beginning here, would be to remove the name check from the constructor in Command and move this to the Application for example (add method maybe?). While doing this, the Application being used won't break for 3rd party bundles but this allows for the second step.

With this first step, the standard Command setup where you optionally use a Container should be unchanged. But for the FrameworkBundle, some changes can be made.

  • Allow adding a name option in the tag for the command name
  • Store the console.command.ids as hashmap instead of list in AddConsoleCommandPass, if name is not defined, keep it numeric
  • Call setName on the command in the FrameworkBundle Application, in registerCommands

There are no direct benefits for either Symfony2 nor 3rd party bundles. However, this will set a nice beginning to be able to lazy load commands and might make it easier to get rid of using the default constructor in the future if you desire not to use it.

// AddConsoleCommandPass, this needs to be changed
$container->setParameter('console.command.ids', array_keys($commandServices));
// old FrameworkBundle Application
protected function registerCommands()
{
    // ...

    if ($container->hasParameter('console.command.ids')) {
        foreach ($container->getParameter('console.command.ids') as $id) {
            $this->add($container->get($id));
        }
    }

// new FrameworkBundle Application
protected function registerCommands()
{
     // ...

    if (!$container->hasParameter('console.command.ids')) {
        return;
    }

    foreach ($container->getParameter('console.command.ids') as $name => $id) {
        $command = $container->get($id);
        if (null !== $command->getName()) {
            throw new \RuntimeException(sprintf('Command %s has a double name configuration. By command: %s, by tag: %s', get_class($command), $command->getName(), $name));
        }
        if (is_numeric($name)) && null === $command->getName()) {
            throw new \RuntimeException('No name was configured for command: %s', get_class($command));
        }
        $this->add($command->setName($name));
    }
}

@stof stof added the Console label Oct 6, 2014
@gnugat
Copy link
Contributor

gnugat commented Jan 19, 2015

Is this issue still relevant? If so, will it still be taken into account for the next release (2.7)?

@mnapoli
Copy link
Contributor Author

mnapoli commented Feb 8, 2015

Bumping again, we are having the problem in Piwik too.

We can't afford instantiating all the commands with all their dependencies (and their dependencies…) just to start the console, that could potentially lead to loading the whole application in memory… (I'm exaggerating just a bit :p)

@gnugat
Copy link
Contributor

gnugat commented Feb 9, 2015

I'd like to also highlight the following use case: displaying help using database (for example to set an option's default value).
This is currently impossible in a symfony full stack application, as it would prevent it from being installed: in order to create the database and the schema, we need to run some console commands, which means loading all commands including the one which tries to make a database query to set the default value of an option.

Just my two cents :) .

@funivan
Copy link

funivan commented Nov 4, 2015

@mnapoli I create PR #16438 . Can you check it and said if this logic cover your needs.
Your custom command resolver can create commands when they are really needed by Application.

For example CustomCommandResolver https://github.com/funivan/symfony/blob/console-command-resolv/src/Symfony/Component/Console/Tests/Fixtures/CustomCommandResolver.php
This class hold reference map name => command class
When Application invoke $commandResolver->get('lazy') Our command resolver create command and return it.

@gnugat hello =) Show help without command initialization is possible with CommandResolver.
How you can do this:

  • create custom command resolver
  • add method getCommandHelp($commandName);
  • create custom command ShowHelp and register it
  • in ShowHelp command just invoke $commandResolver->getCommandHelp($id)

If we create getCommandsDescription in commandResolver interface we can refactor ListCommand to access only command names, not objects. So by default application will initialize all commands and return their description. But for lazy loading we can implement any logic (for example store description in cache)

foreach($resolver->getAllName() as $name){
    $output->writeln($name .'    '. $resolver->getCommandDescription($name));
}

@funivan
Copy link

funivan commented Dec 16, 2015

Is there any progress in this issue ?

@TomasVotruba
Copy link
Contributor

For forthcoming: check PR #16438

@mnapoli
Copy link
Contributor Author

mnapoli commented Mar 3, 2016

@TomasVotruba FYI I had open #13946 at the time too.

@TomasVotruba
Copy link
Contributor

@mnapoli Thanks for reference.

@fezfez
Copy link

fezfez commented May 26, 2016

see this other solution https://github.com/fezfez/symfony-console-di

@chalasr
Copy link
Member

chalasr commented May 17, 2017

See also #22734

nicolas-grekas added a commit that referenced this issue Jul 12, 2017
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
symfony-splitter pushed a commit to symfony/security-bundle that referenced this issue Jul 12, 2017
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 | symfony/symfony#12063 symfony/symfony#16438 symfony/symfony#13946 #21781
| License       | MIT
| Doc PR        | todo

This PR adds command lazy-loading support to the console, based on PSR-11 and DI tags.
(symfony/symfony#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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests