Single Command Application example
Is there a way to use this to create a single command application? I've gotten so used to using attributes to define arguments and options that I don't want to go back!
From https://symfony.com/doc/current/components/console/single_command_tool.html
#!/usr/bin/env php
<?php
require __DIR__.'/../../vendor/autoload.php';
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\SingleCommandApplication;
(new SingleCommandApplication())
->setName('My Super Command') // Optional
->setVersion('1.0.0') // Optional
->addArgument('foo', InputArgument::OPTIONAL, 'The directory')
->addOption('bar', null, InputOption::VALUE_REQUIRED)
->setCode(function (InputInterface $input, OutputInterface $output) {
$output->writeln("Success ");
// output arguments and options
})
->run();
I've gotten so used to using attributes to define arguments and options that I don't want to go back!
Hah!
I haven't really thought about this myself. Maybe something @chalasr could keep in mind when adding the attributes to symfony/console?
As for what you can do now, what about the alternate way to create a single command application? Something like:
#!/usr/bin/env php
<?php
require __DIR__.'/../../vendor/autoload.php';
$application = new Application('My Command', '1.0.0');
$application->addCommands([$command = new CommandWithZenstruckConsoleExtraAttributes()]);
$application->setDefaultCommand($command->getName());
$application->run();
Could this work?
Maybe something @chalasr could keep in mind when adding the attributes to symfony/console?
That's indeed a cool feature in the standalone console usage area that I do have in mind! Stay tuned, Symfony 6.4 development phase is around the corner :)
Could this work?
Wouldn't that mean there would be two files for every command? The shell and the class that uses it?
I have several simple utilities in various bundles, and I don't necessarily want to see all of those utilities with bin/console, e.g. bin/console mybundle:do-something, I'd rather have vendor/bin/mybundle do-something.
So I guess 2 classes would work, but better would be just one. Any ideas on how we could do that? Two classes in the same file? Obviously not PSR compliant though.
If it is 2 classes, I guess the shell would be in /bin, and the command in /src/Command as it currently is. So yes, that probably works. I'll play around with it.
I already have a maker that creates the src/Command part, I could add a --single to create something in /bin.
Two classes in the same file?
I think that would work (both in the bin file).
I'm getting close, but getting the error
Cannot add a required argument "msg" after an optional one "command".
If I remove the #[AsCommand()] attribute I get the folllowing, even though the name is defined.
PHP Fatal error: Uncaught Symfony\Component\Console\Exception\LogicException: The command defined in "CommandWithZenstruckConsoleExtraAttributes" cannot have an empty name. in /home/tac/ca/ac/vendor/symfony/console/Application.php:552
Stack trace:
#0 /home/tac/ca/ac/vendor/symfony/console/Application.php(522): Symfony\Component\Console\Application->add()
#!/usr/bin/env php
<?php
// bin/write-message.php
use Symfony\Component\Console\Attribute\AsCommand;
use Zenstruck\Console\Attribute\Argument;
use Zenstruck\Console\ConfigureWithAttributes;
use Zenstruck\Console\InvokableServiceCommand;
use Zenstruck\Console\RunsCommands;
use Zenstruck\Console\RunsProcesses;
$dir = __DIR__.'/../vendor/autoload.php';
assert(file_exists($dir), $dir);
include $dir;
#[AsCommand('app:write-message', 'Display a message')]
class CommandWithZenstruckConsoleExtraAttributes extends InvokableServiceCommand
{
use ConfigureWithAttributes, RunsCommands, RunsProcesses;
public function __invoke(
#[Argument(description: 'Message to display')] string $msg,
) {
$this->io()->writeln($msg);
}
}
$application = new \Symfony\Component\Console\Application('My Command', '1.0.0');
$application->addCommands([$command = new CommandWithZenstruckConsoleExtraAttributes()]);
$application->setDefaultCommand($command->getName());
$application->run();
Hmm, my first thought is to not be extending InvokableServiceCommand but just the standard Command from symfony/console. Then have the command use the Invokable trait.
Does that change anything?
Oh, here's the issue:
-$application->setDefaultCommand($command->getName());
+$application->setDefaultCommand($command->getName(), isSingleCommand: true);
Yes, this runs!
#!/usr/bin/env php
<?php
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Zenstruck\Console\Attribute\Argument;
use Zenstruck\Console\ConfigureWithAttributes;
use Zenstruck\Console\RunsCommands;
use Zenstruck\Console\RunsProcesses;
use Zenstruck\Console\Invokable;
include __DIR__.'/../vendor/autoload.php';
#[AsCommand('app:write-message', 'Display a message')]
class CommandWithZenstruckConsoleExtraAttributes extends Command
{
use ConfigureWithAttributes, RunsCommands, RunsProcesses;
use Invokable;
public function __invoke(
#[Argument(description: 'Message to display')] string $msg,
) {
$this->io()->writeln($msg);
}
}
$application = new \Symfony\Component\Console\Application('My Command', '1.0.0');
$application->addCommands([$command = new CommandWithZenstruckConsoleExtraAttributes()]);
$application->setDefaultCommand($command->getName(), isSingleCommand: true);
$application->run();
How do I inject services? I'm trying replace a bash script that's a bunch of bin/console calls to Symfony commands, and I'd like to wrap them in a single command script. But neither of the 2 injections here work:
public function __construct(
private KernelInterface $kernel,
string $name = null)
{
parent::__construct($name);
}
public function __invoke(
KernelInterface $kernel,
#[Argument(description: 'Message to display')] string $msg,
) {
$this->io()->writeln("Message is " . $msg);
// $this->runCommand($kernel, 'secrets:list');
}
private function runCommand(KernelInterface $kernel, string $cli)
{
$application = new Application($kernel);
$command = $application->get($cli);
$cliString = '-v';
CommandRunner::from($application, $cliString)
->withOutput($this->io()->output()) // any OutputInterface
->run();
}
How do I inject services?
Is this still in a single command app? If so, you won't have access to the container for DI.