zenstruck/console-extra

一套模块化功能,用于减少Symfony命令的配置冗余。

v1.4.0 2024-02-19 13:23 UTC

This package is auto-updated.

Last update: 2024-09-20 20:26:24 UTC


README

CI codecov

一套模块化功能,用于减少Symfony命令的配置冗余

#[AsCommand('create:user', 'Creates a user in the database.')]
final class CreateUserCommand extends InvokableServiceCommand
{
    use RunsCommands, RunsProcesses;

    public function __invoke(
        IO $io,

        UserManager $userManager,

        #[Argument]
        string $email,

        #[Argument]
        string $password,

        #[Option(name: 'role', shortcut: 'r', suggestions: UserMananger::ROLES)]
        array $roles,
    ): void {
        $userManager->createUser($email, $password, $roles);

        $this->runCommand('another:command');
        $this->runProcess('/some/script');

        $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

安装

composer require zenstruck/console-extra

用法

这个库是一组模块化功能,可以单独使用或组合使用。

注意

为了进一步减少命令的冗余,建议为您的应用程序创建一个抽象基命令,以启用您需要的所有功能。然后让所有应用程序的命令扩展这个基命令。

IO

这是一个扩展了 SymfonyStyle 并实现了 InputInterface(因此实现了 InputInterfaceOutputInterfaceStyleInterface)的辅助对象。

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

单独来看,它并不特别,但它可以自动注入到 Invokable 命令中。

可调用命令

扩展这个类以消除扩展 Command::execute() 的需要,只需将所需内容注入到命令的 __invoke() 方法中。以下是可以自动注入的参数:

  • Zenstruck\Console\IO
  • Symfony\Component\Console\Style\StyleInterface
  • Symfony\Component\Console\Input\InputInterface
  • Symfony\Component\Console\Input\OutputInterface
  • arguments(参数名称必须与参数名称匹配或使用 Zenstruck\Console\Attribute\Argument 属性)
  • options(参数名称必须与选项名称匹配或使用 Zenstruck\Console\Attribute\Option 属性)
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Zenstruck\Console\InvokableCommand;
use Zenstruck\Console\IO;

class MyCommand extends InvokableCommand
{
    // $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)
        ;
    }
}

您可以自动注入“原始”输入/输出

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

没有返回类型(或 void)意味着 0 状态代码。如果您想更改此设置,可以返回一个整数

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

可调用服务命令

如果您使用Symfony框架,您可以通过将服务自动注入到 __invoke() 中将 InvokableCommand 提升到更高的层次。这允许您的命令表现得像 可调用服务控制器(带有 controller.service_arguments)。您注入 IO 而不是 Request

让您的命令扩展 InvokableServiceCommand 并确保它们自动装配/配置。

use App\Service\UserManager;
use Psr\Log\LoggerInterface;
use Zenstruck\Console\InvokableServiceCommand;
use Zenstruck\Console\IO;

class CreateUserCommand extends InvokableServiceCommand
{
    public function __invoke(IO $io, UserManager $userManager, LoggerInterface $logger): void
    {
        // access container parameters
        $environment = $this->parameter('kernel.environment');

        // ...
    }
}

使用DI属性注入

您可以在您的 __invoke() 参数上使用任何 DI属性

use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\Attribute\Target;
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
use Zenstruck\Console\InvokableServiceCommand;

class SomeCommand extends InvokableServiceCommand
{
    public function __invoke(
        #[Autowire('@some.service.id')]
        SomeService $service,

        #[Autowire('%kernel.environment%')]
        string $environment,

        #[Target('githubApi')]
        HttpClientInterface $httpClient,

        #[TaggedIterator('app.handler')]
        iterable $handlers,
    ): void {
        // ...
    }
}

使用属性进行配置

扩展 InvokableCommandInvokableServiceCommand 的命令可以使用属性配置参数和选项

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Zenstruck\Console\Attribute\Argument;
use Zenstruck\Console\Attribute\Option;
use Zenstruck\Console\InvokableCommand;

#[Argument('arg1', description: 'Argument 1 description', mode: InputArgument::REQUIRED)]
#[Argument('arg2', description: 'Argument 1 description')]
#[Argument('arg3', suggestions: ['suggestion1', 'suggestion2'])] // for auto-completion
#[Argument('arg4', suggestions: 'suggestionsForArg4')] // use a method on the command to get suggestions
#[Option('option1', description: 'Option 1 description')]
#[Option('option2', suggestions: ['suggestion1', 'suggestion2'])] // for auto-completion
#[Option('option3', suggestions: 'suggestionsForOption3')] // use a method on the command to get suggestions
class MyCommand extends InvokableCommand
{
    // ...

    private function suggestionsForArg4(): array
    {
        return ['suggestion3', 'suggestion4'];
    }

    private function suggestionsForOption3(): array
    {
        return ['suggestion3', 'suggestion4'];
    }
}

可调用属性

您可以直接在 __invoke() 参数上添加 Option/Argument 属性来定义和注入参数/选项,而不是在类级别定义。

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Zenstruck\Console\Attribute\Argument;
use Zenstruck\Console\Attribute\Option;
use Zenstruck\Console\InvokableCommand;

#[AsCommand('my:command')]
class MyCommand extends InvokableCommand
{
    public function __invoke(
        #[Argument]
        string $username, // defined as a required argument (username)

        #[Argument]
        string $password = 'p4ssw0rd', //  defined as an optional argument (password) with a default (p4ssw0rd)

        #[Option(name: 'role', shortcut: 'r', suggestions: ['ROLE_EDITOR', 'ROLE_REVIEWER'])]
        array $roles = [], // defined as an array option that requires values (--r|role[])

        #[Option(name: 'super-admin')]
        bool $superAdmin = false, // defined as a "value-less" option (--super-admin)

        #[Option]
        ?bool $force = null, // defined as a "negatable" option (--force/--no-force)

        #[Option]
        ?string $name = null, // defined as an option that requires a value (--name=)
    ): void {
        // ...
    }
}

注意

选项/参数 模式默认值 由参数的类型提示/默认值检测,不能在属性中定义。

CommandRunner

有一个 CommandRunner 对象可用于在任何地方(例如控制器)简化命令的运行。

use Zenstruck\Console\CommandRunner;

/** @var \Symfony\Component\Console\Command\Command $command */

CommandRunner::for($command)->run(); // int (the status after running the command)

// pass arguments
CommandRunner::for($command, 'arg --opt')->run(); // int

如果应用程序可用,您可以使用它来运行命令。

use Zenstruck\Console\CommandRunner;

/** @var \Symfony\Component\Console\Application $application */

CommandRunner::from($application, 'my:command')->run();

// pass arguments/options
CommandRunner::from($application, 'my:command arg --opt')->run(); // int

如果您的命令是交互式的,您可以传递输入。

use Zenstruck\Console\CommandRunner;

/** @var \Symfony\Component\Console\Application $application */

CommandRunner::from($application, 'my:command')->run([
    'foo', // input 1
    '', // input 2 (<enter>)
    'y', // input 3
]);

默认情况下,输出被抑制,您可以可选地捕获输出。

use Zenstruck\Console\CommandRunner;

/** @var \Symfony\Component\Console\Application $application */

$output = new \Symfony\Component\Console\Output\BufferedOutput();

CommandRunner::from($application, 'my:command')
    ->withOutput($output) // any OutputInterface
    ->run()
;

$output->fetch(); // string (the output)

运行命令

您可以通过使用RunsCommands特质来赋予您的可调用命令运行其他命令(在应用中定义)的能力。这些子命令将使用与父命令相同的输出

use Symfony\Component\Console\Command;
use Zenstruck\Console\InvokableCommand;
use Zenstruck\Console\RunsCommands;

class MyCommand extends InvokableCommand
{
    use RunsCommands;

    public function __invoke(): void
    {
        $this->runCommand('another:command'); // int (sub-command's run status)

        // pass arguments/options
        $this->runCommand('another:command arg --opt');

        // pass inputs for interactive commands
        $this->runCommand('another:command', [
            'foo', // input 1
            '', // input 2 (<enter>)
            'y', // input 3
        ])
    }
}

RunsProcesses

您可以通过使用RunsProcesses特质来赋予您的可调用命令运行其他进程(需要symfony/process)的能力。默认情况下,进程的标准输出是隐藏的,但可以通过将-v传递给父命令来显示。错误输出始终显示。如果进程失败,则会抛出\RuntimeException异常。

use Symfony\Component\Console\Command;
use Symfony\Component\Process\Process;
use Zenstruck\Console\InvokableCommand;
use Zenstruck\Console\RunsProcesses;

class MyCommand extends InvokableCommand
{
    use RunsProcesses;

    public function __invoke(): void
    {
        $this->runProcess('/some/script');

        // construct with array
        $this->runProcess(['/some/script', 'arg1', 'arg1']);

        // for full control, pass a Process itself
        $this->runProcess(
            Process::fromShellCommandline('/some/script')
                ->setTimeout(900)
                ->setWorkingDirectory('/')
        );
    }
}

CommandSummarySubscriber

将此事件订阅者添加到您的Application的事件分发器中,以便在每次运行命令后显示摘要。摘要包括命令的持续时间和峰值内存使用量。

如果使用Symfony,将其配置为服务以启用

# config/packages/zenstruck_console_extra.yaml

services:
    Zenstruck\Console\EventListener\CommandSummarySubscriber:
        autoconfigure: true

注意

这将显示每次注册的命令运行后的摘要。