在面试时,我经常会问的一个问是:PHP有哪几种运行模式?如果过来应聘的同学回答了有FastCGI常驻型运行模式和CLI命令行运行模式的话,我就会继续追问:在这两种模式下,编写的PHP代码有哪些主要区别?
有较多的同学这个问题回答得都不是很好,估计他们对于CLI运行模式的深入理解并不多,又或者是缺少命令行应用的开发经验。但CLI运行模式是开发计划任务系统的基础,而计划任务又是企业级生态系统中重要的组成部分,可用于定时推送、异步消费、业务监控、提前生成和预热数据等场景。明白我们编写的PHP代码将在怎样的环境下执行,以及应该遵循约束和需要注意的事项,是一名专业开发工程师的基本素养。接下来,我们一起来学习下在CLI运行模式下编写PHP代码的技巧。
命令行参数
如果回退到大学时代,那时我们学习的主要编程语言是C/C++。印象最为深刻时,在课堂上要写很多程序代码,做很多作业,并且每一段程序,每一次作业,特别在实验室最初接触编程时,我们都要面对着一个黑色的屏幕,把通过键盘敲打的字符输入到程序代码中。
下面就是这样一段似曾相识的代码,做的事情很简单,只是获取用户输入的字符串,然后输出诸如“Hello World”的字样。
#include <stdio.h> void main() { char name[20]; // 输入名称 printf("Who are you?\n"); scanf("%s", name); // 输出名称 printf("\nHello, %s!\n", name); }
将以上代码保存到hello.c文件,然后使用gcc进行编译,并生成hello可执行程序。
$ gcc ./hello.c -o hello
成功通过编译并生成hello后,执行此程序,输入你的名字,就可以看到结果输出。
$ ./hello
Who are you?
dogstar
Hello, dogstar!
对于程序,参数输入的方式可谓是多种多样,常见的方式除了标准输入外,还可以使用命令行参数。命令行参数可以跟随在脚本命令后面的参数列表。我们再来看下在Java世界中,对于同样类似的功能,其代码实现如何。Java是严格遵循面向对象编程的,与面向过程的C语言不同。因此,我们需要先创建一个类,然后在类静态函数main主函数内放置待执行的代码片段。
class Hello { public static void main(String[] args) { System.out.printf("Hey, %s!\n", args[0]); } }
这就是解析性脚本编程语言的简洁之处,不需要中间的编译环节,也不需要额外的函数或类封装,直接运行编写的代码。有时,对人友好的事情,对机器就不一定成立。虽然可以快速执行编写的PHP代码,但在执行时就需要重复解析,因此会存在一定的性能影响和损耗。话说回来,虽然PHP的代码可以写得很简单,但在实际的项目开发中,尤其是在大型企业系统开发中,我们追求的是简洁,而不是过分简单。
稍加完善和调整,一个基本的合格的PHP脚本应该这么写:
#!/usr/bin/env php <?php /** * Hello World示例 */ if ($argc < 2) { echo "Usage: $argv[0] <name>\n"; exit(1); } $name = $argv[1]; printf("Hi, %s!\n", $name);
在最前面,在第一行位置,指定这是一个PHP执行文件,具体的写法根据系统配置和PHP安装方式的差异会有所区别。然后,可以使用$argc变量来获取命令行参数的总数,并且当参数个数不符合要求时输出错误提示和Usage使用说明及参数列表。顺便一提,$arg变量存放了参数列表的数据,第一个参数对应数组下标0,是表示当前执行脚本的路径,第二个参数才是脚本后面的第一个参数,以此类推。
编写好PHP脚本代码后,还有重要的一步,是给刚刚新建的PHP代码文件添加可执行的权限。否则,不通过PHP而直接执行此PHP文件的话,会提示权限不足,例如:
$ ./hello.php
-bash: ./hello.php: Permission denied
可执行以下命令添加可执行权限:
$ chmod +x ./hello.php
随后,不带任何参数直接执行,会看到Usage的提示帮助信息。
$ ./hello.php
Usage: ./hello.php
再次执行,并在后面加上名字参数,可以看到正常执行并输出了期望的结果。
$ ./hello.php dogstar
Hi, dogstar!
以上实现和过程,看似简单,却涵盖了PHP在CLI模式下的编写、执行、权限分配等内容,也具备了命令行应用的雏形。可以说是,麻雀虽小,五脏俱全。
但不要高兴得太早,以上内容都是很基本,很简单的,纵使如此,我们也不应过于大意,而要每个环节,每个步骤都能熟悉掌握,融汇贯通。在精通的情况下,我们继续进行扩展,学习更高级的用法。显然,原始的命令参数,如果只是单纯通过位置来区分是非常不友好的,一来不方便记忆;二来不方便扩展,难以调整参数顺序;三来即便前面的参数是可选的,也要手动填充默认值,使用繁琐。正所谓,民曰不便。对此的解决方案是,使用升级版的命令行参数。即,本质上还是命令行参数这一技术,但是表面上的用法却非常人性化。Linux的命令绝大部分都是采用了带参数名称的参数列表,参数名称既可以使用缩写的方式,也可以通过完整的方式来传递。缩写参数名前面带一个横线,完整参数名前面则需要带两个横线。我们一起来看下例子就非常容易理解了。
如果想查看某个文件的最后几行内容,可以使用tail命令。通过短参数n,可以指定多少。例如查看刚才hello.php 文件的最后3行代码。可以这样:
$ tail -n 3 ./hello.php
$name = $argv[1];
printf(“Hi, %s!\n”, $name);
等效的做法,可以使用长参数lines,注意,这时前面需要加上两个横线。即:
$ tail --lines 3 ./hello.php
$name = $argv[1];
printf(“Hi, %s!\n”, $name);
那么,问题来了。如果我们在开发PHP命令行应用时,也希望能采用这样带参数名称的传递方式,应该怎么做?显示,原生PHP缺少这种能力,可以借助第三方开源类库来完成对命令行参数的封装和获取。说到这,顺便提醒一下,在PHP开源社区里,尤其是在开源大社区里,已经有很多优秀成熟的解决方案,当遇到用到新技术时,不要着急编写原始的代码,也不要着急马上去造轮子。我们要做的是,首先要理解这一切技术最底层的实现原理和背后涉及的知识、概念和理论,其次是要学会去发现,去学习和去使用。一如这里的带参数名的命令行参数获取,可以使用Ulrichsg/Getopt这一优秀的工具类库。
通过Ulrichsg/Getopt,逐步向更专业、更完善的方向贴近,同时追求更精湛的开发技艺。同时,由小到大,从点到面,不断丰富自己的开发技能。下面是改用Ulrichsg/Getopt的实现代码。这时的代码更为精练,更为释意,也更为专业。在代码的最后,我们使用了PHP_EOL来作为换行符的输出,避免使用转义符\n在不同操作系统下的兼容性问题。作为一名专业的开发人员,这些细节也是不容忽视的。
#!/usr/bin/env php <?php use Ulrichsg\Getopt\Getopt; use Ulrichsg\Getopt\Option; // 参数设定 $nameOpt = new Option('n', 'name', Getopt::REQUIRED_ARGUMENT); $nameOpt->setDescription('用户昵称'); $helpOpt = new Option('h', 'help'); $helpOpt->setDescription('查看帮助信息'); // 获取命令行参数 $getopt = new Getopt(); $getopt->addOptions(array($nameOpt, $helpOpt)); $getopt->parse(); // 输出结果 if ($getopt['help']) { echo $getopt->getHelpText(); exit(1); } echo "Hi, {$getopt['name']}!", PHP_EOL;
把以上PHP代码保存到hello_getopt.php文件,并赋予执行权限,再次执行,我们可以分别通过缩写的参数名或者完整的参数名来获取命令参数,也可以输出帮助信息。以下在命令行终端上的操作以及结果很好地反映了这一点。
$ ./hello_getopt.php -n dogstar
Hi, dogstar!
$ ./hello_getopt.php --name dogstar
Hi, dogstar!
除了正常获取命令行参数外,还可以查看设定的帮助信息。下面是两种等效的使用方式,不管是短参数-h,还是长参数–help,都能查看帮助信息。
$ ./hello_getopt.php -h
Usage: ./hello_getopt.php [options] [operands]
Options:
-n, --name 用户昵称
-h, --help 查看帮助信息
$ ./hello_getopt.php --help
Usage: ./hello_getopt.php [options] [operands]
Options:
-n, --name 用户昵称
-h, --help 查看帮助信息
如果能娴熟地进行到这一步,已经是达到一名合格技术人员的基本要求了。但如果是一名高级的开发工程师,或者是资深的专家,他会继续将众小的工具与现有的项目、系统、框架进行融合,从而组装成更完备的体系或者生态圈,然后开放给更多的技术人员使用。很多开源框架,除了可用于开发网站项目外,还支持开发命令行终端应用,自然而然也会具备命令行参数获取的内置工具。倘若开源框架本身不具备这样的功能,则可以将Ulrichsg/Getopt整合进来,通过扩展类库,或者辅助工具,或者内核升级,或者封装的代理等途径,以填补框架在细分领域的不足或空白。PhalApi框架CLI扩展就是这样的一个例证。
在PhalApi 2.x 版本下,默认的接口服务的Api接口层实现代码片段如下:
// 文件 /phat/to/phalapi/src/app/Api/Site.php <?php namespace App\Api; use PhalApi\Api; class Site extends Api { public function getRules() { return array( 'index' => array( 'username' => array('name' => 'username', 'default' => 'PhalApi', 'desc' => '用户名'), ), ); } public function index() { return array( 'title' => 'Hello ' . $this->username, 'version' => PHALAPI_VERSION, 'time' => $_SERVER['REQUEST_TIME'], ); } }
如果通过浏览器对此接口服务进行访问,请求链接可以是:
http://dev.phalapi.net/?s=App.Site.Index&username=dogstar
接口返回结果类似是:
{"ret":200,"data":{"title":"Hello dogstar","version":"2.2.3","time":1535208634},"msg":""}
如果切换到命令行终端,可以使用phalapi-cli脚本,其背后正是依赖于Ulrichsg\Getopt完成对接口参数的获取。不同PHP-FPM的运行方式,命令行终端的参数来自于命令行的参数输入,而不是POST参数也不是GET参数。先来看下命令行参数的传递效果和执行结果。
phalapi$ ./bin/phalapi-cli -s App.Site.Index --username dogstar
{"ret":200,"data":{"title":"Hello dogstar","version":"2.2.3","time":1535208690},"msg":""}
可以看到,在phalapi项目的根目录下,执行./bin/phalapi-cli脚本,通过-s 命令行参数指定待执行的接口服务名称,再通过–username 命令行参数指定用户名,最后执行的结果和浏览器访问的方式是等效的。
同样地,我们还可以查看指定接口服务的帮助信息,实际上是将接口的参数规则通过标准输出打印到命令行终端。
phalapi$ ./bin/phalapi-cli -s App.Site.Index -h
Usage: ./bin/phalapi-cli [options] [operands]
Options:
-s, --service 接口服务
-h, --help 查看帮助信息
--username [] 用户名
是不是觉得很神奇?接口的参数,不仅可以来自于HTTP协议的POST或GET参数,还可以是来自于命令行终端的参数。更有趣的是,接口参数的规则,既能用于对参数的自动解析、获取和验证,又能用于自动生成在线接口文档,现在还可以用于命令行终端的帮助信息的组成部分!可算是一举多得,这正是软件开发的精髓!新增的设计和组件,不会破坏既有的现存的成品,也不会排斥旧的设计和组件,反而能与过去的系统优雅地结合、互补、共振。这些巧妙的迭代和演进,并不会增加开发人员的认知负担,也不会因为兼容性问题强制现有系统作出升级或者改造,恰恰相反,新的功能给我们提供了更为广阔的选择和发挥空间。这是一片自由的土地,同时也有章可循。
在使用Ulrichsg\Getopt接收和处理完命令行参数后,在执行接口服务之前,我们需要在Ulrichsg\Getopt类库与PhalApi 2.x框架之间搭建两座桥梁,方能完成它们之间的有效通讯和功能整合。第一座桥梁是把PhalApi 2.x里面的接口服务的接口参数规则告诉Ulrichsg\Getopt;第二座则是把Ulrichsg\Getopt解析的命令行参数结果传递给PhalApi 2.x框架。对这部分实现细节感兴趣的读者,可以在Github上查找phalapi/cli项目,查看源代码,这里不过多展开。
关于命令行参数,暂时分享这么多。概括起来就是,简单地,我们可以通过$argv 这个PHP变量获取最原始的命令行参数。推荐的方式是,使用工具或者开源类库,例如Ulrichsg\Getopt,完成对命令参数的解析、获取和帮助。如果现在已经使用了内部框架或者开源框架进行项目开发,而框架本身并没有内置命令行参数工具时,可以考虑把Ulrichsg\Getopt以扩展、工具、组件或代理的方式整合到框架中,以便填补框架的不足。由此延伸的是,在平时开发过程中,我们也可以遵循这个方向,从小到大,从无到有,不断深入研究和挖掘,从而建立自己的核心技术体系。