在PHP开发过程中,还有很多高级的技能。一旦掌握、解锁了这些高级特性,对于理解框架的实现、复杂项目的开发都能提升到一个新的层次和高度。这些高级编程技巧在平时的开发中也许会容易被忽略,但在关键的时刻它能发挥着重大的作用,并能协助你解决各种疑难杂症。
这一节主要讲解三个高级编程技巧,分别是:对运行堆栈的回溯追踪、对错误的捕捉,以及不死的代码。
1 福尔摩斯般的回溯跟踪
喜欢侦探破案推理的同学,大概都有看过柯南、福尔摩斯、神探狄仁杰。他们这些主解都能根据一点点的蛛丝马迹,破解重大的案件,还原整个事实的真相。
在PHP世界中,如果发生灵异事件没有头绪,或者无法进一步排查定位原因时,有两个重要的工具可以进行回溯追踪。这两个工具的效果是类似,但表现形式会有所不同。分别是:
debug_backtrace(),产生一条回溯跟踪
debug_print_backtrace(),打印一条回溯
下面通过一个示例来快速认识这两个强大工具的作用。 创建两个临时的PHP文件,一个是/tmp/test.php入口文件,一个是/tmp/Debug.php类文件。在里面分别放置以下代码:
/tmp/test.php入口文件代码:
<?php require_once '/tmp/Debug.php'; function test() { $obj = new Debug(); $obj->printBacktrace('first'); echo PHP_EOL; $obj->backtrace('second'); } test();
/tmp/Debug.php类文件代码:
<?php class Debug { public function printBacktrace($name) { debug_print_backtrace(); } public function backtrace($name) { var_dump(debug_backtrace()); } }
执行/tmp/test.php文件,可以看到以下输出内容。
$ php /tmp/test.php #0 Debug->printBacktrace(first) called at [/tmp/test.php:7] #1 test() called at [/tmp/test.php:12] array(2) { [0] => array(7) { 'file' => string(13) "/tmp/test.php" 'line' => int(9) 'function' => string(9) "backtrace" 'class' => string(5) "Debug" 'object' => class Debug#1 (0) { } 'type' => string(2) "->" 'args' => array(1) { [0] => string(6) "second" } } [1] => array(4) { 'file' => string(13) "/tmp/test.php" 'line' => int(12) 'function' => string(4) "test" 'args' => array(0) { } } }
第一部分是debug_print_backtrace()函数输出的内容,以字符串的形式输出整个调用链。第二部分是debug_backtrace()函数返回的内容,以数组的形式返回。所谓包含的信息非常丰富,细到哪个PHP文件,哪一行代码,哪一个类,哪一个函数或者哪个方法,调用类型是什么,甚至传递的参数都能一清二楚。
看完了简单的示例。开源框架通常都会使用到这两个工具中的一个,或者两个。其中又数debug_backtrace()函数用得最多。
例如在Yii 2中,有:
$ grep debug_backtrace * -R vendor/yiisoft/yii2/base/ErrorHandler.php: $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); vendor/yiisoft/yii2/log/Logger.php: $ts = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
在Symfony 4.0中,有:
$ grep debug_backtrace * -R vendor/symfony/validator/Validator/TraceableValidator.php: $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 7); vendor/symfony/debug/ErrorHandler.php: $lightTrace = $this->tracedErrors & $type ? $this->cleanTrace(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3), $type, $file, $line, false) : array(); vendor/symfony/http-kernel/Kernel.php: $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3); vendor/symfony/http-kernel/DataCollector/DumpDataCollector.php: $trace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS, 7);
在PhalApi 2.2中,有:
$ grep _backtrace * -R vendor/phalapi/kernal/src/Helper/Tracer.php: $backTrace = debug_backtrace();
上面这三个开源框架中,都可以搜索得到debug_backtrace()的身影,并且都采摘了与错误处理、调试有关的搜索结果。
2 错误捕捉
这一节是最有意思的章节之一,因为学完这一节,你将能完全明白很多开源框架中友好的错误提示信息是如何产生的。
先来简单梳理一下PHP有哪些错误,它的级别是什么,以及影响是什么。
Fatal Error 致命错误出现致命错误时,会终止PHP的执行。例如调用了一个不存在的方法,或者常见的在NULL对象调用某个方法。对应的错误级别通常是E_ERROR。
Parse Error 语法错误PHP语法错误时会产生此类错误,例如缺少必要的分号,大括号不对齐。对应的错误级别通常是E_PARSE。你也可以使用php -l 命令检测某个PHP文件是否存在语法错误。Warning Error 警告错误这类错误只会给出相关的提示信息,并不会终止执行。常见的有E_WARNING。
Notice Error 通知错误同样地,这类错误只是给出通知错误信息,不会终止执行。例如输出了一个未声明的变量,则产生”PHP Notice: Undefined variable“提示。常用的有E_NOTICE。另外还有一类错误使用提比较少,那就是E_DEPRECATED弃用提示。例如下面代码运行后,会提示”PHP Deprecated: Function call_user_method() is deprecated in ……“。
<?php error_reporting(E_ALL & E_DEPRECATED); class A { public function test() {} } call_user_method('test', new A());
如果需要设置自己的错误处理,可以使用set_error_handler()函数设置用户自定义的错误处理函数。下面举两个开源框架的例子,一个是Symfony框架,另一个是ThinkPHP框架。
Symfony框架 4
首先,使用以下命令快速安装Symfony,并创建自己的项目symfony。
$ composer create-project symfony/website-skeleton symfony
然后启动服务:
$ php bin/console server:start 0.0.0.0:8000
最后,按照Symfony官方文档的提示,编写一个简单的控制器,但是经过我们改装过后的。在LuckyController::number()里面,我们故意使用了一个未声明的变量$number。代码如下:
<?php // $ vim ./src/Controller/LuckyController.php namespace App\Controller; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; class LuckyController { /** * @Route("/lucky/number", name="app_lucky_number") */ public function number() { return new Response( '<html><body>Lucky number: '.$number.'</body></html>' // 有问题!使用未声明的变量 ); } }
在浏览器访问:http://localhost:8000/lucky/number,可以看到类似这样的错误提示页面。
图 Symfony的错误提示
在Symfony框架代码中搜索set_error_handler,可以发现有以下相关的搜索结果。
$ grep set_error_handler * -R vendor/symfony/debug/ErrorHandler.php: if (null === $prev = set_error_handler(array($handler, 'handleError'))) { vendor/symfony/debug/ErrorHandler.php: set_error_handler(array($handler, 'handleError'), $handler->thrownErrors | $handler->loggedErrors); vendor/symfony/debug/ErrorHandler.php: $handler = set_error_handler('var_dump'); vendor/symfony/debug/ErrorHandler.php: set_error_handler(array($this, 'handleError'), $this->thrownErrors | $this->loggedErrors); vendor/symfony/debug/ErrorHandler.php: set_error_handler(array($this, 'handleError'));
用编辑器打开./vendor/symfony/debug/ErrorHandler.php文件,可以在ErrorHandler::register()内找到使用set_error_handler()函数的身影。为节省篇幅,这里贴出了关键的代码。
<?php namespace Symfony\Component\Debug; class ErrorHandler { /** * Registers the error handler. * * @param self|null $handler The handler to register * @param bool $replace Whether to replace or not any existing handler * * @return self The registered error handler */ public static function register(self $handler = null, $replace = true) { // …… if ($handlerIsNew = null === $handler) { $handler = new static(); } if (null === $prev = set_error_handler(array($handler, 'handleError'))) { restore_error_handler(); // Specifying the error types earlier would expose us to https://bugs.php.net/63206 set_error_handler(array($handler, 'handleError'), $handler->thrownErrors | $handler->loggedErrors); $handler->isRoot = true; } // ……
继续往下看,则可以找到具体处理的函数实现代码。
/** * Handles errors by filtering then logging them according to the configured bit fields. */ public function handleError($type, $message, $file, $line) { // Level is the current error reporting level to manage silent error. // Strong errors are not authorized to be silenced. $level = error_reporting() | E_RECOVERABLE_ERROR | E_USER_ERROR | E_DEPRECATED | E_USER_DEPRECATED; $log = $this->loggedErrors & $type; $throw = $this->thrownErrors & $type & $level; $type &= $level | $this->screamedErrors; // ……
小结一下,有时研究开源框架是一件很有意思的事情。因为你会发现,其实它们做的事情都是差不多的,但各自又有微妙的区别。一旦你掌握了底层PHP原生态函数的使用,就能够完全明白为什么会这样。从框架提供的友好界面开始,根据关键原生态函数名搜索,找到代码的相关位置,然后展开具体实现的代码版本,你就能在其中找到真相,找到你想了解的东西。
ThinkPHP框架 5.0
再来看一下ThinkPHP框架的错误提示是怎样的。安装ThinkPHP 5.0的命令如下:
$ composer create-project topthink/think=5.0.* tp5 --prefer-dist
下载安装完毕后,配置对应的Nginx就可以正常访问了。
接下来,让我们来搞搞破坏,故意制造一些小问题,让它触发回溯追踪。首先,需要开启应用调试模式,并且开启trace功能。这个调试模式几乎在全部框架中都会有开关设置的,因为在生产环境是不允许暴露任何相关的错误信息,以防被外界利用。修改项目下./application/config.php文件,将里面的app_debug和app_trace都更新为true,即:
// 应用调试模式 'app_debug' => true, // 应用Trace 'app_trace' => true,
随后,写一行有问题的代码,比如在默认控制器(对应文件是./application/index/controller/Index.php)里输出一个未声明的变量。
<?php public function index() { echo $aa; // 有问题!输出一个未声明的变量 return '十年磨一剑 - 为API开发设计的高性能框架'; }
再次访问首页后,就会出现完整详细的错误信息,其中就包括了整个请求过程中执行的调用栈。效果类似如下:
图 TP6的错误提示
搜索ThinkPHP框架的源代码,可以发现有以下搜索结果:
$ grep set_error * -R thinkphp/library/think/Error.php: set_error_handler([__CLASS__, 'appError']);
展开看一下前后的代码,有:
<?php namespace think; class Error { /** * 注册异常处理 * @access public * @return void */ public static function register() { error_reporting(E_ALL); set_error_handler([__CLASS__, 'appError']); // …… } /** * 错误处理 * @access public * @param integer $errno 错误编号 * @param integer $errstr 详细错误信息 * @param string $errfile 出错的文件 * @param integer $errline 出错行号 * @return void * @throws ErrorException */ public static function appError($errno, $errstr, $errfile = '', $errline = 0) { $exception = new ErrorException($errno, $errstr, $errfile, $errline); // 符合异常处理的则将错误信息托管至 think\exception\ErrorException if (error_reporting() & $errno) { throw $exception; } self::getExceptionHandler()->report($exception); }
通过set_error_handler([__CLASS__, 'appError']);
这一行代码,ThinkPHP注册了自己的错误处理机制。所以,前面看到的错误页面,正是think\Error::appError()产生输出的。
比较了国外和国内的开源框架后,也许细心读者已经找到了它们的共同点。基本思路都是先注册自己的错误处理回调函数,再具体实现。这里面也用到了我们前面所说的回调类型。可以说是一环扣一环,环环相扣。在这里,再稍微重复做个小广告:理解回调类型真的很重要。
3 生生不息的代码
在我喜欢的三国杀游戏,有个武将角色周泰,他有个技能是不屈。技能的解释,通俗来说,就是他死了不一定是死。在他死之后还可以在无血状态下再多死几次。我们都戏称杀死已经死去的周泰叫”鞭尸“。
PHP编程也有类似的技能,可以在PHP执行结束后仍然能继续执行一段代码。这时,可以使用register_shutdown_function()函数,一个在各大开源框架都会使用到的PHP原生函数。拥有它,你将能拥有更强大的控制力。它所赋予你的能力,甚至能超乎你的想象。
下面来领略一下。
正常执行完毕
毫无疑问,如果使用register_shutdown_function()函数注册了终止时执行的函数,那么在PHP代码执行结束后就会触发所注册的函数的执行。下面代码演示了这一点。
<?php class ErrorHandler { public static function handleAfterShutdown() { echo '这里是终止时将会执行的代码……', PHP_EOL; } } register_shutdown_function('ErrorHandler::handleAfterShutdown'); $date = date('Y-m-d H:i:s'); echo '现在时间是:', $date, PHP_EOL;
同样地,传递给register_shutdown_function()的第一个参数的类型是回调类型,这里我们的回调函数是ErrorHandler::handleAfterShutdown(),结合前面的知识可以知道,类的静态方法的回调函数表示可以使用字符串表示,格式是:(类名) + (::) + (方法名)。
这段代码的执行结果,会输出类似以下内容:
现在时间是:2018-05-21 22:58:16 这里是终止时将会执行的代码……
可以看到,handleAfterShutdown()里面的代码会在最后执行。
使用die()或exit()主动结束
那么,对于在代码执行过程的中途,如果是开发人员自己手动结束的话,会不会也能触发前面注册的函数执行呢?
答案是可以的。因为虽然是手动通过die()或exit()来主动结束PHP代码的执行,但也属于正常的退出,所以最终还是会执行通过register_shutdown_function()注册的回调函数,是顺理成章的。我们可以在前面的代码最后继续添加以下代码来证明这一点。
// …… echo '现在时间是:', $date, PHP_EOL; // 手动终止 die(); $time = time(); echo '当前时间戳是:', $time, PHP_EOL;
继续执行,效果会和前面一样,除了时间显示不同外。
因致命错误导致的终止
刚刚这两种场景都很好理解,但第三种场景可能就要超乎你的想象了。前面有说到,PHP的错误级别中,有Fatal Error 致命错误,比如E_ERROR类型,例如调用一个不存在的方法时就会出现此类错误。那么这时还会继续执行所注册的回调函数吗?
暂时先保留答案,但通过下面的演示代码,我们就不难找到真相了。
调整一下刚才的代码,将die()那一行代码注释掉,并且在下面故意调用一个不存在的方法foo()。然后再次执行。
// 手动终止 // die(); // 故意调用一个不存在的方法 foo(); $time = time(); echo '当前时间戳是:', $time, PHP_EOL;
执行的结果,从前往后,分别输出了现在时间、PHP致命错误的信息,以及最后在handleAfterShutdown()里面的代码!也就是说,即便是出现了致命错误,最终还是会执行所注册的回调函数!
现在时间是:2018-05-21 23:20:50 /path/to/shutdown.php on line 18 PHP Stack trace: PHP 1. {main}()/path/to/shutdown.php:0 这里是终止时将会执行的代码……
register_shutdown_function()是一个非常有用的工具,通常会与set_exception_handler()一起使用。set_exception_handler()可用于设置用户自定义的异常处理函数。这两个函数,在Yii、Symfony、ThinkPHP等开源框架中都能找到相关的使用代码。大家感兴趣,可以像前面那样自行搜索研究。
最后,为了让代码看起来不那么乱,稍微再浪费一点点纸张把本节示例的完整代码粘贴一下。
<?php class ErrorHandler { public static function handleAfterShutdown() { echo '这里是终止时将会执行的代码……', PHP_EOL; } } register_shutdown_function('ErrorHandler::handleAfterShutdown'); $date = date('Y-m-d H:i:s'); echo '现在时间是:', $date, PHP_EOL; // 手动终止 // die(); // 故意调用一个不存在的方法 foo(); $time = time(); echo '当前时间戳是:', $time, PHP_EOL; echo '你不会看到这一句,正如你看不到我在写这书时被多少个蚊子在追杀[捂脸]', PHP_EOL;
注册生生不息的代码这一技巧,既可以用在框架设计中增强对代码的控制强,为开发工程师提供友好的错误提示页面并引导其快速定位问题,还可以用在业务项目中以满足特定场景下的非功能性需求,例如可以将写入到文件的日记,每次纪录时先放入一个PHP数组,最后执行完毕后,再打包一次性写入。
小结
关于PHP原生态的使用,暂时先探讨到这里。这里稍微做一个简单的回顾,以加深对这一章的理解。
很多技术知识点,看似零散,实际是有着微妙的联系,编织起来可构成一张完全的技能网络。正如本章所介绍的空与非空、数组排序、魔术方法、阻塞式调用、回调函数以及回溯追踪等。还是那句话,PHP入门很简单,但要想做到深入和精通,需要持续投入很多的时间和精力。如果将回调函数,以及错误捕捉、不同回调函数的注册等结合起来,你就能明白很多开源框架设计的原理,各自的应用场景,以及最终表现出来的界面背后的运行机制。同时,在本章中,我们也分别探索了Yii、Symfony、ThinkPHP、PhalApi等PHP开源框架在这些方面相关的底层代码。可能一开始还是还难完全吸收,但可以先有一个初步的概念,后面再慢慢消化,逐渐做到融会贯通。PHP原生态蕴藏着巨大的知识宝藏,这里所提及的只是冰山一角。希望能对大家有所帮助,同时激发各位对技术深究的好奇心。
我们讨论了生生不息的代码。其实这里是一语双关。PHP代码执行结束了,并不是真的就结束了,可能还有额外的代码正在开始执行。一个结束,是另一个开始。这也意味着学习PHP也一样,虽然这一章的学习结束了,但对于我,对于大家,对于普遍的开发工程师,学习PHP企业级系统开发,这只是一个开始。后面,我们将转入理论学习与项目实战之间的过滤阶段。如果说理论学习是教学,项目实战是上前线、上战场,那么下一章介绍的PHPUnit单元测试就是岗前练兵、模拟实战。目的是为了让我们在一个更安全的环境下,进行尝试和锻炼。准备好了吗?休息一下,继续前进!