Skip to content

[Console] Deprecate returning a non-int value from a \Closure function set via Command::setCode() #60076

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
Mar 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions UPGRADE-7.3.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ AssetMapper
Console
-------

* Omitting parameter types in callables configured via `Command::setCode()` method is deprecated
* Omitting parameter types or returning a non-integer value from a `\Closure` set via `Command::setCode()` method is deprecated

Before:

Expand All @@ -32,8 +32,10 @@ Console
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

$command->setCode(function (InputInterface $input, OutputInterface $output) {
$command->setCode(function (InputInterface $input, OutputInterface $output): int {
// ...

return 0;
});
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,11 @@ public function testRunOnlyWarnsOnUnregistrableCommand()
$kernel
->method('getBundles')
->willReturn([$this->createBundleMock(
[(new Command('fine'))->setCode(function (InputInterface $input, OutputInterface $output) { $output->write('fine'); })]
[(new Command('fine'))->setCode(function (InputInterface $input, OutputInterface $output): int {
$output->write('fine');

return 0;
})]
)]);
$kernel
->method('getContainer')
Expand Down Expand Up @@ -163,7 +167,11 @@ public function testRegistrationErrorsAreDisplayedOnCommandNotFound()
$kernel
->method('getBundles')
->willReturn([$this->createBundleMock(
[(new Command(null))->setCode(function (InputInterface $input, OutputInterface $output) { $output->write('fine'); })]
[(new Command(null))->setCode(function (InputInterface $input, OutputInterface $output): int {
$output->write('fine');

return 0;
})]
)]);
$kernel
->method('getContainer')
Expand Down Expand Up @@ -193,7 +201,11 @@ public function testRunOnlyWarnsOnUnregistrableCommandAtTheEnd()
$kernel
->method('getBundles')
->willReturn([$this->createBundleMock(
[(new Command('fine'))->setCode(function (InputInterface $input, OutputInterface $output) { $output->write('fine'); })]
[(new Command('fine'))->setCode(function (InputInterface $input, OutputInterface $output): int {
$output->write('fine');

return 0;
})]
)]);
$kernel
->method('getContainer')
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/Console/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ CHANGELOG
* Deprecate methods `Command::getDefaultName()` and `Command::getDefaultDescription()` in favor of the `#[AsCommand]` attribute
* Add support for Markdown format in `Table`
* Add support for `LockableTrait` in invokable commands
* Deprecate returning a non-integer value from a `\Closure` function set via `Command::setCode()`

7.2
---
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Component/Console/Command/Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ public function complete(CompletionInput $input, CompletionSuggestions $suggesti
*/
public function setCode(callable $code): static
{
$this->code = new InvokableCommand($this, $code, triggerDeprecations: true);
$this->code = new InvokableCommand($this, $code);

return $this;
}
Expand Down
10 changes: 6 additions & 4 deletions src/Symfony/Component/Console/Command/InvokableCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ class InvokableCommand
{
private readonly \Closure $code;
private readonly \ReflectionFunction $reflection;
private bool $triggerDeprecations = false;

public function __construct(
private readonly Command $command,
callable $code,
private readonly bool $triggerDeprecations = false,
) {
$this->code = $this->getClosure($code);
$this->reflection = new \ReflectionFunction($this->code);
Expand All @@ -49,17 +49,17 @@ public function __invoke(InputInterface $input, OutputInterface $output): int
{
$statusCode = ($this->code)(...$this->getParameters($input, $output));

if (null !== $statusCode && !\is_int($statusCode)) {
if (!\is_int($statusCode)) {
if ($this->triggerDeprecations) {
trigger_deprecation('symfony/console', '7.3', \sprintf('Returning a non-integer value from the command "%s" is deprecated and will throw an exception in Symfony 8.0.', $this->command->getName()));

return 0;
}

throw new LogicException(\sprintf('The command "%s" must return either void or an integer value in the "%s" method, but "%s" was returned.', $this->command->getName(), $this->reflection->getName(), get_debug_type($statusCode)));
throw new \TypeError(\sprintf('The command "%s" must return an integer value in the "%s" method, but "%s" was returned.', $this->command->getName(), $this->reflection->getName(), get_debug_type($statusCode)));
}

return $statusCode ?? 0;
return $statusCode;
}

/**
Expand All @@ -85,6 +85,8 @@ private function getClosure(callable $code): \Closure
return $code(...);
}

$this->triggerDeprecations = true;

if (null !== (new \ReflectionFunction($code))->getClosureThis()) {
return $code;
}
Expand Down
44 changes: 32 additions & 12 deletions src/Symfony/Component/Console/Tests/ApplicationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,10 @@ public function testRegister()

public function testRegisterAmbiguous()
{
$code = function (InputInterface $input, OutputInterface $output) {
$code = function (InputInterface $input, OutputInterface $output): int {
$output->writeln('It works!');

return 0;
};

$application = new Application();
Expand Down Expand Up @@ -1275,7 +1277,9 @@ public function testAddingOptionWithDuplicateShortcut()
->register('foo')
->setAliases(['f'])
->setDefinition([new InputOption('survey', 'e', InputOption::VALUE_REQUIRED, 'My option with a shortcut.')])
->setCode(function (InputInterface $input, OutputInterface $output) {})
->setCode(function (InputInterface $input, OutputInterface $output): int {
return 0;
})
;

$input = new ArrayInput(['command' => 'foo']);
Expand All @@ -1298,7 +1302,9 @@ public function testAddingAlreadySetDefinitionElementData($def)
$application
->register('foo')
->setDefinition([$def])
->setCode(function (InputInterface $input, OutputInterface $output) {})
->setCode(function (InputInterface $input, OutputInterface $output): int {
return 0;
})
;

$input = new ArrayInput(['command' => 'foo']);
Expand Down Expand Up @@ -1435,8 +1441,10 @@ public function testRunWithDispatcher()
$application->setAutoExit(false);
$application->setDispatcher($this->getDispatcher());

$application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output) {
$application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output): int {
$output->write('foo.');

return 0;
});

$tester = new ApplicationTester($application);
Expand Down Expand Up @@ -1491,8 +1499,10 @@ public function testRunDispatchesAllEventsWithExceptionInListener()
$application->setDispatcher($dispatcher);
$application->setAutoExit(false);

$application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output) {
$application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output): int {
$output->write('foo.');

return 0;
});

$tester = new ApplicationTester($application);
Expand Down Expand Up @@ -1559,8 +1569,10 @@ public function testRunAllowsErrorListenersToSilenceTheException()
$application->setDispatcher($dispatcher);
$application->setAutoExit(false);

$application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output) {
$application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output): int {
$output->write('foo.');

return 0;
});

$tester = new ApplicationTester($application);
Expand Down Expand Up @@ -1671,8 +1683,10 @@ public function testRunWithDispatcherSkippingCommand()
$application->setDispatcher($this->getDispatcher(true));
$application->setAutoExit(false);

$application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output) {
$application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output): int {
$output->write('foo.');

return 0;
});

$tester = new ApplicationTester($application);
Expand All @@ -1698,8 +1712,10 @@ public function testRunWithDispatcherAccessingInputOptions()
$application->setDispatcher($dispatcher);
$application->setAutoExit(false);

$application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output) {
$application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output): int {
$output->write('foo.');

return 0;
});

$tester = new ApplicationTester($application);
Expand Down Expand Up @@ -1728,8 +1744,10 @@ public function testRunWithDispatcherAddingInputOptions()
$application->setDispatcher($dispatcher);
$application->setAutoExit(false);

$application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output) {
$application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output): int {
$output->write('foo.');

return 0;
});

$tester = new ApplicationTester($application);
Expand Down Expand Up @@ -1858,12 +1876,12 @@ public function testFindAlternativesDoesNotLoadSameNamespaceCommandsOnExactMatch
'foo:bar' => function () use (&$loaded) {
$loaded['foo:bar'] = true;

return (new Command('foo:bar'))->setCode(function () {});
return (new Command('foo:bar'))->setCode(function (): int { return 0; });
},
'foo' => function () use (&$loaded) {
$loaded['foo'] = true;

return (new Command('foo'))->setCode(function () {});
return (new Command('foo'))->setCode(function (): int { return 0; });
},
]));

Expand Down Expand Up @@ -1934,8 +1952,10 @@ public function testThrowingErrorListener()
$application->setAutoExit(false);
$application->setCatchExceptions(false);

$application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output) {
$application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output): int {
$output->write('foo.');

return 0;
});

$tester = new ApplicationTester($application);
Expand Down
33 changes: 28 additions & 5 deletions src/Symfony/Component/Console/Tests/Command/CommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidOptionException;
use Symfony\Component\Console\Helper\FormatterHelper;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
Expand Down Expand Up @@ -350,8 +351,10 @@ public function testRunWithProcessTitle()
public function testSetCode()
{
$command = new \TestCommand();
$ret = $command->setCode(function (InputInterface $input, OutputInterface $output) {
$ret = $command->setCode(function (InputInterface $input, OutputInterface $output): int {
$output->writeln('from the code...');

return 0;
});
$this->assertEquals($command, $ret, '->setCode() implements a fluent interface');
$tester = new CommandTester($command);
Expand Down Expand Up @@ -396,8 +399,10 @@ public function testSetCodeWithStaticClosure()

private static function createClosure()
{
return function (InputInterface $input, OutputInterface $output) {
return function (InputInterface $input, OutputInterface $output): int {
$output->writeln(isset($this) ? 'bound' : 'not bound');

return 0;
};
}

Expand All @@ -411,16 +416,20 @@ public function testSetCodeWithNonClosureCallable()
$this->assertEquals('interact called'.\PHP_EOL.'from the code...'.\PHP_EOL, $tester->getDisplay());
}

public function callableMethodCommand(InputInterface $input, OutputInterface $output)
public function callableMethodCommand(InputInterface $input, OutputInterface $output): int
{
$output->writeln('from the code...');

return 0;
}

public function testSetCodeWithStaticAnonymousFunction()
{
$command = new \TestCommand();
$command->setCode(static function (InputInterface $input, OutputInterface $output) {
$command->setCode(static function (InputInterface $input, OutputInterface $output): int {
$output->writeln(isset($this) ? 'bound' : 'not bound');

return 0;
});
$tester = new CommandTester($command);
$tester->execute([]);
Expand Down Expand Up @@ -495,14 +504,28 @@ public function testDeprecatedMethods()

new FooCommand();
}

/**
* @group legacy
*/
public function testDeprecatedNonIntegerReturnTypeFromClosureCode()
{
$this->expectDeprecation('Since symfony/console 7.3: Returning a non-integer value from the command "foo" is deprecated and will throw an exception in Symfony 8.0.');

$command = new Command('foo');
$command->setCode(function () {});
$command->run(new ArrayInput([]), new NullOutput());
}
}

// In order to get an unbound closure, we should create it outside a class
// scope.
function createClosure()
{
return function (InputInterface $input, OutputInterface $output) {
return function (InputInterface $input, OutputInterface $output): int {
$output->writeln($this instanceof Command ? 'bound to the command' : 'not bound to the command');

return 0;
};
}

Expand Down
Loading
Loading