tobento/service-console

使用Symfony Console作为默认实现的命令行界面。

1.0.4 2024-08-22 15:59 UTC

This package is auto-updated.

Last update: 2024-09-22 16:07:03 UTC


README

使用Symfony Console作为默认实现的命令行界面。

目录

入门

添加运行此命令的控制台服务项目的最新版本。

composer require tobento/service-console

需求

  • PHP 8.0或更高版本

亮点

  • 框架无关,与任何项目兼容
  • 解耦设计

文档

控制台

创建控制台

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)

添加命令

创建命令之后,您可以在控制台中添加它们

$console->addCommand(SampleCommand::class);

// or
$console->addCommand(new SampleCommand());

运行控制台

$console->run();

执行命令

您可能想执行一个命令而不是运行控制台。

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

带有命令类的示例

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

带有命令名称的示例

$console->addCommand(SampleCommand::class);

$console->execute(command: 'sample');

创建命令

命令

您可以使用Command::class来创建简单的命令。

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)

查看交互器部分了解更多信息。

参数和选项

参数详细说明

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

选项详细说明

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

抽象命令

只需扩展AbstractCommand::class即可创建更复杂的命令。

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

查看参数和选项以获取更多详细信息。

使用签名

您可以使用SIGNATURE作为定义您命令名称、描述、参数和选项的另一种方式。

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

参数

  • {name}必需,期望单个值
  • {name?}可选,期望单个值
  • {name[]}必需且可变,期望多个值
  • {name[]?}可选且可变,期望多个值
  • {name=}可选,默认值为null
  • {name=foo}可选,默认值为foo
  • {name=[foo,bar]}可选且可变,默认值为foobar

选项

  • {--name}作为布尔值,如果存在则为true,否则为false
  • {--n|name}n作为短名称
  • {--name=}默认值为null
  • {--name=foo}默认值为foo
  • {--name[]}可变,期望多个值
  • {--name=[foo,bar]}可变,默认值为foobar

通常情况下,选项是可选的!

交互器

交互器让您在处理命令的同时与控制台的输入和输出进行交互

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
        // ...
    });

检索参数和选项值

检索指定的输入参数和选项。参见参数和选项

// 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 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 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

检索原始输入

您可以使用rawInput方法检索传递给命令的原始输入。

// 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']

写入输出

$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');

写入格式化输出

您可以使用以下方式编写格式化的输出

$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</>');

编写表格

$io->table(
    headers: ['Name', 'Email'],
    rows: [
        ['Tom', 'tom@example.com'],
    ],
);

提问

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

密保问题

您可以提出一个密保问题

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

确认问题

您可以提出确认问题

if ($io->confirm('Do you wish to continue?', default: true)) {
    // ...
}

选择问题

您可以提出选择问题

$color = $io->choice(
    question: 'What color do you wish to use?',
    choices: ['red', 'blue'],
    default: 'red',
    multiselect: true,
);

进度条

$io->progressStart(max: 5);

foreach (range(0, 5) as $number) {
    sleep(1);
    $io->progressAdvance(step: 1);
}

$io->progressFinish();

详细程度级别

  • quiet 无消息输出
  • normal 正常输出
  • v 低详细程度
  • vv 中等详细程度
  • vvv 高详细程度
if ($io->isVerbose('vv')) {
    $io->write('Some Text');
}

// or:
$io->write('Some Text', 'v');
$io->write('Some Text', 'vv');
$io->write('Some Text', 'vvv');

命令参数

忽略验证错误参数

您可以将IgnoreValidationErrors参数添加到忽略验证错误,这在某些情况下可能很有用。

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

锁定

尚不支持。

信号

尚不支持。

事件

如果您在控制台定义了事件监听器,您可以监听以下事件

测试

您可以使用TestCommand::class来测试命令。

您可以通过查看Tobento\Service\Console\Test\TestCommandTest::class来获取示例。

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

传递输入参数和选项

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

传递容器或控制台

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
    }
}

带有输入

您可以使用返回新TestCommand::class实例的withInput方法

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自定义交互器

您可以使用interactorFactory参数创建自定义交互器

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)

致谢