consolidation/annotation-command

该软件包已被放弃,不再维护。作者建议使用 consolidation/annotated-command 软件包。

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

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

This package is auto-updated.

Last update: 2024-07-23 15:41:43 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 alter。

钩子

除了命令外,命令文件还可以提供钩子。包含 @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" 变体(如果有),提供了更大的灵活性,特别是在钩子顺序方面(以及一致性)。在某一类型的钩子中,执行顺序是未定义的,也没有保证。请注意,许多验证、处理和修改钩子可能会运行,但第一个状态或提取钩子成功返回结果将停止处理同一类型的其他钩子。

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

命令事件钩子

命令事件钩子是通过 Symfony Console 命令事件通知回调机制调用的。这发生在事件分发和命令/选项验证之前。请注意,Symfony 不允许在这个钩子中修改 $input 对象;在这里所做的任何更改都将被重置,因为 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'
    );
}

初始化钩子

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

用于 Robo PHPconsolidation/config 项目使用 @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");

}

交互式钩子

交互式钩子(InteractorInterface)在参数和选项验证之前运行。如果命令行上未提供必需的参数和选项,可以在这一阶段通过提示用户来提供。请注意,如果提供了 --no-interaction 标志,则不会调用交互式钩子,而命令事件钩子和 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);
    }
}

验证钩子

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

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

验证钩子可以通过修改提供的 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》,那么将调用结果对象的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提供格式化程序。请参阅下面的《API 使用》。

日志记录

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 使用

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

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

要在一个应用程序中使用注解命令,请将您的命令类实例传递给 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 应用程序类获取终端宽度,并将其提供给 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);