consolidation/annotated-command

从注解的命令类方法初始化 Symfony Console 命令。

5.0.0-beta1 2023-02-28 14:02 UTC

This package is auto-updated.

Last update: 2024-08-23 15:51:01 UTC


README

从注解/属性命令类方法初始化 Symfony Console 命令。

CI scrutinizer codecov license

组件状态

当前在使用Robo (1.x+),Drush (9.x+) 和 Terminus (1.x+)。

动机

Symfony Console 提供了一系列类,这些类被广泛用于实现命令行工具。越来越流行使用注解来描述通过注解方法实现的命令(例如其参数、选项等)的特性。

使用此技术的现有命令行工具包括

此库提供了一组例程,可以从提供的类中定义的所有公共方法生成 Symfony\Component\Console\Command\Command。

注意如果您正在寻找一种非常快速的方式来编写基于 Symfony Console 的命令行工具,您应该考虑使用基于此库构建的 Robo,它添加了额外的便利性,以帮助您快速启动。使用 g1a/starter 来快速搭建新的命令行工具。请参阅将 Robo 用作框架。当然,如果您愿意,也可以在不使用 Robo 的情况下使用此项目。

库使用

这是一个打算在其他项目中使用的库。请在您的 composer.json 文件中要求

    "require": {
        "consolidation/annotated-command": "^4"
    },

示例注解命令类

命令类的公共方法定义了它的命令,每个方法的参数定义了其参数和选项。如果参数在文档块中具有相应的 "@option" 注解,则它是一个选项;否则,它是一个参数。

class MyCommandClass
{
    /**
     * This is the my:echo command
     *
     * This command will concatenate two parameters. If the --flip flag
     * is provided, then the result is the concatenation of two and one.
     *
     * @command my:echo
     * @param string $one The first parameter.
     * @param string $two The other parameter.
     * @param bool $flip The "flip" option
     * @option flip Whether or not the second parameter should come first in the result.
     * @aliases c
     * @usage bet alpha --flip
     *   Concatenate "alpha" and "bet".
     */
    public function myEcho($one, $two, $flip = false)
    {
        if ($flip) {
            return "{$two}{$one}";
        }
        return "{$one}{$two}";
    }
}

或通过 PHP 8 属性。

    #[CLI\Name(name: 'my:echo', aliases: ['c'])]
    #[CLI\Help(description: 'This is the my:echo command', synopsis: "This command will concatenate two parameters. If the --flip flag\nis provided, then the result is the concatenation of two and one.",)]
    #[CLI\Param(name: 'one', description: 'The first parameter')]
    #[CLI\Param(name: 'two', description: 'The other parameter')]
    #[CLI\Option(name: 'flip', description: 'Whether or not the second parameter should come first in the result.')]
    #[CLI\Usage(name: 'bet alpha --flip', description: 'Concatenate "alpha" and "bet".')]
    public function myEcho($one, $two = '', $flip = false)
    {
        if ($options['flip']) {
            return "{$two}{$one}";
        }
        return "{$one}{$two}";
    }

遗留注解命令方法

声明命令的遗留方法仍然受支持。当使用遗留方法时,如果有命令选项,则将它们声明为方法的最后一个参数。选项将被作为关联数组传递;最后一个参数的默认选项应该列出命令识别的选项。其余的参数是参数。具有默认值的参数是可选的;没有默认值的参数是必需的。

class MyCommandClass
{
    /**
     * This is the my:echo command
     *
     * This command will concatenate two parameters. If the --flip flag
     * is provided, then the result is the concatenation of two and one.
     *
     * @command my:echo
     * @param integer $one The first parameter.
     * @param integer $two The other parameter.
     * @param array $options An option that takes multiple values.
     * @option flip Whether or not the second parameter should come first in the result.
     * @aliases c
     * @usage bet alpha --flip
     *   Concatenate "alpha" and "bet".
     */
    public function myEcho($one, $two, $options = ['flip' => false])
    {
        if ($options['flip']) {
            return "{$two}{$one}";
        }
        return "{$one}{$two}";
    }
}

选项默认值

必须是一个关联数组,其键是选项的名称,其值可以是以下之一:

  • 布尔值 false,表示选项不取值。
  • 一个包含可能提供但不是必需的默认值的 字符串
  • 特殊值 InputOption::VALUE_REQUIRED,表示用户必须为选项提供值。
  • 特殊值 InputOption::VALUE_OPTIONAL,产生以下行为
    • 如果选项提供了一个值(例如 --foo=bar),则该值将是一个字符串。
    • 如果选项在命令行上存在但没有值(例如 --foo),则该值将是 true
    • 如果选项根本不存在于命令行上,则该值将是 null
    • 如果用户明确设置 --foo=0,则值将被转换为 false
    • 限制:如果使用除 ArgvInput(或其子类)之外的任何输入对象,则无论是否指定值(--foo)或选项,值都将是 null。当使用 StringInput 时,使用 --foo=1 而不是 --foo 以避免此问题。
  • 特殊值 true 会产生以下行为
    • 如果选项提供了一个值(例如 --foo=bar),则该值将是一个字符串。
    • 如果选项在命令行上存在但没有值(例如 --foo),则该值将是 true
    • 如果命令行上根本不存在此选项,则值也将是 true
    • 如果用户明确设置 --foo=0,则值将被转换为 false
    • 如果用户在命令行上添加了 --no-foo,则 foo 的值将是 false
  • 一个空数组,表示选项可能在命令行中出现多次。

不应使用其他值作为默认值。例如,$options = ['a' => 1]不正确 的;相反,应使用 $options = ['a' => '1']

选项的默认值也可以通过 @default 注解提供。请参阅下面的钩子修改。

钩子

除了命令之外,命令文件还可以提供钩子。包含 @hook 注解的命令文件方法被注册为钩子而不是命令。钩子注解的格式为

@hook type target

钩子 类型 决定了在命令生命周期中何时调用此钩子。可用的钩子类型将在以下部分详细描述。

钩子 目标 指定钩子将附加到哪个命令或命令。有几种不同的方式来指定钩子目标。

  • 命令的主要名称(例如 my:command)或命令的方法名称(例如 myCommand)将只将钩子附加到该命令。
  • 一个注解(例如 @foo)将钩子附加到任何带有给定标签的命令。
  • 如果目标指定为 *,则钩子将附加到所有命令。
  • 如果省略了目标,则钩子将附加到与钩子实现相同的类中定义的每个命令。

在命令处理请求流程中有十种类型的钩子

  • 命令事件(Symfony)
    • @pre-command-event
    • @command-event
    • @post-command-event
  • 选项
    • @pre-option
    • @option
    • @post-option
  • 初始化(Symfony)
    • @pre-init
    • @init
    • @post-init
  • 交互(Symfony)
    • @pre-interact
    • @interact
    • @post-interact
  • 验证
    • @pre-validate
    • @validate
    • @post-validate
  • 命令
    • @pre-command
    • @command
    • @post-command
  • 处理
    • @pre-process
    • @process
    • @post-process
  • 修改
    • @pre-alter
    • @alter
    • @post-alter
  • 状态
    • @status
  • 提取
    • @extract

除此之外,还有两个额外的钩子可用

如果可用,这些钩子的 "pre" 和 "post" 变体提供了更多的灵活性(并且具有一致性)。在钩子的一种类型中,运行顺序是未定义的,并且没有保证。请注意,许多验证、处理和修改钩子可能会运行,但第一个成功返回结果的 status 或 extract 钩子将停止进一步处理相同类型的钩子。

每个钩子都有一个定义其调用约定的接口;然而,当注册钩子时可以使用任何可调用的,这在需要支持 PHP 7.0 之前的版本(没有匿名类)时很方便。

命令事件钩子

命令事件钩子通过Symfony控制台命令事件通知回调机制被调用。这发生在事件分发和命令/选项验证之前。注意,在Symfony中不允许在此钩子中修改$输入对象;在此处所做的任何更改都将被重置,因为Symfony会重新解析该对象。对参数和选项的更改应在initialize钩子(非交互式更改)或interact钩子(自然用于交互式更改)中进行。

选项事件钩子

选项事件钩子(OptionHookInterface)在执行特定命令时被调用,或者当其帮助命令被调用时。可以通过调用提供的$command对象的addOption方法在此处添加命令的任何附加选项。请注意,选项钩子仅用于计算动态选项。静态选项可以通过在任何使用它们的钩子上使用@option注解来添加。有关示例,请参阅下面的Alter Hook文档。

use Consolidation\AnnotatedCommand\AnnotationData;
use Symfony\Component\Console\Command\Command;

/**
 * @hook option some:command
 */
public function additionalOption(Command $command, AnnotationData $annotationData)
{
    $command->addOption(
        'dynamic',
        '',
        InputOption::VALUE_NONE,
        'Option added by @hook option some:command'
    );
}

Initialize钩子

initialize钩子(InitializeHookInterface)在interact钩子之前运行。它可以从配置文件或其他来源提供命令参数和选项。它永远不应该进行任何用户交互。

consolidation/config项目(在Robo PHP中使用)使用@hook init自动注入从config.yml配置文件中未提供在命令行上的选项值。

use Consolidation\AnnotatedCommand\AnnotationData;
use Symfony\Component\Console\Input\InputInterface;

/**
 * @hook init some:command
 */
public function initSomeCommand(InputInterface $input, AnnotationData $annotationData)
{
    $value = $input->getOption('some-option');
    if (!$value) {
        $input->setOption('some-option', $this->generateRandomOptionValue());
    }
}

您可以通过使用简单的数组语法在此处修改AnnotationData。下面,我们为属性列表添加了一个额外的显示字段标签。

use Consolidation\AnnotatedCommand\AnnotationData;
use Symfony\Component\Console\Input\InputInterface;

/**
 * @hook init some:command
 */
public function initSomeCommand(InputInterface $input, AnnotationData $annotationData)
{
    $annotationData['field-labels'] .= "\n" . "new_field: My new field";
}

或者,您可以使用AnnotationData类的set()append()方法。

use Consolidation\AnnotatedCommand\AnnotationData;
use Symfony\Component\Console\Input\InputInterface;

/**
 * @hook init some:command
 */
public function initSomeCommand(InputInterface $input, AnnotationData $annotationData)
{
    // Add a line to the field labels.
    $annotationData->append('field-labels', "\n" . "new_field: My new field");
    // Replace all field labels.
    $annotationData->set('field-labels', "one_field: My only field");

}

Interact钩子

interact钩子(InteractorInterface)在参数和选项验证之前运行。在命令行上未提供的必需参数和选项可以在此阶段通过提示用户来提供。请注意,如果提供了--no-interaction标志,则不会调用interact钩子,而命令事件钩子和init钩子会被调用。

use Consolidation\AnnotatedCommand\AnnotationData;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

/**
 * @hook interact some:command
 */
public function interact(InputInterface $input, OutputInterface $output, AnnotationData $annotationData)
{
    $io = new SymfonyStyle($input, $output);

    // If the user did not specify a password, then prompt for one.
    $password = $input->getOption('password');
    if (empty($password)) {
        $password = $io->askHidden("Enter a password:", function ($value) { return $value; });
        $input->setOption('password', $password);
    }
}

Validate钩子

validate钩子(ValidatorInterface)的目的是确保当前命令的目标状态在所需的上下文中是可用的。在此次钩子之前,Symfony已经验证了参数和选项。如果需要,可以更改参数和选项的值,尽管这最好在configure钩子中完成。验证钩子可以执行以下几种操作

  • 不执行任何操作。这表示验证成功。
  • 返回CommandError。验证失败,执行停止。CommandError包含状态结果代码和消息,将被打印。
  • 抛出异常。异常将被转换为CommandError。
  • 返回false。消息为空,状态为1。已弃用。

validate钩子可以通过修改提供的CommandData参数中的Input对象来更改命令的参数和选项。可以运行多个验证钩子,但如果任何一个失败,则命令的执行将停止。

use Consolidation\AnnotatedCommand\CommandData;

/**
 * @hook validate some:command
 */
public function validatePassword(CommandData $commandData)
{
    $input = $commandData->input();
    $password = $input->getOption('password');

    if (strpbrk($password, '!;$`') === false) {
        throw new \Exception("Your password MUST contain at least one of the characters ! ; ` or $, for no rational reason whatsoever.");
    }
}

命令钩子

命令钩子提供用于语义目的。预命令和命令钩子等同于后验证钩子,应遵循接口(ValidatorInterface)。所有后验证钩子都会在第一个预命令钩子被调用之前被调用。同样,后命令钩子等同于预处理钩子,应实现接口(ProcessResultInterface)。

命令回调本身(被注解为@command的方法)是在最后一个命令钩子之后、第一个后命令钩子之前被调用的。

use Consolidation\AnnotatedCommand\CommandData;

/**
 * @hook pre-command some:command
 */
public function preCommand(CommandData $commandData)
{
    // Do something before some:command
}

/**
 * @hook post-command some:command
 */
public function postCommand($result, CommandData $commandData)
{
    // Do something after some:command
}

处理钩子

处理钩子(ProcessResultInterface)专门设计用于将一系列处理指令转换为最终结果。Robo 中的 CollectionProcessHook 类是一个实现示例;如果一个 Robo 命令返回一个 TaskInterface,那么 Robo 处理钩子将执行该任务并返回结果。这允许预处理钩子修改任务,例如通过向任务集合中添加更多操作。

处理钩子不应用于其他目的。

use Consolidation\AnnotatedCommand\CommandData;

/**
 * @hook process some:command
 */
public function process($result, CommandData $commandData)
{
    if ($result instanceof MyInterimType) {
        $result = $this->convertInterimResult($result);
    }
}

修改钩子

修改钩子(AlterResultInterface)更改结果对象。修改钩子应仅对其明确识别的结果对象类型进行操作。它们可以返回相同类型的对象,或者将对象转换为其他类型。

如果出现错误,并且修改钩子希望强制命令失败,则它可以返回一个 CommandError 对象或抛出一个异常。

use Consolidation\AnnotatedCommand\CommandData;

/**
 * Demonstrate an alter hook with an option
 *
 * @hook alter some:command
 * @option $alteration Alter the result of the command in some way.
 * @usage some:command --alteration
 */
public function alterSomeCommand($result, CommandData $commandData)
{
    if ($commandData->input()->getOption('alteration')) {
        $result[] = $this->getOneMoreRow();
    }

    return $result;
}

如果需要为选项提供一个默认值,可以通过 @default 注解来完成。

use Consolidation\AnnotatedCommand\CommandData;

/**
 * Demonstrate an alter hook with an option that has a default value
 *
 * @hook alter some:command
 * @option $name Give the result a name.
 * @default $name George
 * @usage some:command --name=George
 */
public function nameSomeCommand($result, CommandData $commandData)
{
    $result['name'] = $commandData->input()->getOption('name')

    return $result;
}

状态钩子

已废弃

使用状态确定器钩子,命令应简单地使用 CommandResult 对象分别返回它们的退出代码和结果数据。

状态钩子(StatusDeterminerInterface)负责确定命令是否成功(状态码 0)或失败(状态码 > 0)。命令返回的结果对象可能是一个复合对象,其中包含有关命令结果的多项信息。如果结果对象实现了 ExitCodeInterface,则调用结果对象的 getExitCode() 方法来确定命令的状态结果码。如果没有实现 ExitCodeInterface,则执行附加到此命令的所有状态钩子;第一个成功返回结果的状态钩子将停止进一步执行状态钩子,并使用其返回的结果作为此操作的状态结果码。

如果没有状态钩子返回任何结果,则假定成功。

提取钩子

已废弃

有关更灵活的替代方案,请参阅 output-formatters 中的 RowsOfFieldsWithMetadata

提取钩子(《ExtractOutputInterface》)负责确定命令的实际渲染输出应该是什么。命令返回的结果对象可能是一个包含多个关于命令结果信息的复合对象。如果结果对象实现了《OutputDataInterface》,则调用结果对象的《code>getOutputData()方法来确定应该显示给用户的哪些信息作为命令执行的成果。如果没有实现OutputDataInterface,则执行此命令的所有提取钩子;第一个成功返回输出数据的钩子将停止其他提取钩子的执行。

如果没有提取钩子返回任何数据,则如果结果对象是字符串,则直接打印结果对象;否则,不输出任何内容(除了命令本身产生的任何内容)。

事件钩子

命令可以定义自己的自定义事件;为此,它们只需实现CustomEventAwareInterface,并使用CustomEventAwareTrait。然后可以使用事件钩子定义每个自定义事件的事件处理器。

使用事件钩子的处理器看起来如下

/**
 * @hook on-event custom-event
 */
public function handlerForCustomEvent(/* arbitrary parameters, as defined by custom-event */)
{
    // do the needful, return what custom-event expects
}

然后,为了在命令中使用它

class MyCommands implements CustomEventAwareInterface
{
    use CustomEventAwareTrait;

    /**
     * @command my-command
     */
    public myCommand($options = [])
    {
        $handlers = $this->getCustomEventHandlers('custom-event');
        // iterate and call $handlers
    }
}

定义自定义事件的命令负责声明回调函数的预期参数、返回值及其使用方式。

替换命令钩子

替换命令(《ReplaceCommandHookInterface》)钩子允许您用您自己的另一个方法替换命令的方法。

例如,如果您想替换foo:bar命令,可以使用以下代码

<?php
class MyReplaceCommandHook  {

  /**
   * @hook replace-command foo:bar
   *
   * Parameters must match original command method.
   */
  public function myFooBarReplacement($value) {
    print "Hello $value!";
  }
}

输出

如果命令方法返回整数,则用作命令退出状态码。如果命令方法返回字符串,则将其打印出来。

如果使用《Consolidation/OutputFormatters》项目,则用户可以指定一个--format选项来选择用于将命令提供的输出转换为字符串的格式化程序。要使此功能正常工作,应用程序必须向AnnotatedCommandFactory提供格式化程序。请参见下面的《a href="#user-content-api-usage" rel="nofollow noindex noopener external ugc">API Usage》。

日志记录

Annotated-Command项目对日志记录一无所知。如果命令希望记录进度,则CommandFile类应实现LoggerAwareInterface,命令行工具应通过LoggerAwareTrait的setLogger()方法注入一个用于其使用的记录器。推荐使用《Robo》。

访问Symfony对象

如果您想使用注解,但仍想访问Symfony命令,例如,为了获取辅助器的引用以调用一些旧代码,您可以创建一个扩展\Consolidation\AnnotatedCommand\AnnotatedCommand(这是\Symfony\Component\Console\Command\Command)的普通Symfony命令。省略configure方法,并在execute()方法上放置您的注解。

还可以向命令文件中的任何注解方法添加InputInterface和/或OutputInterface参数(参数必须在命令参数之前)。

参数注入

就像这个库默认会在任何命令函数的参数列表开头注入$input和/或$output一样,您也可以添加一个处理器来注入其他对象。

给定一个类似于以下示例的SymfonyStyleInjector实现

use Consolidation\AnnotatedCommand\ParameterInjector

class SymfonyStyleInjector implements ParameterInjector
{
    public function get(CommandData $commandData, $interfaceName)
    {
        return new MySymfonyStyle($commandData->input(), $commandData->output());
    }
}

然后,如果您的应用程序初始化代码中已注册 SymfonyStyleInjector,则将为任何接受 SymfonyStyle 参数的命令处理方法提供 'MySymfonyStyle' 的实例。

$commandProcessor->parameterInjection()->register('Symfony\Component\Console\Style\SymfonyStyle', new SymfonyStyleInjector);

以下类默认可用于通过命令方法注入

  • Symfony\Component\Console\Input\InputInterface
  • Symfony\Component\Console\Output\OutputInterface
  • Consolidation\AnnotatedCommand\AnnotationData
  • Consolidation\OutputFormatters\Options\FormatterOptions

请注意,这些实例也通过传递给大多数命令钩子的 CommandData 对象可用。

处理标准输入

任何 Symfony 命令都可以使用提供的 StdinHandler 来实现从标准输入读取的命令。

  /**
   * @command example
   * @option string $file
   * @default $file -
   */
  public function example(InputInterface $input)
  {
      $data = StdinHandler::selectStream($input, 'file')->contents();
  }

此示例将从 stdin 流中读取所有可用数据到 $data,或者,也可以通过 --file=/path 选项读取指定文件的整个内容。

有关更多详细信息,包括使用 StdinHandle 与 DI 容器结合使用的示例,请参阅 StdinHandler.php 中的注释。

API 使用

如果您想使用 Annotated Commands 来构建命令行工具,建议您使用 Robo 作为框架,因为它会为您设置所有各种命令类。如果您想将 Annotated Commands 集成到其他框架中,请参阅下面的部分。

设置命令工厂并实例化命令

要在应用程序中使用注解命令,请将您的命令类实例传递给 AnnotatedCommandFactory::createCommandsFromClass()。结果将是一系列可以添加到您的应用程序中的命令。

$myCommandClassInstance = new MyCommandClass();
$commandFactory = new AnnotatedCommandFactory();
$commandFactory->setIncludeAllPublicMethods(true);
$commandFactory->commandProcessor()->setFormatterManager(new FormatterManager());
$commandList = $commandFactory->createCommandsFromClass($myCommandClassInstance);
foreach ($commandList as $command) {
    $application->add($command);
}

如果您有多个命令类,可以这样做。如果是这样,只需多次调用 AnnotatedCommandFactory::createCommandsFromClass()。

如果您不希望将类中的每个公共方法都添加为命令,请使用 AnnotatedCommandFactory::setIncludeAllPublicMethods(false),只有标记有 @command 注解的方法才会成为命令。

请注意,setFormatterManager() 操作是可选的;如果未使用 Consolidation/OutputFormatters,则省略此操作。

可以通过 AnnotatedCommandFactory::addCommandInfoAlterer() 添加 CommandInfoAltererInterface,它将在创建命令之前有机会调整从命令文件解析出的每个 CommandInfo 对象。

命令文件发现

还提供了一个发现类 CommandFileDiscovery,用于帮助在文件系统中查找命令文件。用法如下

$discovery = new CommandFileDiscovery();
$myCommandFiles = $discovery->discover($path, '\Drupal');
foreach ($myCommandFiles as $myCommandClass) {
    $myCommandClassInstance = new $myCommandClass();
    // ... as above
}

有关命令文件命名约定和搜索位置的讨论,请参阅 #12

如果在不同的命令文件路径处使用不同的命名空间,请按如下方式更改发现调用

$myCommandFiles = $discovery->discover(['\Ns1' => $path1, '\Ns2' => $path2]);

作为上述内容的快捷方式,方法 discoverNamespaced() 将取每个路径的最后一个目录名,并将其附加到提供的基命名空间。例如,这符合 Drupal 模块的约定。

配置输出格式化程序(例如,启用自动换行)

Output Formatters 项目支持自动格式化表格输出。为了使自动换行正确工作,必须通过 FormatterOptions::setWidth() 将终端宽度传递给 Output Formatters 处理程序。

在 Annotated Commands 项目中,这是通过依赖注入完成的。如果将 PrepareFormatter 对象传递给 CommandProcessor::addPrepareFormatter(),则它将有机会在创建时设置 FormatterOptions 的属性。

提供了一个 PrepareTerminalWidthOption 类,用于使用 Symfony Application 类获取终端宽度,并将其提供给 FormatterOptions。它是这样注入的

$terminalWidthOption = new PrepareTerminalWidthOption();
$terminalWidthOption->setApplication($application);
$commandFactory->commandProcessor()->addPrepareFormatter($terminalWidthOption);

为了提供对宽度的更多控制,创建自己的 PrepareTerminalWidthOption 子类,并按需调整宽度。

其他回调

除了钩子管理器提供的钩子外,还有其他回调可用于更改注解命令库的操作方式。

工厂监听器

每当使用命令文件实例创建注解命令时,都会通知工厂监听器。

public function AnnotatedCommandFactory::addListener(CommandCreationListenerInterface $listener);

监听器可以在命令工厂提供时构建命令文件实例。

选项提供者

选项提供者在命令构建过程中有机会向命令添加选项。

public function AnnotatedCommandFactory::addAutomaticOptionProvider(AutomaticOptionsProviderInterface $listener);

完整的CommandInfo记录和所有注解数据都可用,因此您可以为方法注解为@fooable的每个命令添加选项--foo

CommandInfo修改者

CommandInfo修改者可以在创建命令之前立即调整有关命令的信息。通常,这些将用于为命令的特定注解提供默认值,或者根据命令文件实例实现的接口采取其他操作。

public function alterCommandInfo(CommandInfo $commandInfo, $commandFileInstance);