Skip to content

[Console] Add support for command lazy-loading #22734

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

Merged
merged 1 commit into from
Jul 12, 2017

Conversation

chalasr
Copy link
Member

@chalasr chalasr commented May 17, 2017

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

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:

$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).

public function __construct(ContainerInterface $container, array $commandNames)
{
$this->container = $container;
$this->commandNames = $commandNames;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the point of having an array of command names when you could just have all these in the container ? Just for the all ?

Copy link
Member Author

@chalasr chalasr May 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, for find() and related helpers especially (resolving req asrequire)

*/
public function get($name)
{
if (!in_array($name, $this->commandNames, true)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we replace this by if(!$this->has($name)) to avoid duplicating code? (unless this function call makes a big difference in performance)

Copy link
Member Author

@chalasr chalasr May 18, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no strong opinion on this, I looked at the existing and it seems duplicating has been preferred https://github.com/symfony/symfony/blob/master/src/Symfony/Component/DependencyInjection/ServiceLocator.php#L47.
However it means that if a sub class extends has() for some reasons, get() would still use the "default" way. Not sure if it's a good thing, perhaps someone else has an hint?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would use the api method indeed, or mark final :)

@nicolas-grekas nicolas-grekas added this to the 3.4 milestone May 18, 2017
@chalasr chalasr force-pushed the lazy-console-commands branch 6 times, most recently from 3539734 to 10871a4 Compare May 18, 2017 14:15
Copy link
Contributor

@ro0NL ro0NL left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

like this in general 👍

} elseif ($this->commandLoader && $this->commandLoader->has($name)) {
$command = $this->commandLoader->get($name);
if (!$command->getName()) {
$command->setName($name);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wouldnt it be more safe to always set this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indeed

if (isset($this->commands[$name])) {
$command = $this->commands[$name];
} elseif ($this->commandLoader && $this->commandLoader->has($name)) {
$command = $this->commandLoader->get($name);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$command = $this->commands[$name] = ...;?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add() does more than $this->command[$name] = $command, hence calling it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

omfg. my bad :)

/**
* @return string[] All registered command names
*/
public function getCommandNames();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getNames?

*/
public function get($name)
{
if (!in_array($name, $this->commandNames, true)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would use the api method indeed, or mark final :)

@@ -35,22 +48,41 @@ public function process(ContainerBuilder $container)
if (!$r = $container->getReflectionClass($class)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this skip abstracts first?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

private $loaderServiceId;
private $commandTag;

public function __construct($loaderServiceId = 'console.command_loader', $commandTag = 'console.command')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imo. $tag or $commandLoaderServiceId ;-)

@@ -478,7 +495,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);
Copy link
Contributor

@ro0NL ro0NL May 18, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

personally i prefer x || (y && z)

@chalasr chalasr force-pushed the lazy-console-commands branch 4 times, most recently from 6461a31 to 3ec61bb Compare May 18, 2017 18:13
@theofidry
Copy link
Contributor

Wouldn't an alternative be to make an abstract static function getName(): string which could be called without instantiating the command? This would be a simpler solution and avoid the need to sync the command name from the tag and the code, although it would require to change the way we currently give the names to the commands.

@chalasr
Copy link
Member Author

chalasr commented May 18, 2017

@theofidry I thought about it.
First, making getName() static would forbid having several commands for the same class, that is a valid use case to me (2b82fcb).
It would also be hard to achieve from a BC pov (actually, I don't see any smooth upgrade path for making a method static).
Lastly, this is mostly about commands registered as services (with dependencies), the current implementation doesn't require any change for basic command which are still in majority.
So I think the static alternative would be a too big step for the need that this is trying to solve.

@chalasr
Copy link
Member Author

chalasr commented May 18, 2017

sync the command name from the tag and the code

Note that when using this feature, you don't need to set the name in your command. It is automatically set when the command is loaded. Same for aliases.

@chalasr chalasr force-pushed the lazy-console-commands branch from 3ec61bb to 7e90c79 Compare May 18, 2017 20:43
@Koc
Copy link
Contributor

Koc commented May 19, 2017

This implementation looks better than previous

@sepehr
Copy link
Contributor

sepehr commented Jul 11, 2017

This is a very good news for a lot of people around the world! Thanks.

@chalasr chalasr force-pushed the lazy-console-commands branch 4 times, most recently from 6cd19b0 to 9fa9fc7 Compare July 11, 2017 22:59
Copy link
Member

@nicolas-grekas nicolas-grekas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 with "for fine tuning" comments.

In a future PR, I think CommandLoaderInterface could be replaced by a more generic interface in the DI component, which would be an extension of PSR11 and would add a "getProvidedServiceIds" to it. But that's to be discussed in another PR :)

/**
* @return iterable All registered commands mapped by names
*/
public function all();
Copy link
Member

@nicolas-grekas nicolas-grekas Jul 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unless I'm misread, this method is not used at all, better remove it then, esp. since getNames() already allows iterating when needed

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed, all() removed

* @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)
Copy link
Member

@nicolas-grekas nicolas-grekas Jul 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if container can be keyed by command names, then we won't need $commandMap (we'll need "$commandNames", which would be enough.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started with only command names, but @mnapoli noticed that it prevents mapping a command to a service that is not named the same as the command (#22734 (comment)), which can be useful especially outside of the fullstack I think.
A good side effect is that using a commandMap makes that we register only one closure for a command and its aliases (they're all mapped to the same service id in the commandMap array instead of in the locator factories, that gives a lighter container). Still not convinced?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, fine for me, thanks for the details

@ogizanagi
Copy link
Contributor

@nicolas-grekas :

In a future PR, I think CommandLoaderInterface could be replaced by a more generic interface in the DI component, which would be an extension of PSR11 and would add a "getProvidedServiceIds" to it. But that's to be discussed in another PR :)

I think we should avoid replacing CommandLoaderInterface completely, which would mean tying this to the DI component. In a simple CLI application, you probably won't require symfony/dependency-injection but simply create your own implementation instead.
BTW, I'd like to suggest a simple factory based implementation in another PR (similar to DI component's ServiceLocator).

@chalasr chalasr force-pushed the lazy-console-commands branch from 9fa9fc7 to 7f97519 Compare July 12, 2017 09:59
@nicolas-grekas
Copy link
Member

Thank you @chalasr.

@nicolas-grekas nicolas-grekas merged commit 7f97519 into symfony:3.4 Jul 12, 2017
nicolas-grekas added a commit that referenced this pull request 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
@chalasr chalasr deleted the lazy-console-commands branch July 12, 2017 12:27
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));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be more straightforward if aliases are just the additional tags using the command attribute as well?
Then there is no need for an alias property at all and this strange condition doesn't apply either.

tags:
        - { name: console.command, command: app:my-command }
        - { name: console.command, command: app:my-alias }

chalasr pushed a commit that referenced this pull request Jul 19, 2017
…application with lazy-loading needs (ogizanagi)

This PR was merged into the 3.4 branch.

Discussion
----------

[Console] Add a factory command loader for standalone application with lazy-loading needs

| Q             | A
| ------------- | ---
| Branch?       | 3.4 <!-- see comment below -->
| Bug fix?      | no
| New feature?  | yes <!-- don't forget updating src/**/CHANGELOG.md files -->
| BC breaks?    | no
| Deprecations? | no <!-- don't forget updating UPGRADE-*.md files -->
| Tests pass?   | yes (failure unrelated)
| Fixed tickets | #22734 (comment) <!-- #-prefixed issue number(s), if any -->
| License       | MIT
| Doc PR        | todo (with symfony/symfony-docs#8147)

So standalone applications can also benefit from the lazy loading feature without requiring a PSR-11 implementation specifically for this need.

The loader does not memoize any resolved command from factories, as it's the `Application` responsibility and the `ContainerCommandLoader` does not either (the PSR-11 does not enforce two successive calls to return the same value).

Commits
-------

9b40b4a [Console] Add a factory command loader for standalone application with lazy-loading needs
symfony-splitter pushed a commit to symfony/console that referenced this pull request Jul 19, 2017
…application with lazy-loading needs (ogizanagi)

This PR was merged into the 3.4 branch.

Discussion
----------

[Console] Add a factory command loader for standalone application with lazy-loading needs

| Q             | A
| ------------- | ---
| Branch?       | 3.4 <!-- see comment below -->
| Bug fix?      | no
| New feature?  | yes <!-- don't forget updating src/**/CHANGELOG.md files -->
| BC breaks?    | no
| Deprecations? | no <!-- don't forget updating UPGRADE-*.md files -->
| Tests pass?   | yes (failure unrelated)
| Fixed tickets | symfony/symfony#22734 (comment) <!-- #-prefixed issue number(s), if any -->
| License       | MIT
| Doc PR        | todo (with symfony/symfony-docs#8147)

So standalone applications can also benefit from the lazy loading feature without requiring a PSR-11 implementation specifically for this need.

The loader does not memoize any resolved command from factories, as it's the `Application` responsibility and the `ContainerCommandLoader` does not either (the PSR-11 does not enforce two successive calls to return the same value).

Commits
-------

9b40b4a [Console] Add a factory command loader for standalone application with lazy-loading needs
fabpot added a commit that referenced this pull request Jul 20, 2017
…oconfigure enabled (chalasr)

This PR was merged into the 3.4 branch.

Discussion
----------

[Console] Fix registering lazy command services with autoconfigure enabled

| Q             | A
| ------------- | ---
| Branch?       | 3.4
| Bug fix?      | no
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | n/a
| License       | MIT
| Doc PR        | n/a

For
```yaml
_defaults:
    autoconfigure: true

App\:
    resource: '../../src/*'

App\Command\FooCommand:
    tags:
        - { name: console.command, command: foo }
```

Before you get the following error:
> Missing "command" attribute on tag "console.command" for service "App\Command\FooCommand"

Now the command is lazy.

----
Btw, @Tobion's #22734 (comment)
> Wouldn't it be more straightforward if aliases are just the additional tags using the command attribute as well?
Then there is no need for an alias property at all and this strange condition doesn't apply either.

Partially addressed here by removing the need for repeating the `command` attribute on each `console.command` tag
```yaml
# before
tags:
    - { name: console.command, command: foo }
    - { name: console.command, command: foo, alias: foobar }

# after
tags:
    - { name: console.command, command: foo }
    - { name: console.command, alias: foobar }
```

Tobias proposal:

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

I wanted to propose exactly the same at first, but finally found more clear to add a specific attribute for aliases, especially because relying on the order on which tags are defined sounds less good to me. Please tell me about your preference.

(And sorry for the noise around this feature, I want to polish it for 3.4)

Commits
-------

8a71aa3 Fix registering lazy command services with autoconfigure enabled
symfony-splitter pushed a commit to symfony/console that referenced this pull request Jul 20, 2017
…oconfigure enabled (chalasr)

This PR was merged into the 3.4 branch.

Discussion
----------

[Console] Fix registering lazy command services with autoconfigure enabled

| Q             | A
| ------------- | ---
| Branch?       | 3.4
| Bug fix?      | no
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | n/a
| License       | MIT
| Doc PR        | n/a

For
```yaml
_defaults:
    autoconfigure: true

App\:
    resource: '../../src/*'

App\Command\FooCommand:
    tags:
        - { name: console.command, command: foo }
```

Before you get the following error:
> Missing "command" attribute on tag "console.command" for service "App\Command\FooCommand"

Now the command is lazy.

----
Btw, @Tobion's symfony/symfony#22734 (comment)
> Wouldn't it be more straightforward if aliases are just the additional tags using the command attribute as well?
Then there is no need for an alias property at all and this strange condition doesn't apply either.

Partially addressed here by removing the need for repeating the `command` attribute on each `console.command` tag
```yaml
# before
tags:
    - { name: console.command, command: foo }
    - { name: console.command, command: foo, alias: foobar }

# after
tags:
    - { name: console.command, command: foo }
    - { name: console.command, alias: foobar }
```

Tobias proposal:

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

I wanted to propose exactly the same at first, but finally found more clear to add a specific attribute for aliases, especially because relying on the order on which tags are defined sounds less good to me. Please tell me about your preference.

(And sorry for the noise around this feature, I want to polish it for 3.4)

Commits
-------

8a71aa31bb Fix registering lazy command services with autoconfigure enabled
@Danack
Copy link

Danack commented Aug 13, 2017

Hi everyone,

The solution that has been raised in this ticket seems complex and a workaround for the fundamental problem that combining the 'routing' of commands with the dispatching of commands is a 'bad idea', which is why most modern frameworks separate those two concerns in HTTP contexts; not separating them in CLI contexts seems just the wrong thing to do.

I've was hoping when I raised this issue a while ago that it could be improved in the next version of Symfony. I still think that would be a better approach than using lazy loading, which always makes it harder to reason about what is actually happening inside an application.

I realise github comments are never the best place to discuss philosophical differences in software architecture - do you guys have a more appropriate place to discuss this?

Otherwise I think I'm going to invite people to an 'official', maintained fork of the Symfony/console library, which would be along the lines of the version I've been using for a couple of years https://github.com/danack/console

@nicolas-grekas
Copy link
Member

I don't see what's complex with implemeting CommandLoaderInterface. Basically, it's what you say: a intermediary "router" between names and commands.

@Ocramius
Copy link
Contributor

Ocramius commented Aug 13, 2017 via email

@nicolas-grekas
Copy link
Member

nicolas-grekas commented Aug 13, 2017

(sorry, bad issue number, reposting on #11974)
(hum, ok, no, the comment has been double-posted, I've been confused :) )

This was referenced Oct 18, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.