Supercharge your Symfony console commands!

Overview

zenstruck/console-extra

CI Status codecov

A modular set of features to reduce configuration boilerplate for your commands:

/**
 * Creates a user in the database.
 *
 * @command create:user email password --r|role[]
 */
final class CreateUserCommand extends InvokableServiceCommand
{
    use ConfigureWithDocblocks;

    public function __invoke(IO $io, UserRepository $repo): void
    {
        $repo->createUser($io->argument('email'), $io->argument('password'), $io->option('role'));

        $io->success('Created user.');
    }
}
bin/console create:user kbond p4ssw0rd -r ROLE_EDITOR -r ROLE_ADMIN

 [OK] Created user.

 // Duration: < 1 sec, Peak Memory: 10.0 MiB

Installation

composer require zenstruck/console-extra

Usage

This library is a set of modular features that can be used separately or in combination.

TIP: To reduce command boilerplate even further, it is recommended to create an abstract base command for your app that enables all the features you desire. Then have all your app's commands extend this.

IO

This is a helper object that extends SymfonyStyle and implements InputInterface (so it implements InputInterface, OutputInterface, and StyleInterface).

use Zenstruck\Console\IO;

$io = new IO($input, $output);

$io->getOption('role'); // InputInterface
$io->writeln('a line'); // OutputInterface
$io->success('Created.'); // StyleInterface

// additional methods
$io->input(); // get the "wrapped" input
$io->output(); // get the "wrapped" output

On its own, it isn't very special, but it can be auto-injected into Invokable commands.

Invokable

Use this trait to remove the need for extending Command::execute() and just inject what your need (ie IO) into your command's __invoke() method.

use Symfony\Component\Console\Command\Command;
use Zenstruck\Console\Invokable;
use Zenstruck\Console\IO;

class MyCommand extends \Symfony\Component\Console\Command\Command
{
    use Invokable;

    public function __invoke(IO $io)
    {
        $role = $io->option('role');

        $io->success('created.');
    }
}

You can auto-inject the "raw" input/output:

public function __invoke(IO $io, InputInterface $input, OutputInterface $output)

No return type (or void) implies a 0 status code. You can return an integer if you want to change this:

public function __invoke(IO $io): int
{
    return $success ? 0 : 1;
}

InvokableServiceCommand

If using the Symfony Framework, you can take Invokable to the next level by auto-injecting services into __invoke(). This allows your commands to behave like Invokable Service Controllers (with controller.service_arguments). Instead of a Request, you inject IO.

Have your commands extend InvokableServiceCommand and ensure they are auto-wired/configured.

use App\Repository\UserRepository;
use Psr\Log\LoggerInterface;
use Zenstruck\Console\InvokableServiceCommand;
use Zenstruck\Console\IO;

class CreateUserCommand extends InvokableServiceCommand
{
    public function __invoke(IO $io, UserRepository $repo, LoggerInterface $logger): void
    {
        // ...
    }
}

AutoName

Use this trait to have your command's name auto-generated from the class name:

use Symfony\Component\Console\Command\Command;
use Zenstruck\Console\AutoName;

class CreateUserCommand extends Command
{
    use AutoName; // command's name will be "app:create-user"
}

ConfigureWithDocblocks

Use this trait to allow your command to be configured by your command class' docblock. phpdocumentor/reflection-docblock is required for this feature (composer install phpdocumentor/reflection-docblock).

Example:

use Symfony\Component\Console\Command\Command;
use Zenstruck\Console\ConfigureWithDocblocks;

/**
 * This is the command's description.
 *
 * This is the command help
 *
 * Multiple
 *
 * lines allowed.
 *
 * @command my:command
 * @alias alias1
 * @alias alias2
 * @hidden
 *
 * @argument arg1 First argument is required (this is the argument's "description")
 * @argument ?arg2 Second argument is optional
 * @argument arg3=default Third argument is optional with a default value
 * @argument arg4="default with space" Forth argument is "optional" with a default value (with spaces)
 * @argument ?arg5[] Fifth argument is an optional array
 *
 * @option option1 First option (no value) (this is the option's "description")
 * @option option2= Second option (value required)
 * @option option3=default Third option with default value
 * @option option4="default with space" Forth option with "default" value (with spaces)
 * @option o|option5[] Fifth option is an array with a shortcut (-o)
 */
class MyCommand extends Command
{
    use ConfigureWithDocblocks;
}

NOTES:

  1. If the @command tag is absent, AutoName is used.
  2. All the configuration can be disabled by using the traditional methods of configuring your command.
  3. Command's are still lazy using this method of configuration but there is overhead in parsing the docblocks so be aware of this.

@command Tag

You can pack all the above into a single @command tag. This can act like routing for your console:

/**
 * @command |app:my:command|alias1|alias2 arg1 ?arg2 arg3=default arg4="default with space" ?arg5[] --option1 --option2= --option3=default --option4="default with space" --o|option5[]
 */
class MyCommand extends Command
{
    use ConfigureWithDocblocks;
}

NOTES:

  1. The | prefix makes the command hidden.
  2. Argument/Option descriptions are not allowed.

TIP: It is recommended to only do this for very simple commands as it isn't as explicit as splitting the tags out.

CommandSummarySubscriber

Add this event subscriber to your Application's event dispatcher to display a summary after every command is run. The summary includes the duration of the command and peak memory usage.

If using Symfony, configure it as a service to enable:

# config/packages/zenstruck_console_extra.yaml
Zenstruck\Console\EventListener\CommandSummarySubscriber:
    autoconfigure: true

NOTE: This will display a summary after every registered command runs.

Comments
  • --help not working in command runner

    --help not working in command runner

    Using the MyCommand example in the documentation

    use Zenstruck\Console\CommandRunner;
    class AppController extends AbstractController
    {
    
        #[Route(path: '/run-command', name: 'app_run_command')]
        public function runCommand(MyCommand $command): Response
        {
            CommandRunner::for($command, 'Bob p@ssw0rd --role ROLE_ADMIN')->run(); // works great
            CommandRunner::for($command, '--help')->run(); // fails, says --help isn't defined
        }
    }
    
    opened by tacman 5
  • How to configure an invokable command within a bundle

    How to configure an invokable command within a bundle

    I'd like to autowire an invokable command from within a bundle. While I simply auto-wire it, I get this error:

      Cannot autowire service "Survos\Bundle\MakerBundle\Command\MakeMethodCommand": argument "$container" of method "Zenstruck\Console\InvokableServiceCommand::setInvokeContainer()" references interface "Psr\Container  
      \ContainerInterface" but no such service exists. Available autowiring aliases for this interface are: "$parameterBag".                                                       
    

    So I added a method call to the container (guessing, really)

            $builder->autowire(MyCommand::class)
                ->addTag('console.command')
                ->addMethodCall('setInvokeContainer', [new Reference('service_container')])
            ;
    

    And now get:

    In Container.php line 235:
                                                                                                                                                                                                                            
      The "Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface" service or alias has been removed or inlined when the container was compiled. You should either make it public, or stop using the con  
      tainer directly and use dependency injection instead.                                                                                                                                                                 
              
    

    What is the correct way to autowire the command? Does it need to happen in the compilerPass?

    opened by tacman 4
  • How to set the command help

    How to set the command help

    bin/console my:command --help 
    bin/console make:crud --help 
    

    Symfony's make:crud shows some help lines at the bottom, how is that set in an invokable command? I tried adding this to __invoke, but it didn't seem to work (although again, it might be related to lazy-loading).

            $this
                // the command help shown when running the command with the "--help" option
                ->setHelp('This is the help, not the description, and can be multiple lines.');
    
    opened by tacman 3
  • --help not working in command runner

    --help not working in command runner

    Using the MyCommand example in the documentation

    use Zenstruck\Console\CommandRunner;
    class AppController extends AbstractController
    {
    
        #[Route(path: '/run-command', name: 'app_run_command')]
        public function runCommand(MyCommand $command): Response
        {
            CommandRunner::for($command, 'Bob p@ssw0rd --role ROLE_ADMIN')->run(); // works great
            CommandRunner::for($command, '--help')->run(); // fails, says --help isn't defined
        }
    }
    
    opened by tacman 1
  • Detect arguments/options from `__invoke()` method

    Detect arguments/options from `__invoke()` method

    public function __invoke(
        #[Argument(...see InputArgument constructor...)]
        string $arg1,
    
        #[Option(...see InputOption constructor...)]
        string $opt1,
    )
    
    enhancement 
    opened by kbond 0
  • Allow injecting command args/options into `__invoke()`

    Allow injecting command args/options into `__invoke()`

    class MyCommand extends \Symfony\Component\Console\Command\Command
    {
        use Invokable;
    
        // $username/$roles are the argument/option defined below
        public function __invoke(IO $io, string $username, array $roles)
        {
            $io->success('created.');
    
            // even if you don't inject IO, it's available as a method:
            $this->io(); // IO
        }
    
        public function configure(): void
        {
            $this
                ->addArgument('username', InputArgument::REQUIRED)
                ->addOption('roles', mode: InputOption::VALUE_IS_ARRAY)
            ;
        }
    }
    
    enhancement 
    opened by kbond 0
  • `ParameterBagInterface` available in `InvokableServiceCommand`

    `ParameterBagInterface` available in `InvokableServiceCommand`

    final class CreateUserCommand extends InvokableServiceCommand
    {
        public function __invoke(IO $io): void
        {
            $env = $this->parameter('kernel.environment');
        }
    }
    
    enhancement 
    opened by kbond 0
  • `ExecutesProcess` trait

    `ExecutesProcess` trait

    class MyCommand extends Command
    {
        use Invokable, ExecutesProcess;
    
        public function __invoke(IO $io)
        {
            $this->executeProcess('/some/script');
    
            // hide sensitive strings (replaces with XXX)
            $this->executeProcess('/some/script', ['my-database-password']);
        }
    }
    
    enhancement 
    opened by kbond 0
  • `ExecutesCommands` trait

    `ExecutesCommands` trait

    class MyCommand extends Command
    {
        use Invokable, ExecutesCommands;
    
        public function __invoke(IO $io)
        {
            $this->executeCommand('doctrine:database:drop', ['--force' => true]);
    
            // equivalent to above
            $this->executeCommand('doctrine:database:drop --force');
        }
    }
    
    enhancement 
    opened by kbond 0
  • Add Invoke Argument Factories

    Add Invoke Argument Factories

    Added via event or base command constructor.

    Use Case:

    Would give the ability to change the IO style w/o updating typehints.

    $command->useArgumentFactory(IO::class, fn($input, $output) => new CustomIO($input, $output));
    
    enhancement 
    opened by kbond 0
  • `ConfigureWithAttributes`

    `ConfigureWithAttributes`

    Add Argument/Option attributes:

    #[Argument('arg1', Argument::REQUIRED, 'some description', 'default')]
    #[Option('option1', 'o', Option::VALUE_Required, 'some description', 'default')]
    class MyCommand extends Command {}
    
    enhancement 
    opened by kbond 0
  • Support using DI attributes in `__invoke()` (requires Symfony 6.2)

    Support using DI attributes in `__invoke()` (requires Symfony 6.2)

    Unlike other services, injecting a stateMachine during invoke throws an exception:

        public function __invoke(
            IO                     $io,
            WorkflowInterface  $catalogStateMachine
    
    In CheckExceptionOnInvalidReferenceBehaviorPass.php line 83:
                                                                                                                                         
      The service ".service_locator.Va9WIi3" has a dependency on a non-existent service "Symfony\Component\Workflow\WorkflowInterface".  
                                                                                                                                         
    

    I wonder if it has to do with the ability to inject a specific service by name, rather than just type. Injecting via the constructor works as expected.

        public function __construct(private WorkflowInterface  $catalogStateMachine, string $name = null)
        {
            parent::__construct($name);
        }
    
        public function __invoke(
            IO                     $io,
    
    enhancement 
    opened by tacman 2
  • change default name of options to kebab-case

    change default name of options to kebab-case

            #[Option(description: 'do not actually update')] // default name: 'dry-run' ? 
            bool    $dryRun,
    

    It seems to me that the default name should be 'dry-run', since I don't think camelCase is used as frequently.

    https://commons.apache.org/proper/commons-cli/

    bin/console do:something --dryRun
    bin/console do:something --dry-run
    

    All Symfony commands:

      -n, --no-interaction  Do not ask any interactive question
      -e, --env=ENV         The Environment name. [default: "dev"]
          --no-debug        Switch off debug mode.
    

    Of course, that's a BC change.

    opened by tacman 1
  • argument and option validation?

    argument and option validation?

    Can the invokable commands use validation? That is

    Since you're looking at Closures for auto-completion, I wanted to brainstorm about the idea of assertions / validations in the commands.

    One approach:

    #[Argument(description: 'path to image directory', assert: fn(string $path) => is_dir($path), msg: "%s is not a valid path")]
    string $path,
    
    // to replace
    
    ): void {
    
        if (!is_dir($path)) {
            throw new \LogicException("$path is not a directory");
        }
    

    Perhaps the assertion could also take a Symfony Expression?

    It's not just individual elements that need validation, it might be the whole set (like a form validation).

    #[Argument(description: 'image to process', assert: [$this, 'fileExists'])]
    ?string $imageFilename,
    
    private function fileExists() {
        $pathToImage = $this->path . $this->imageFilename;
        if (!file_exists($pathToImage)) {
            throw new \LogicException("$pathToImage is not a file");
        }
    

    Of course, even better would be to use the Symfony validators that already exist:

    #[AsCommand('app:create-record', 'Create an image record with associated PDF')]
    final class MyCommand extends InvokableServiceCommand
    {
        use ConfigureWithAttributes, RunsCommands, RunsProcesses;
    
        public function __invoke(
            IO      $io,
    
            #[Argument(description: 'path to pdf')]
            #[Assert\File(
                mimeTypes: ['application/pdf', 'application/x-pdf'],
                mimeTypesMessage: 'Please provide a valid PDF',
            )]
            string  $pdf,
    
            #[Argument(description: 'image filename')]
            #[Assert\Image()]
            string  $image,
    
            #[Option(description: 'path to pdf')]
            #[Assert\File()]
            ?string $pdfPath="~/Documents/",
    
    

    The Constraint would need to be extended, to allow for the property: \Attribute::TARGET_PARAMETER

    #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE | \Attribute::TARGET_PARAMETER)]
    class File extends Constraint
    

    Again, just brainstorming. It feels like there's an elegant solution in here somewhere.

    opened by tacman 0
  • Auto-complete / suggested values

    Auto-complete / suggested values

    Is there a way to add suggested values (as an array or closure) to invokable commands? From the Symfony console documentation at https://symfony.com/doc/current/console/input.html#adding-argument-option-value-completion

    // ...
    use Symfony\Component\Console\Completion\CompletionInput;
    use Symfony\Component\Console\Completion\CompletionSuggestions;
    
    class GreetCommand extends Command
    {
        // ...
        protected function configure(): void
        {
            $this
                ->addArgument(
                    'names',
                    InputArgument::IS_ARRAY,
                    'Who do you want to greet (separate multiple names with a space)?',
                    null,
                    function (CompletionInput $input) {
                        // the value the user already typed, e.g. when typing "app:greet Fa" before
                        // pressing Tab, this will contain "Fa"
                        $currentValue = $input->getCompletionValue();
    
                        // get the list of username names from somewhere (e.g. the database)
                        // you may use $currentValue to filter down the names
                        $availableUsernames = ...;
    
                        // then suggested the usernames as values
                        return $availableUsernames;
                    }
                )
            ;
        }
    }
    

    Is there a way to enhance the configuration? Maybe something like

    $argument = $this->getArgument('names');
    $argument->setSuggestedValues(function (...));
    

    Obviously, I'm trying to see if it's possible to switch completely to invokable commands.

    opened by tacman 6
  • How to set the command help

    How to set the command help

    bin/console my:command --help 
    bin/console make:crud --help 
    

    Symfony's make:crud shows some help lines at the bottom, how is that set in an invokable command? I tried adding this to __invoke, but it didn't seem to work (although again, it might be related to lazy-loading).

            $this
                // the command help shown when running the command with the "--help" option
                ->setHelp('This is the help, not the description, and can be multiple lines.');
    
    opened by tacman 4
Releases(v1.1.0)
  • v1.1.0(Jul 12, 2022)

    04f4309 [minor] deprecate ConfigureWithDocblocks (#30) by @kbond edc10ea [minor] rename tests (#30) by @kbond dedbab2 [feature] define opts/args on __invoke() params with Argument|Option (#27) by @kbond 7717dad [minor] cs fix by @kbond a1e132f [feature] allow injecting args/options into __invoke() (#25) by @kbond

    Full Change List

    Source code(tar.gz)
    Source code(zip)
  • v1.0.1(May 26, 2022)

    09b1f70 [minor] move setting Invokable::$io to initialize method by @kbond d6ebf15 [doc] fix CommandSummarySubscriber config by @kbond 0ca67cc [minor] use Required attribute when possible by @kbond 27c1fa1 [minor] support Symfony 6.1 (#24) by @kbond

    Full Change List

    Source code(tar.gz)
    Source code(zip)
  • v1.0.0(Apr 8, 2022)

  • v0.2.0(Apr 1, 2022)

    4bdd457 [minor] adjust sca gh action (#22) by @kbond a52f85f [feature] add ConfigureWithAttributes (#22) by @kbond c187693 [feature] add RunsProcesses trait (#22) by @kbond 81d6048 [feature] add CommandRunner & RunsCommands trait (#22) by @kbond 5ce72e4 [feature] add Invokable::io() (#22) by @kbond f9428ee [feature] add InvokableServiceCommand::parameter() (#21) by @kbond 8f03882 [minor] Remove necessary ltrim call (#17) by @jdreesen 9ca25dc [minor] add static code analysis with phpstan (#16) by @kbond

    Full Change List

    Source code(tar.gz)
    Source code(zip)
  • v0.1.1(Dec 1, 2021)

  • v0.1.0(Oct 26, 2021)

Owner
Kevin Bond
Kevin Bond
Console - The Console component eases the creation of beautiful and testable command line interfaces.

Console Component The Console component eases the creation of beautiful and testable command line interfaces. Sponsor The Console component for Symfon

Symfony 9.4k Jan 7, 2023
ReactPHP Shell, based on the Symfony Console component.

Pecan Event-driven, non-blocking shell for ReactPHP. Pecan (/pɪˈkɑːn/) provides a non-blocking alternative to the shell provided in the Symfony Consol

Michael Crumm 43 Sep 4, 2022
Display your Laravel routes in the console, but make it pretty. 😎

Pretty Routes for Laravel Display your Laravel routes in the console, but make it pretty. ?? Installation You can install the package via composer: co

Alex 630 Dec 30, 2022
A Cli tool to save you time, and gives you the power to scaffold all of your models,controllers,commands

A Cli tool to save you time, and gives you the power to scaffold all of your models,controllers,commands... at once Installation You can install the p

Coderflex 16 Nov 11, 2022
The Hoa\Console library.

Hoa is a modular, extensible and structured set of PHP libraries. Moreover, Hoa aims at being a bridge between industrial and research worlds. Hoa\Con

Hoa 366 Dec 14, 2022
Customized loading ⌛ spinner for Laravel Artisan Console.

Laravel Console Spinner Laravel Console Spinner was created by Rahul Dey. It is just a custom Progress Bar inspired by icanhazstring/symfony-console-s

Rahul Dey 71 Oct 26, 2022
It's like Tailwind CSS, but for the console.

Tailcli allows building unique, beautiful command-line applications, using tailwind classes. It's like Tailwind CSS, but for the console. Installation

Nuno Maduro 1.8k Jan 7, 2023
Laravel Console Toolkit

This Package provides some usefully console features like the attribute syntax for arguments and options, validation, auto ask and casting.

Tobi 29 Nov 29, 2022
Console component from Zend Framework

zend-console Repository abandoned 2019-12-31 This repository has moved to laminas/laminas-console. Zend\Console is a component to design and implement

Zend Framework 47 Mar 16, 2021
Simple and customizable console log output for CLI apps.

Console Pretty Print Simple and customizable console log output for CLI apps. Highlights Simple installation (Instalação simples) Very easy to customi

William Alvares 3 Aug 1, 2022
[ABANDONED] PHP library for executing commands on multiple remote machines, via SSH

#Shunt Inspired by Ruby's Capistrano, Shunt is PHP library for executing commands on multiple remote machines, via SSH. Specifically, this library was

The League of Extraordinary Packages 436 Feb 20, 2022
Library for creating CLI commands or applications

Console Motivation: this library purpose is to provide a lighter and more robust API for console commands and/or applications to symfony/console. It c

Théo FIDRY 16 Dec 28, 2022
A developer-friendly wrapper around execution of shell commands.

ptlis/shell-command A developer-friendly wrapper around execution of shell commands. There were several goals that inspired the creation of this packa

brian ridley 18 Dec 31, 2022
Handle signals in artisan commands

Using this package you can easily handle signals like SIGINT, SIGTERM in your Laravel app.

Spatie 96 Dec 29, 2022
A package built for lumen that ports most of the make commands from laravel.

A package built for lumen that ports most of the make commands from laravel. For lumen v5.1, but will most likely work for 5.2 as well. I haven't tested. If you have requests, let me know, or do it yourself and make a pull request

Michael Bonds 22 Mar 8, 2022
Simple but yet powerful library for running almost all artisan commands.

:artisan gui Simple but yet powerful library for running some artisan commands. Requirements Laravel 8.* php ^7.3 Installation Just install package: c

null 324 Dec 28, 2022
A simple object oriented interface to execute shell commands in PHP

php-shellcommand php-shellcommand provides a simple object oriented interface to execute shell commands. Installing Prerequisites Your php version mus

Michael Härtl 283 Dec 10, 2022
PHP library for executing commands on multiple remote machines, via SSH

#Shunt Inspired by Ruby's Capistrano, Shunt is PHP library for executing commands on multiple remote machines, via SSH. Specifically, this library was

The League of Extraordinary Packages 436 Feb 20, 2022