8000 [Console] Run `InvokableCommand` via `execute()` method. by crynobone · Pull Request #60353 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[Console] Run InvokableCommand via execute() method. #60353

8000 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

Conversation

crynobone
Copy link
Contributor
@crynobone crynobone commented May 6, 2025
Q A
Branch? 7.3
Bug fix? no
New feature? no
Deprecations? no
Issues n/a
License MIT

Laravel Framework always allows handling a command either via handle() method or __invoke() method. The changes made for Symfony 7.3 will create an issue on existing Laravel applications on Laravel 11 and 12 when version 7.3.0 becomes available.

Having the changes in this PR will allow Laravel to override configureAsInvokableCommand() to keep the current behavior.


Given the following class on a basic Laravel installation:

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Contracts\Config\Repository as ConfigRepository;

class HelloWorldCommand extends Command
{
    /**
     * The name of the console command.
     *
     * @var string
     */
    protected $name = 'app:hello-world';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Command description';

    /**
     * Execute the console command.
     */
    public function __invoke(ConfigRepository $config)
    {
        $this->components->info($config->get('app.name'));

        return self::SUCCESS;
    }
}

Result on Symfony 7.2

CleanShot 2025-05-06 at 15 02 44

Result on Symfony 7.3

CleanShot 2025-05-06 at 15 03 26

@crynobone crynobone requested a review from chalasr as a code owner May 6, 2025 06:50
@carsonbot carsonbot added this to the 7.3 milestone May 6, 2025
@carsonbot
Copy link

It looks like you unchecked the "Allow edits from maintainer" box. That is fine, but please note that if you have multiple commits, you'll need to squash your commits into one before this can be merged. Or, you can check the "Allow edits from maintainers" box and the maintainer can squash for you.

Cheers!

Carsonbot

@crynobone crynobone changed the title [Console] Set InvokableCommand via method instead of directly within … [Console] Set InvokableCommand via method instead of directly within __construct() May 6, 2025
@OskarStark
Copy link
Contributor

friendly ping @yceruto

@crynobone crynobone changed the title [Console] Set InvokableCommand via method instead of directly within __construct() [Console] Set InvokableCommand via a method instead of directly within __construct() May 6, 2025
@xabbuh
Copy link
Member
xabbuh commented May 6, 2025

Can you elaborate a bit on why the upcoming changes for 7.3 are causing issues for Laravel and how the proposed change would fix it? Right now, I have some trouble understanding why this is necessary.

@crynobone
Copy link
Contributor Author

Given the following class on a basic Laravel installation:

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Contracts\Config\Repository as ConfigRepository;

class HelloWorldCommand extends Command
{
    /**
     * The name of the console command.
     *
     * @var string
     */
    protected $name = 'app:hello-world';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Command description';

    /**
     * Execute the console command.
     */
    public function __invoke(ConfigRepository $config)
    {
        $this->components->info($config->get('app.name'));

        return self::SUCCESS;
    }
}

Result on Symfony 7.2

CleanShot 2025-05-06 at 15 02 44

Result on Symfony 7.3

CleanShot 2025-05-06 at 15 03 26

@crynobone
Copy link
Contributor Author

Since is_callable($this) check was made directly in __construct() and set the command to be handled via InvokableCommand, this skips any preparation that Laravel made within Command::execute() method, including IoC injection (see example above).

@@ -134,9 +134,7 @@ public function __construct(?string $name = null)
$this->setHelp($attribute?->help ?? '');
}

if (\is_callable($this)) {
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if the fix instead could be to additionally check here that the execute() method is not overridden.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In the example, App\Console\Commands\HelloWorldCommand doesn't have an execute() method, only Illuminate\Console\Command has it.

It would be nice if Symfony introduce an interface to handle invokable command (as this is a new feature for Symfony 7.3) so it doesn't rely on is_callable().

Copy link
Member

Choose a reason for hiding this comment

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

For the check I had in mind it doesn't really matter if it's an intermediate class or the actual command class that implements execute(). We would just ensure that the class declaring the method is not Symfony's Command class.

Copy link
Member

It would be nice if Symfony introduce an interface to handle invokable command (as this is a new feature for Symfony 7.3) so it doesn't rely on is_callable().

An interface is not desired on our side as we want commands to be any callable really.

Copy link
Member
@GromNaN GromNaN May 6, 2025

Choose a reason for hiding this comment

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

In the example, App\Console\Commands\HelloWorldCommand doesn't have an execute() method, only Illuminate\Console\Command has it.

This confirms that the execute() method is overridden in Illuminate\Console\Command.

I wonder if the fix instead could be to additionally check here that the execute() method is not overridden.

I agree with this solution, here's an implementation.

if (\is_callable($this) && (new \ReflectionMethod($this, 'execute'))->getDeclaringClass()->name !== self::class) {
    $this->code = new InvokableCommand($this, $this(...));
}

@crynobone The solution you propose solves the issue you raise, but creates a new one because it introduces a new protected method tied to an implementation detail and becomes a maintenance constraint.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nice, happy if anyone take over and make a PR for that as I will only be able to work on it much later today/tomorrow. Thank you

Copy link
Member

Choose a reason for hiding this comment

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

I agree to add only && (new \ReflectionMethod($this, 'execute'))->getDeclaringClass()->name !== self::class to this line, that does solve the original issue, right? please also add a new test around this case, thanks!

Alternatively wouldn't it make more sense to resolve/invoke $this->code from execute() method, otherwise throw LogicException?

I don’t think it’s a good idea: invokable code detection should happen as early as possible due to its configuration in the getNativeDefinition() method (that’s why we perform this check in the constructor). Also, moving the code invocation to the execute() method will break the priority behavior, and thus affect some apps relying on ->setCode() having higher priority over execute() (which is currently the case).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My suggestion mainly moving changes from run() to execute(). I don't see how that would make a different.

Copy link
Member
@GromNaN GromNaN May 6, 2025

Choose a reason for hiding this comment

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

I opened #60361

The value of $this->code influences how the command definition is generated.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks @GromNaN

@crynobone crynobone marked this pull request as draft May 6, 2025 10:33
@crynobone crynobone force-pushed the configure-invokable-command branch from 089ea87 to f4503b6 Compare May 6, 2025 13:22
@crynobone crynobone changed the title [Console] Set InvokableCommand via a method instead of directly within __construct() [Console] Run InvokableCommand via execute() method. May 6, 2025
@crynobone crynobone marked this pull request as ready for review May 6, 2025 13:24
@carsonbot
Copy link

It looks like you unchecked the "Allow edits from maintainer" box. That is fine, but please note that if you have multiple commits, you'll need to squash your commits into one before this can be merged. Or, you can check the "Allow edits from maintainers" box and the maintainer can squash for you.

Cheers!

Carsonbot

@yceruto
Copy link
Member
yceruto commented May 6, 2025

Let's close here in favor of #60361

Thanks @crynobone for reporting it and suggest a solution!

chalasr added a commit that referenced this pull request May 6, 2025
…ority over `__invoke()` (GromNaN)

This PR was merged into the 7.3 branch.

Discussion
----------

[Console] Ensure overriding `Command::execute()` keeps priority over `__invoke()`

| Q             | A
| ------------- | ---
| Branch?       | 7.3
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| Issues        | Replace #60353
| License       | MIT

Implements #60353 (comment)

Commits
-------

f77c403 Ensure overriding Command::execute() keep priority over __invoke
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.

7 participants
0