PHP让人意外的软件包生态


最近业余时间在写一个Web游戏,技术选项是Go+TypeScript,使用gRPC-Web进行通讯。在做数值策划,生成数据的时候,还是想用顺手的PHP来写工具。所以,有了这篇文章。

我之前用Go写过一个批量操作远程主机的工具,用的命令行软件包是spf13/cobra。写PHP的命令行工具,我也是想找一个比较出名好用的软件包来减少无意义的重复工作。经过一番搜索 symfony/console出现在我眼前,简单阅读了一下官方文档,开始了我的第一个PHP命令行程序之旅。

入口文件:tools.php

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

use Game\Generator\Chapter;
use Symfony\Component\Console\Application;

require_once __DIR__ . '/vendor/autoload.php';

$application = new Application('Game Tools', '0.1');
$application->add(new Chapter());
$application->run();

生成数据的命令行类Chapter.php实现

<?php
namespace Game\Generator;

use Symfony\Component\Console\Command\Command;

class Chapter extends Command
{
}

运行:

chmod +x ./tools.php && ./tools.php

出乎意料,竟然报错了 !

PHP Fatal error:  Uncaught Symfony\Component\Console\Exception\LogicException: The command defined in "Game\Generator\Chapter" cannot have an empty name. in /Users/nemo/workspace/go/game001/tools/vendor/symfony/console/Application.php:470
Stack trace:
#0 /Users/nemo/workspace/go/game001/tools/tools.php(12): Symfony\Component\Console\Application->add(Object(Game\Generator\Chapter))
#1 {main}
  thrown in /Users/nemo/workspace/go/game001/tools/vendor/symfony/console/Application.php on line 470

看样子是需要我指定一个name呢,我加上name后,命令类如下:

<?php
namespace Game\Generator;

use Symfony\Component\Console\Command\Command;

class Chapter extends Command
{
    function __construct()
    {
        parent::__construct('chapter');
    }
}

这下应该正常工作了吧?运行结果:

You must override the execute() method in the concrete command class. 

这个时候已经让我想起了Windows的“正在处理一些事情”,如这般可笑。

我笑着实现了execute方法,内容如下:

<?php
namespace Game\Generator;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class Chapter extends Command
{
    function __construct()
    {
        parent::__construct('chapter');
    }

    function execute(InputInterface $input, OutputInterface $output)
    {
    }
}

然后,真的觉得应该可以了吧的时候,又Fatal了!!!

PHP Fatal error:  Uncaught TypeError: Return value of "Game\Generator\Chapter::execute()" must be of the type int, "NULL" returned. in /Users/nemo/workspace/go/game001/tools/vendor/symfony/console/Command/Command.php:258
Stack trace:
#0 /Users/nemo/workspace/go/game001/tools/vendor/symfony/console/Application.php(912): Symfony\Component\Console\Command\Command->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#1 /Users/nemo/workspace/go/game001/tools/vendor/symfony/console/Application.php(264): Symfony\Component\Console\Application->doRunCommand(Object(Game\Generator\Chapter), Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#2 /Users/nemo/workspace/go/game001/tools/vendor/symfony/console/Application.php(140): Symfony\Component\Console\Application->doRun(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#3 /Users/nemo/workspace/go/game001/tools/tools.ph in /Users/nemo/workspace/go/game001/tools/vendor/symfony/console/Command/Command.php on line 258

此时,我已经决定不用这个软件包了。。。但是,既然尝试了,hello world是肯定要有的~ 所以,耐心按照提示继续修改,如下:

<?php
namespace Game\Generator;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class Chapter extends Command
{
    function __construct()
    {
        parent::__construct('chapter');
    }

    function execute(InputInterface $input, OutputInterface $output)
    {
        return 0;
    }
}

终于,再次运行,没有报错了。

正确的实现应该是什么样呢?

Command应该是一个抽象类,因为要求子类必须提供一个execute方法实现。

而且还要有一个getName()的抽象方法,要求子类明确自己的name。

对于我所使用的这个软件包,已经是5.0版本了 ,最小版本要求是PHP7.2.5

requires

php: ^7.2.5
symfony/polyfill-mbstring: ~1.0
symfony/polyfill-php73: ^1.8
symfony/service-contracts: ^1.1|^2

那么,Command类应该改写成这样(省略N多无关内容):

/**
 * Base class for all commands.
 */
abstract class Command
{

    /**
     * Return the command's name
     * @return string
     */
    abstract public function getName(): string;

    /**
     * Executes the current command.
     *
     * This method is not abstract because you can use this class
     * as a concrete class. In this case, instead of defining the
     * execute() method, you set the code to execute by passing
     * a Closure to the setCode() method.
     *
     * @param InputInterface $input
     * @param OutputInterface $output
     * @return int 0 if everything went fine, or an exit code
     *
     * @see setCode()
     */
    abstract protected function execute(InputInterface $input, OutputInterface $output): int
}

这个时候,再尝试写一个命令是什么体验呢?

首先,我们依旧写一个空类:

<?php
namespace Game\Generator;

use Symfony\Component\Console\Command\Command;

class Chapter extends Command
{
}

此时,IDE会提醒你,基类有两个抽象方法需要实现。然后,我们按照提示,实现两个抽象方法(IDE可以直接帮忙生成这俩方法)

<?php
namespace Game\Generator;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class Chapter extends Command
{
    /**
     * Return the command's name
     * @return string
     */
    public function getName(): string
    {
        // TODO: Implement getName() method.
    }

    /**
     * Executes the current command.
     *
     * This method is not abstract because you can use this class
     * as a concrete class. In this case, instead of defining the
     * execute() method, you set the code to execute by passing
     * a Closure to the setCode() method.
     *
     * @param InputInterface $input
     * @param OutputInterface $output
     * @return int 0 if everything went fine, or an exit code
     *
     * @see setCode()
     */
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        // TODO: Implement execute() method.
    }
}

此时,可以看到两个方法的签名要求返回值,我们补上返回值。

<?php
namespace Game\Generator;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class Chapter extends Command
{
    /**
     * Return the command's name
     * @return string
     */
    public function getName(): string
    {
        return 'chapter';
    }

    /**
     * Executes the current command.
     *
     * This method is not abstract because you can use this class
     * as a concrete class. In this case, instead of defining the
     * execute() method, you set the code to execute by passing
     * a Closure to the setCode() method.
     *
     * @param InputInterface $input
     * @param OutputInterface $output
     * @return int 0 if everything went fine, or an exit code
     *
     * @see setCode()
     */
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        return 0;
    }
}

至此,一个基本命令就实现完了。行云流水形容的也就是大概如此吧。


《“PHP让人意外的软件包生态”》 有 1 条评论

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注