webmozart/console

一个可用、美观且易于测试的PHP控制台工具包。

1.0.0-beta5 2016-02-05 10:03 UTC

This package is auto-updated.

Last update: 2024-09-08 01:10:10 UTC


README

Build Status Build status Scrutinizer Code Quality Latest Stable Version Total Downloads Dependency Status

最新版本: 1.0.0-beta5

PHP >= 5.3.9

一个可用、美观且易于测试的PHP控制台工具包。

目标

本包的目标是

  • 构建类似于 "git" 命令的PHP应用
  • 以最少的代码量
  • 可测试
  • 健壮
  • 美观。

现有的所有控制台库都不符合这些要求,因此我将 Symfony Console组件 重构成了你现在看到的这样。

安装

使用 Composer 安装此包

$ composer require webmozart/console:~1.0@beta

基本配置

控制台应用程序通过配置类进行配置。以示例,我们将创建一个名为 "git" 的PHP命令

use Webmozart\Console\Config\DefaultApplicationConfig;

class GitApplicationConfig extends DefaultApplicationConfig
{
    protected function configure()
    {
        parent::configure();
        
        $this
            ->setName('git')
            ->setVersion('1.0.0')
            
            // ...
        ;
    }
}

这个基本配置告诉控制台可执行文件名为 "git",当前版本为 1.0.0。现在让我们创建 "git" 可执行文件

#!/usr/bin/env php
<?php

use Webmozart\Console\ConsoleApplication;

if (file_exists($autoload = __DIR__.'/../../../autoload.php')) {
    require_once $autoload;
} else {
    require_once __DIR__.'/../vendor/autoload.php';
}

$cli = new ConsoleApplication(new GitApplicationConfig());
$cli->run();

复杂的自动加载块确保在您直接在您的包中运行可执行文件以及当您的包通过Composer安装到其他项目中时都能找到自动加载文件。

更改您的可执行文件权限并尝试运行它

$ chmod a+x bin/git
$ bin/git
Git version 1.0.0
...

命令

到目前为止,我们的应用程序做不了太多。让我们添加一个 "log" 命令,该命令显示最新的提交

class GitApplicationConfig extends DefaultApplicationConfig
{
    protected function configure()
    {
        $this
            // ...
            
            ->beginCommand('log')
                ->setDescription('Show the latest commits')
                ->setHandler(new LogCommandHandler())
            ->end()
        ;
    }
}

如您所见,命令的执行被委托给了 LogCommandHandler。由于处理器是一个独立的类,它可以很容易地在隔离状态下进行测试。让我们实现一个基本的处理器

use Webmozart\Console\Api\Args\Args;
use Webmozart\Console\Api\Command\Command;
use Webmozart\Console\Api\IO\IO;

class LogCommandHandler
{
    public function handle(Args $args, IO $io, Command $command)
    {
        // Simulate the retrieval of the commits
        $commits = array(
            'commit1',
            'commit2',
            'commit3',
        );
        
        foreach ($commits as $commit) {
            $io->writeLine($commit);
        }
        
        return 0;
    }
}

我们的命令处理器的 handle() 方法检索最多三个参数

  • Args $args:调用命令时传入的参数和选项。
  • IO $io:I/O,提供了对标准输入、标准输出和错误输出的访问。
  • Command $command:当前执行的命令。

您可以省略不需要的参数。

每个处理器都应该在成功处理后返回0,如果失败则返回介于1和255之间的任何整数。

让我们运行这个命令

$ bin/git log
commit1
commit2
commit3

参数

接下来,我们将向 "log" 命令添加一个名为 <branch> 的参数,我们可以使用它来选择要显示的分支

use Webmozart\Console\Api\Args\Format\Argument;

class GitApplicationConfig extends DefaultApplicationConfig
{
    protected function configure()
    {
        $this
            // ...
            
            ->beginCommand('log')
                // ...
                
                ->addArgument('branch', Argument::OPTIONAL, 'The branch to display', 'master')
            ->end()
        ;
    }
}

我们添加了一个可选参数 "branch",默认值为 "master"。我们可以在处理器的 Args 实例中访问参数的值

class LogCommandHandler
{
    public function handle(Args $args, IO $io)
    {
        $io->writeLine('Branch: '.$args->getArgument('branch'));
        $io->writeLine('--');
        
        // ...
    }
}

让我们带参数运行这个命令

$ bin/git log 1.0
Branch: 1.0
--
commit1
commit2
commit3

addArgument() 方法的第二个参数接受不同标志的位组合

选项

选项是您可以传递给命令的附加、可选设置。让我们向我们的命令添加一个选项 --max=<limit>,该选项将显示的提交数量限制为传递的限制

use Webmozart\Console\Api\Args\Format\Option;

class GitApplicationConfig extends DefaultApplicationConfig
{
    protected function configure()
    {
        $this
            // ...
            
            ->beginCommand('log')
                // ...
                
                ->addOption('max', null, Option::REQUIRED_VALUE, 'The maximum number of commits', 25)
            ->end()
        ;
    }
}

选项的配置与参数的配置非常相似。我们创建了一个需要值的 --max 选项,如果用户未设置该选项,则其默认值为25。

我们可以在命令处理器的 getOption() 方法中访问传递的值

class LogCommandHandler
{
    public function handle(Args $args, IO $io)
    {
        // ...
        
        $io->writeLine('Limit: '.$args->getOption('max').' commits');
        
        // ...
    }
}

此外,您还经常需要 isOptionSet() 方法,该方法告诉您用户在调用命令时是否实际上传递了该选项。

让我们带选项运行这个命令

$ bin/git log --max 10
Branch: master
Limit: 10 commits
--
commit1
commit2
commit3

选项支持单字符的简写名称。简写选项名称只需前缀一个短横线,而不是两个,例如:-m。让我们通过设置addOption()方法的第二个参数来添加这个别名。

use Webmozart\Console\Api\Args\Format\Option;

class GitApplicationConfig extends DefaultApplicationConfig
{
    protected function configure()
    {
        $this
            // ...
            
            ->beginCommand('log')
                // ...
                
                ->addOption('max', 'm', Option::REQUIRED_VALUE, 'The maximum number of commits', 25)
            ->end()
        ;
    }
}

现在命令也可以这样运行。

$ bin/git log -m 10

就像对于参数一样,选项支持不同标志的位运算组合,这些标志控制选项的处理方式。

依赖关系

我们的命令处理程序通常会依赖外部服务来访问信息或执行业务逻辑。这些服务可以通过命令处理程序的构造函数注入。例如,假设我们需要一个CommitRepository来访问“log”命令中列出的提交。

class LogCommandHandler
{
    private $repository;
    
    public function __construct(CommitRepository $repository)
    {
        $this->repository = $repository;
    }
    
    public function handle(Args $args, IO $io)
    {
        $commits = $this->repository->findByBranch($args->getArgument('branch'));
        
        // ...
    }
}

由于CommitRepository被注入到命令处理程序中,我们在测试处理程序时可以轻松地用一个模拟对象替换仓库。

我们还需要更改配置以注入仓库。

class GitApplicationConfig extends DefaultApplicationConfig
{
    protected function configure()
    {
        $this
            // ...
            
            ->beginCommand('log')
                // ...
                
                ->setHandler(new LogCommandHandler(new CommitRepository()))
            ->end()
        ;
    }
}

如果应用程序增长,每次执行configure()方法时都会创建很多对象——即使不需要这些对象的命令也没有执行。让我们将我们的setHandler()调用改为闭包,以便按需执行处理程序。

class GitApplicationConfig extends DefaultApplicationConfig
{
    protected function configure()
    {
        $this
            // ...
            
            ->beginCommand('log')
                // ...
                
                ->setHandler(function () {
                    return new LogCommandHandler(new CommitRepository());
                })
            ->end()
        ;
    }
}

现在,只有在执行“log”命令时才会创建LogCommandHandler及其依赖项。

子命令

“log”命令是一个非常简单的例子,但许多实际用例比这更复杂。考虑“git remote”命令,它被分为几个子命令。

$ git remote
$ git remote add ...
$ git remote rename ...
$ git remote remove ...

可以在配置中使用beginSubCommand()方法引入这样的子命令。

class GitApplicationConfig extends DefaultApplicationConfig
{
    protected function configure()
    {
        $this
            // ...
            
            ->beginCommand('remote')
                ->setDescription('Manage the remotes of your Git repository')
                ->setHandler(function () {
                    return new RemoteCommandHandler(new RemoteManager());
                })
                
                ->beginSubCommand('list')
                    ->setHandlerMethod('handleList')
                ->end()
                
                ->beginSubCommand('add')
                    ->setHandlerMethod('handleAdd')
                    ->addArgument('name', Argument::REQUIRED, 'The remote name')
                    ->addArgument('url', Argument::REQUIRED, 'The remote URL')
                ->end()
                
                // ...
            ->end()
        ;
    }
}

与常规命令一样,子命令接受选项、参数和命令处理程序。然而,通常更方便的是为每个子命令创建一个有一个方法的单个处理程序。可以使用setHandlerMethod()选择处理程序方法。

我们的RemoteCommandHandler的基本实现与“log”命令的处理程序非常相似。

class RemoteCommandHandler
{
    private $manager;
    
    public function __construct(RemoteManager $manager)
    {
        $this->manager = $manager;
    }
    
    public function handleList(Args $args, IO $io)
    {
        $remotes = $this->manager->getRemotes();
        
        // ...
        
        return 0;
    }
    
    public function handleAdd(Args $args)
    {
        $name = $args->getArgument('name');
        $url = $args->getArgument('url');
        
        $this->manager->addRemote($name, $url);
        
        return 0;
    }
}

如果我们注入一个工作的RemoteManager,现在可以执行这两个命令了。

$ git remote add origin http://example.com
$ git remote list

如果我们想在不选择子命令的情况下默认执行“list”命令,我们需要用markDefault()将其标记为默认命令。

class GitApplicationConfig extends DefaultApplicationConfig
{
    protected function configure()
    {
        $this
            // ...
            
            ->beginCommand('remote')
                // ...
                
                ->beginSubCommand('list')
                    // ...
                    
                    ->markDefault()
                ->end()
                
                // ...
            ->end()
        ;
    }
}

现在我们可以用以下方式运行列表命令:

$ git remote

最后,如果您只想用git remote而不是git remote list来运行命令,可以使用markAnonymous()

颜色和样式

控制台支持将您传递给IO类的输出中的颜色和样式。例如,让我们输出一些加粗文本。

$io->writeLine("Current branch: <b>$branch</b>");

就像在HTML中一样,样式是通过标记标签插入到您的文本中的。默认情况下,以下样式被定义:

您可以使用配置中的addStyle()方法更改现有的样式或添加自定义样式。

use Webmozart\Console\Api\Formatter\Style;

class GitApplicationConfig extends DefaultApplicationConfig
{
    protected function configure()
    {
        $this
            // ...
            
            ->addStyle(Style::tag('c3')->fgMagenta())
        ;
    }
}

以下是可以在Style类上调用的方法的列表。

表格

您可以使用Table类绘制表格。

use Webmozart\Console\UI\Component\Table;

$table = new Table();

$table->setHeaderRow(array('Remote Name', 'Remote URL'));

foreach ($remotes as $remote) {
    $table->addRow(array($remote->getName(), $remote->getUrl()));
}

$table->render($io);

表格支持单词换行,这样长文本就不会打断输出。上述表格的默认输出类似于以下内容:

+-------------+-------------------------------+
| Remote Name | Remote URL                    |
+-------------+-------------------------------+
| origin      | http://example.com/repository |
| fork        | http://fork.com/repository    |
+-------------+-------------------------------+

您可以通过传递一个自定义的TableStyle到构造函数来更改表格的样式。要么构建自己的TableStyle,要么使用预定义的一个。

use Webmozart\Console\UI\Component\Table;
use Webmozart\Console\UI\Component\TableStyle;

$table = new Table(TableStyle::solidBorder());
┌──────────────┬───────────────────────────────┐
│ Remote Name  │ Remote URL                    │
├──────────────┼───────────────────────────────┤
│ origin       │ http://example.com/repository │
│ fork         │ http://fork.com/repository    │
└──────────────┴───────────────────────────────┘

以下预定义的表格样式存在:

使用Symfony助手

由于这个库是对Symfony Console组件的完全重构,您不能直接使用Symfony Console助手。如果您想使用它们,您需要相应地包装输入和输出。

use Webmozart\Console\Adapter\ArgsInput;
use Webmozart\Console\Adapter\IOOutput;

$colors = $helper->ask(new ArgsInput($args->getRawArgs(), $args), new IOOutput($io), $question);

如果您觉得这很麻烦,并且在您的代码中多次使用,请考虑使用特性。

use Webmozart\Console\Adapter\ArgsInput;
use Webmozart\Console\Adapter\IOOutput;

trait QuestionTrait
{
    protected function ask(Args $args, IO $io, Question $question)
    {
        // You could cache these instances, but you probably won't need it
        $helper = new QuestionHelper();

        return $helper->ask(new ArgsInput($args->getRawArgs(), $args), new IOOutput($io), $question);
    }
}

class MyCommandHandler
{
    use QuestionTrait;

    public function handle(Args $args, IO $io)
    {
        $question = new ChoiceQuestion(
            'Please select your favorite colors (defaults to red and blue)',
            array('red', 'blue', 'yellow'),
            '0,1'
        );

        $question->setMultiselect(true);

        $colors = $this->ask($args, $io, $question);
    }
}

作者

贡献

非常欢迎您做出贡献!

  • 您可以在 问题跟踪器 上报告您发现的任何错误或问题。
  • 您可以在包的 Git 仓库 中获取源代码。

支持

如果您遇到问题,请发送邮件到 bschussek@gmail.com 或在 Twitter 上联系 @webmozart

许可证

本包的所有内容均受 MIT 许可证 授权。