console-extra icon indicating copy to clipboard operation
console-extra copied to clipboard

Single Command Application example

Open tacman opened this issue 2 years ago • 10 comments

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();

tacman avatar May 10 '23 12:05 tacman

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?

kbond avatar May 10 '23 16:05 kbond

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 :)

chalasr avatar May 10 '23 17:05 chalasr

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.

tacman avatar May 10 '23 21:05 tacman

Two classes in the same file?

I think that would work (both in the bin file).

kbond avatar May 10 '23 21:05 kbond

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();

tacman avatar Jun 09 '23 12:06 tacman

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?

kbond avatar Jun 09 '23 17:06 kbond

Oh, here's the issue:

-$application->setDefaultCommand($command->getName());
+$application->setDefaultCommand($command->getName(), isSingleCommand: true);

kbond avatar Jun 09 '23 17:06 kbond

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();

tacman avatar Jun 10 '23 15:06 tacman

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();
    }

tacman avatar Jun 18 '23 20:06 tacman

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.

kbond avatar Jun 20 '23 13:06 kbond