tobento / service-console
Command Line Interface using Symfony Console as default implementation.
Requires
- php: >=8.0
- psr/container: ^2.0
- psr/event-dispatcher: ^1.0
- symfony/console: ^6.0
- tobento/service-autowire: ^1.0.9
Requires (Dev)
- mockery/mockery: ^1.6
- phpunit/phpunit: ^9.5
- tobento/service-collection: ^1.0
- tobento/service-container: ^1.0.6
- tobento/service-event: ^1.0
- vimeo/psalm: ^4.0
README
Command Line Interface using Symfony Console as default implementation.
Table of Contents
- Getting started
- Documentation
- Credits
Getting started
Add the latest version of the console service project running this command.
composer require tobento/service-console
Requirements
- PHP 8.0 or greater
Highlights
- Framework-agnostic, will work with any project
- Decoupled design
Documentation
Console
Create Console
use Tobento\Service\Console\Symfony; use Tobento\Service\Console\ConsoleInterface; use Psr\Container\ContainerInterface; use Psr\EventDispatcher\EventDispatcherInterface; $console = new Symfony\Console( name: 'app', container: $container, // ContainerInterface // you may define a event dispatcher: eventDispatcher: $eventDispatcher, // EventDispatcherInterface ); var_dump($console instanceof ConsoleInterface); // bool(true)
Add Command
After Creating Commands you can add them in the console:
$console->addCommand(SampleCommand::class); // or $console->addCommand(new SampleCommand());
Run Console
$console->run();
Execute Command
You may want to execute a command instead of running the console.
use Tobento\Service\Console\ExecutedInterface; $executed = $console->execute( command: SampleCommand::class, // passing arguments and options as array: input: [ // passing arguments: 'username' => 'Tom', // with array value: 'username' => ['Tom', 'Tim'], // passing options: '--some-option' => 'value', // with array value: '--some-option' => ['value'], ], // or you may pass the command, arguments and options as string input: 'command:name Tom --bar=1' ); var_dump($executed instanceof ExecutedInterface); // bool(true) $command = $executed->command(); // string $code = $executed->code(); // int $output = $executed->output(); // string
Example with command class
use Tobento\Service\Console\Command; use Tobento\Service\Console\InteractorInterface; $command = (new Command(name: 'name')) ->handle(function(InteractorInterface $io): int { // do sth: return 0; }); $console->execute(command: $command);
Example with command name
$console->addCommand(SampleCommand::class); $console->execute(command: 'sample');
Creating Commands
Command
You may use the Command::class
to create simple commands.
use Tobento\Service\Console\Command; use Tobento\Service\Console\CommandInterface; use Tobento\Service\Console\InteractorInterface; $command = (new Command(name: 'mail:send')) // you may set a description: ->description('Send an email to a user(s)') // you may set a usage text: ->usage('Send emails ...') // you may add an argument(s): ->argument( name: 'user', description: 'The Id(s) of the user', variadic: true, ) // you may add an option(s): ->option( name: 'queue', description: 'Whether the email should be queued', ) // handle the command: ->handle(function(InteractorInterface $io, MailerInterface $mailer): int { // retrieve input arguments and options: $userIds = $io->argument('user'); $queue = $io->option('queue'); // send emails using the mailer... // you may write some output: $io->write(sprintf( 'email(s) send to user ids %s queued [%s]', implode(',', $userIds), $queue ? 'true' : 'false', )); return Command::SUCCESS; // return Command::FAILURE; // return Command::INVALID; }); var_dump($command instanceof CommandInterface); // bool(true)
Check out the Interactor section to learn more about it.
Arguments and Options
Arguments in detail
use Tobento\Service\Console\Command; $command = (new Command(name: 'sample')) ->argument( // The name of the argument: name: 'name', // you may define a description: description: 'Some description', // you may define a default value(s) (null default): value: ['foo', 'bar'], // mixed // set if the argument is optional (false default): optional: true, // if true expecting multiple values (false default): variadic: true, // not supported yet! suggestedValues: null, );
Options in detail
use Tobento\Service\Console\Command; $command = (new Command(name: 'sample')) ->option( // The name of the option: name: 'name', // you may define a description: description: 'Some description', // you may define a default value(s) (null default): value: ['foo', 'bar'], // mixed // variadic: variadic: null, // (default) // acts as boolean value, if exists true, otherwise false. variadic: false, // optional value (e.g. --name or --name=foo) if not specified default value is used. variadic: true, // is variadic expecting multiple values (e.g. --name=foo --name=bar). // if not specified default values are used. // not supported yet! suggestedValues: null, );
Abstract Command
Simply extend from the AbstractCommand::class
to create more complex commands.
use Tobento\Service\Console\AbstractCommand; use Tobento\Service\Console\InteractorInterface; class SendEmails extends AbstractCommand { /** * The command name. */ public const NAME = 'email:send'; /** * The command description. */ public const DESC = 'Send an email to a user(s)'; /** * The command usage text. */ public const USAGE = 'Send emails ...'; /** * Create a new instance. */ public function __construct() { // you may add an argument(s): $this->argument( name: 'user', description: 'The Id(s) of the user', variadic: true, ); // you may add an option(s): $this->option( name: 'queue', description: 'Whether the email should be queued', ); } /** * Handle the command. * * @param InteractorInterface $io * @return int The exit status code: * 0 SUCCESS * 1 FAILURE If some error happened during the execution * 2 INVALID To indicate incorrect command usage e.g. invalid options */ public function handle(InteractorInterface $io, MailerInterface $mailer): int { // retrieve input arguments and options: $userIds = $io->argument('user'); $queue = $io->option('queue'); // send emails using the mailer... // you may write some output: $io->write(sprintf( 'email(s) send to user ids %s queued [%s]', implode(',', $userIds), $queue ? 'true' : 'false', )); return 0; // or use the available constants: // return static::SUCCESS; // return static::FAILURE; // return static::INVALID; } }
Check out Arguments and Options for more detail.
Using Signature
You may use the SIGNATURE
as an alternative way to define the name, description, arguments and options of your command.
use Tobento\Service\Console\AbstractCommand; use Tobento\Service\Console\InteractorInterface; class SendEmails extends AbstractCommand { /** * The signature of the console command. */ public const SIGNATURE = ' mail:send | Send an email to a user(s) {user : The Id(s) of the user} {--queue : Whether the email should be queued} '; /** * Handle the command. * * @param InteractorInterface $io * @return int The exit status code: * 0 SUCCESS * 1 FAILURE If some error happened during the execution * 2 INVALID To indicate incorrect command usage e.g. invalid options */ public function handle(InteractorInterface $io): int { return 0; } }
Arguments
{name}
required, expecting single value{name?}
optional, expecting single value{name[]}
required and variadic, expecting multiple values{name[]?}
optional and variadic, expecting multiple values{name=}
optional withnull
as default value{name=foo}
optional withfoo
as default value{name=[foo,bar]}
optional and variadic withfoo
andbar
as default values
Options
{--name}
acts as boolean value, if existstrue
, otherwisefalse
{--n|name}
withn
as short name{--name=}
withnull
as default value{--name=foo}
withfoo
as default value{--name[]}
variadic, expecting multiple values{--name=[foo,bar]}
variadic withfoo
andbar
as default values
Options are optional in general!
Interactor
The interactor let you interact with the input and output from the console while handling your command:
use Tobento\Service\Console\Command; use Tobento\Service\Console\InteractorInterface; $command = (new Command(name: 'mail:send')) // handle the command: ->handle(function(InteractorInterface $io): int { // ... // use the interactor $io to interact // ... });
Retrieving Argument and Option Values
Retrieve input argument(s) and option(s) if specified. See Arguments and Options.
// Argument(s): $value = $io->argument(name: 'name'); // all values indexed by the argument name: $values = $io->arguments(); // Option(s): $value = $io->option(name: 'name'); // all values indexed by the option name: $values = $io->options();
Argument details
// Argument with variadic: false $value = $io->argument(name: 'name'); // NULL, if the argument was not passed when running the command // Not NULL, if the argument was passed when running the command // Argument with variadic: true $value = $io->argument(name: 'name'); // Array empty if the argument was not passed when running the command
Option details
// Option with variadic: null $value = $io->option(name: 'name'); // bool(false), if the option was not passed when running the command // bool(true), if the option was passed when running the command // Option with variadic: false $value = $io->option(name: 'name'); // NULL, if the option was not passed when running the command // Not NULL, if the option was passed when running the command // Option with variadic: true $value = $io->option(name: 'name'); // Array, empty if the option was not passed when running the command
Retrieving Raw Input
You may use the rawInput
method to retrieve the raw input that was passed to the command.
// if this command was run as: // php ap command:name foo --bar --baz=1 $rawInput = $io->rawInput(); // ['command:name', 'foo', '--bar', '--baz=1'] // you may exclude the command name: $rawInput = $io->rawInput(withoutCommandName: false); // ['foo', '--bar', '--baz=1']
Writing Output
$io->write('Some text'); $io->write('Some Text', 'newline'); $io->write('Some Text', newline: 1); // Write a single blank line: $io->newLine(); // Write three blank lines: $io->newLine(num: 3); // Write specific messages: $io->info('An info message'); $io->comment('A comment message'); $io->warning('A warning message'); $io->error('An error message'); $io->success('A success message');
Writing formatted output
You may write formatted output by the following way:
$io->write('<comment>Some text</comment>'); $io->write('<fg=red>Some text</>'); $io->write('<bg=red>Some text</>'); $io->write('<fg=white;bg=red>Some text</>');
Writing tables
$io->table( headers: ['Name', 'Email'], rows: [ ['Tom', 'tom@example.com'], ], );
Asking Questions
$name = $io->ask('What is your name?'); // With validator: $name = $io->ask('What is your name?', validator: function(string $answer): void { if ($answer !== 'something') { throw new \Exception('Your answer is incorrect'); } }); // With max attempts: $name = $io->ask('What is your name?', attempts: 2);
Secret question
You may ask a secret question:
$password = $io->secret('What is the password?'); // With validator: $name = $io->secret('What is your name?', validator: function(string $answer): void { if ($answer !== 'something') { throw new \Exception('Your answer is incorrect'); } }); // With max attempts: $name = $io->secret('What is your name?', attempts: 2);
Confirmation question
You may ask for confirmation:
if ($io->confirm('Do you wish to continue?', default: true)) { // ... }
Choice question
You may ask a choice question:
$color = $io->choice( question: 'What color do you wish to use?', choices: ['red', 'blue'], default: 'red', multiselect: true, );
Progress Bar
$io->progressStart(max: 5); foreach (range(0, 5) as $number) { sleep(1); $io->progressAdvance(step: 1); } $io->progressFinish();
Verbosity Levels
quiet
No message outputnormal
Normal outputv
Low verbosityvv
Medium verbosityvvv
High verbosity
if ($io->isVerbose('vv')) { $io->write('Some Text'); } // or: $io->write('Some Text', 'v'); $io->write('Some Text', 'vv'); $io->write('Some Text', 'vvv');
Command Parameters
Ignore Validation Errors Parameter
You may add the IgnoreValidationErrors
parameter to ignore validation errors which may be useful in some cases.
use Tobento\Service\Console\Command; use Tobento\Service\Console\InteractorInterface; use Tobento\Service\Console\Parameter; $command = (new Command(name: 'mail:send')) ->parameter(new Parameter\IgnoreValidationErrors()); // handle the command: ->handle(function(InteractorInterface $io): int { return Command::SUCCESS; });
Locking
Not supported yet.
Signals
Not supported yet.
Events
You may listen to the following events if you have defined a event listener in the console:
Testing
You may test commands using the TestCommand::class
.
You may check out the Tobento\Service\Console\Test\TestCommandTest::class
for examples.
use PHPUnit\Framework\TestCase; use Tobento\Service\Console\Test\TestCommand; class SampleCommandTest extends TestCase { public function testCommand() { (new TestCommand(command: SampleCommand::class)) // output expectations: ->expectsOutput('lorem') ->doesntExpectOutput('ipsum') ->expectsOutputToContain('lorem') ->doesntExpectOutputToContain('ipsum') ->expectsTable( headers: ['Name', 'Email'], rows: [ ['Tim', 'tom@example.com'], ], ) // questions expectations: ->expectsQuestion('What is your name?', answer: 'Tom') ->expectsQuestion('What colors do you wish to use?', answer: ['red', 'yellow']) ->expectsQuestion('Do you wish to continue?', answer: true) // exit code expectation: ->expectsExitCode(0) // execute test: ->execute(); } }
Passing input arguments and options
use PHPUnit\Framework\TestCase; use Tobento\Service\Console\Test\TestCommand; use Tobento\Service\Console\CommandInterface; class SampleCommandTest extends TestCase { public function testCommand() { (new TestCommand( command: SampleCommand::class, // string|CommandInterface input: [ // passing arguments: 'username' => 'Tom', // with array value: 'username' => ['Tom', 'Tim'], // passing options: '--some-option' => 'value', // with array value: '--some-option' => ['value'], // pass null for options with variadic: null '--some-option' => null, ], )) // set expectations: ->expectsOutput('lorem') ->expectsExitCode(0) // execute test: ->execute(); } }
Passing Container Or Console
use PHPUnit\Framework\TestCase; use Tobento\Service\Console\Test\TestCommand; use Tobento\Service\Console\ConsoleInterface; use Psr\Container\ContainerInterface; class SampleCommandTest extends TestCase { public function testCommand() { (new TestCommand(command: SampleCommand::class)) ->expectsExitCode(0) // if no dependencies ->execute() // null // passing the console to test on: ->execute($console) // ConsoleInterface // or just passing the container // (recommended way as console independent): ->execute($container); // ContainerInterface } }
With input
You may use the withInput
method returning a new TestCommand::class
instance:
use PHPUnit\Framework\TestCase; use Tobento\Service\Console\Test\TestCommand; class SampleCommandTest extends TestCase { private function command(): TestCommand { return new TestCommand(command: SampleCommand::class); } public function testCommand() { $this->command() ->withInput([ 'username' => 'Tom', ]) ->expectsExitCode(0) ->execute(); } }
Symfony
Symfony Console
use Tobento\Service\Console\Symfony; use Tobento\Service\Console\ConsoleInterface; use Psr\Container\ContainerInterface; use Psr\EventDispatcher\EventDispatcherInterface; $console = new Symfony\Console( name: 'app', container: $container, // ContainerInterface // you may define a event dispatcher: eventDispatcher: $eventDispatcher, // EventDispatcherInterface ); var_dump($console instanceof ConsoleInterface); // bool(true)
Symfony Custom Interactor
You may create a custom interactor using the interactorFactory
parameter:
use Tobento\Service\Console\Symfony; use Tobento\Service\Console\ConsoleInterface; use Tobento\Service\Console\InteractorInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Psr\Container\ContainerInterface; $interactorFactory = function( Command $command, InputInterface $input, OutputInterface $output ): InteractorInterface { return new Symfony\Interactor( command: $command, input: $input, output: $output, style: null, // null|SymfonyStyle ); // or create another Interactor fitting your needs }; $console = new Symfony\Console( name: 'app', container: $container, // ContainerInterface interactorFactory: $interactorFactory, ); var_dump($console instanceof ConsoleInterface); // bool(true)