-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[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
Comments
Described problem looks very similar to #11974 (that one has BC breaks, though). |
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. |
@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.
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). |
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. |
@mnapoli be careful that the name is not enough. You need the aliases too.
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 ( 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 |
The command creation is not in the Application class. It is in the Bundle class. The console component does not instantiate commands for you |
You need the alias and also the |
well, the
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. |
What about instanciating them on first call to fetch name+isEnabled and cache the result? |
@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. |
This does not work because in |
I have only added the name to my example for that reason. |
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 |
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.
|
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.
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.
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.
@lyrixx the list will load all the commands, so
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) |
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. |
@mnapoli I will have a look at this issue and the best way to improve the current situation for 2.7. |
@fabpot fantastic! |
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 With this first step, the standard
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));
}
} |
Is this issue still relevant? If so, will it still be taken into account for the next release (2.7)? |
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) |
I'd like to also highlight the following use case: displaying help using database (for example to set an option's default value). Just my two cents :) . |
@mnapoli I create PR #16438 . Can you check it and said if this logic cover your needs. For example @gnugat hello =) Show help without command initialization is possible with
If we create foreach($resolver->getAllName() as $name){
$output->writeln($name .' '. $resolver->getCommandDescription($name));
} |
Is there any progress in this issue ? |
For forthcoming: check PR #16438 |
@TomasVotruba FYI I had open #13946 at the time too. |
@mnapoli Thanks for reference. |
see this other solution https://github.com/fezfez/symfony-console-di |
See also #22734 |
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
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
Commands have to be instantiated to be registered in the console application:
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:
That option is BC compatible, but not that good for DX, not very practical.
2. CommandResolver
This is inspired by how HttpKernel works.
The HttpKernel uses a
ControllerResolver
to create controllers. The Console component could have aCommandResolverInterface
: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.
The text was updated successfully, but these errors were encountered: