The original PHP only had errors and no exceptions. Looking at some old documents, you can see that a lot of error output is directly echoed to the html tag. Modern frameworks have already wrapped everything up, and you can have a more beautiful error display page by directly throwing an exception, such as rails' better errors. Of course, modern frameworks for PHP have also done a good job, such as laravel. However, our company currently still uses codeigniter 2, and its error and exception handling are still relatively crude. by upgrading to The opportunity of PHP7 was to sort out PHP’s error and exception handling mechanisms.
Recommended tutorial: "PHP Tutorial"
PHP Errors and Exceptions
PHP5 has implemented exception handling, which is no different from other languages. Big, it's nothing more than try, catch, uncaught, don't press down, let's talk about the error first.
PHP Errors
In addition to exceptions, the most common thing in PHP5 is to throw errors. You can find the definitions of all errors in the official documentation. These errors can be roughly divided into WARNING, ERROR (fatal error), NOTICE, etc.1. The article Summary of PHP's Error Mechanism gives the scenarios in which each error occurs.
E_DEPRECATED(8192) Runtime notification, when enabled, will give a warning for code that may not work properly in future versions.
E_USER_DEPRECATED(16384) is a
#E_NOTICE(8) runtime notification generated by the user using the PHP function trigger_error() in the code. Indicates that the script encounters a situation that may appear as an error
E_USER_NOTICE(1024) is a notification message generated by the user using PHP's trigger_error() function in the code
E_WARNING(2) Runtime warning (non-fatal error)
E_USER_WARNING(512) Generated by the user using PHP's trigger_error() function in the code
E_CORE_WARNING(32) PHP initialization Warnings generated by the PHP engine core during startup
E_COMPILE_WARNING(128) Zend script engine generates compile-time warnings
E_ERROR(1) Fatal runtime errors
E_USER_ERROR(256) The user uses PHP's trigger_error() function in the code to generate
E_CORE_ERROR(16) A fatal error generated by the PHP engine core during the PHP initialization startup process
E_COMPILE_ERROR(64) Fatal compile-time error generated by Zend script engine
E_PARSE(4) Compile-time syntax parsing error. Parsing errors are generated only by the analyzer
E_STRICT(2048) Enable PHP to suggest modifications to the code to ensure the best interoperability and forward compatibility of the code
E_RECOVERABLE_ERROR(4096) Fatal error that can be caught. It indicates that a potentially dangerous error has occurred, but has not caused the PHP engine to become unstable. If the error is not caught by a user-defined handler (see set_error_handler() ), it will become an E_ERROR and the script will terminate.
E_ALL(30719) All error and warning messages (the manual says it does not contain E_STRICT, but after testing, it actually contains E_STRICT).
Common ones are:
<?php // E_ERROR nonexist(); // PHP Fatal error: Call to undefined function nonexist() throw new Exception(''); // 未捕获异常也是 fatal error // E_NOTICE $a = $b; // PHP Notice: Undefined variable $a = []; $a[2]; // PHP Notice: Undefined offset: 2 // E_WARNNING require 'nonexist.php' // warning and fatal error
Due to historical reasons, this old ci2 framework has many unreasonable things, such as reading non-existent log files; we also have some irregular uses of PHP, such as:
<?php $req = []; $user_id = $req['user_id']; // PHP error: Undefined offset if (null === $user_id) { /* do something */}
Many places in our code rely on this kind of performance of obtaining non-existent keys and getting null, and every time this is used, there will be an E_NOTICE error. Although you can use array_exists to do if else, it is more troublesome after all. After PHP7, you can use clear data structures such as Map, Set, and Vector through data structure plug-ins to better solve this problem.
PHP error handling
If no configuration is made, PHP errors will be printed directly. Old PHP applications actually did this. But modern applications obviously cannot do this. Errors in modern applications should follow the following rules2:
Be sure to let PHP report errors;
In the development environment, Display errors;
cannot display errors in the production environment;
Records errors in both development and production environments.
In a production environment, errors cannot be printed directly. They should be recorded in the log file and a general error message returned to the user. The set_error_handler function is to set a user-defined error handling function to handle errors that occur in the script. We can write the error information to the log file in this function and return the error information uniformly.
本来这个函数是搭配 trigger_error 函数使用的。用户通过 trigger_error 产生 error,然后用 error_handler 来处理错误。只是在这种场景下往往「异常」更好用,所以这么用的并不多。
在前述的系统自带的 16 种错误中,有一部分相当重要的错误并不能被 error_handler 捕获3:
以下级别的错误不能由用户定义的函数来处理: E_ERROR、 E_PARSE、 E_CORE_ERROR、 E_CORE_WARNING、 E_COMPILE_ERROR、E_COMPILE_WARNING,和在调用 set_error_handler() 函数所在文件中产生的大多数 E_STRICT。
这些错误将无法记录下来,同时也不方便统一处理4。在 PHP7 之前的 PHP 版本一个很大的痛点就是:发生了 E_ERROR 错误,无法捕获,导致数据库的事务无法回滚造成数据不一致5。
另外一个需要注意的是, error_handler 处理完毕,脚本将会继续执行发生错误的后一行。在某些情况下,你可能希望遇到某些错误可以中断脚本的执行。在官方文档中已说明,
同时注意,在需要时你有责任使用 die()。 如果错误处理程序返回了,脚本将会继续执行发生错误的后一行。
也就是说,某些情况下,我们处理完 E_WARNING 之后,需要及时退出脚本(即 die() 或者 exit())。
PHP 异常
异常是对程序错误的一种优秀的处理方式,较于错误,异常的优点是默认打印调用栈,便于调试,可控等,可以参考一下鸟哥的文章我们什么时候应该使用异常,清晰的点明了错误码和异常的优缺点。
对异常的处理也要遵循前述的错误处理规则2。在我们的日常开发中,不可能保证可以 catch 所有的异常,而未被 catch 的异常将以 fatal error 的形式中断脚本的执行并输出错误信息。所以要借助 set_exception_handler,统一处理所有未被 catch 的异常。我们可以像 error_handler 那样,在 exception_handler 中处理 log,将数据库的事务回滚。
前面提到,error_handler 需要在必要的时候手动中断脚本, PHP 文档中给出的一种实践是,在 error_handler 中 throw ErrorException,代码示例如下:
<?php function exception_error_handler($severity, $message, $file, $line) { if (!(error_reporting() & $severity)) { // This error code is not included in error_reporting return; } throw new ErrorException($message, 0, $severity, $file, $line); } set_error_handler("exception_error_handler"); /* Trigger exception */ strpos();
这样凡是不想忽略的 error,都会以 Uncaught ErrorException 的形式返回并中断脚本。
PHP 异常机制
鸟哥通过一个例子讲解了 PHP 的异常的处理机制,在这里转述一下。
<?php function onError($errCode, $errMesg, $errFile, $errLine) { echo "Error Occurred\n"; throw new Exception($errMesg); } function onException($e) { echo '********exception: ' . $e->getMessage(); } set_error_handler("onError"); set_exception_handler("onException"); require("nonexist.php");
其运行结果为
- Error Occurred
- PHP Fatal error
而 onException 并没有执行到,说明在 error_handler 中 throw exception 不会被 exception_handler 截获。
require 不存在的文件会抛出两个错误,
- WARNING : 在PHP试图打开这个文件的时候抛出
- E_COMPILE_ERROR : 从PHP打开文件的函数返回失败以后抛出
PHP 中的异常处理机制如下:
而PHP在遇到 Fatal Error 的时候,会直接 zend_bailout,而 zend_bailout 会导致程序流程直接跳过上面代码段,也可以理解为直接 exit 了(longjmp),这就导致了 user_exception_handler 没有机会发生作用。
PHP 错误分类
综上所述,在 PHP 中,错误和异常可以分为以下 3 个类别:异常,可截获错误,不可截获错误。异常和可截获错误虽然机理不同,但可以当做是同一种处理方式,而不可截获错误是另一种,是一种较为棘手的错误类型。马上将会讲到,PHP7 中的 fatal error 是一种继承自 Throwable 的 Error,是可以被 try catch 住的。通过这一方式 PHP7 解决了这一难题。
PHP7 的错误和异常
PHP 7 改变了大多数错误的报告方式。不同于传统(PHP 5)的错误报告机制,现在大多数错误被作为 Error 异常抛出(在 PHP7 中,只有 fatal error 和 recoverable error 抛出异常,其他 error 比如 warning 和 notice 的表现不变6)。PHP7 中的 Error 和 Exception 的关系如图 6:
interface Throwable |- Exception implements Throwable |- ... |- Error implements Throwable |- TypeError extends Error |- ParseError extends Error |- ArithmeticError extends Error |- pisionByZeroError extends ArithmeticError |- AssertionError extends Error
值得注意的是,Error 类表现上和 Exception 基本一致,可以像 Exception 异常一样被第一个匹配的 try / catch 块所捕获,如果没有匹配的 catch 块,则调用异常处理函数(事先通过 set_exception_handler() 注册7)进行处理。 如果尚未注册异常处理函数,则按照传统方式处理,被报告为一个致命错误(Fatal Error)。但并非继承自 Exception 类(要考虑到和 PHP5 的兼容性),所以不能用 catch (Exception $e) { ... } 来捕获,而需要使用 catch (Error $e) { ... },当然,也可以使用 set_exception_handler 来捕获。
但是,用户不能自己定义类实现 Throwable,这是为了保证只有 Exception 和 Error 才可以抛出。
PHP7 的 ERROR 处理
PHP7 中的 fatal error 会抛出 Error,且可以被正常 catch 到:
<?php $a = 1; try { $a->nonexist(); } catch (Error $e) { // Handle error }
也有些错误场景下会抛出更加详细的错误,比如:
<?php // TypeError function test(int $i) { echo $i; } try { test('test'); } catch (TypeError $e) { // Handle error } // ParseError try{ eval('i=1;'); } catch (ParseError $e) { echo $e->getMessage(), "\n"; } // ArithmeticError try { $value = 1 << -1; } catch (ArithmeticError $e) { echo $e->getMessage(), "\n"; } // pisionByZeroError try { $value = 1 % 0; } catch (pisionByZeroError $e) { echo $e->getMessage(), "\n"; }
Error 和 Exception 的选择
当需要自定义处理错误的时候,应该选择继承 Error 还是 Exception 呢?
我们注意到,PHP7 中是将曾经的 fatal error 变成了 Error 抛出,而 fatal error 一般都是一些不需要在运行时处理的错误,这种错误旨在提醒程序员,这里的代码写的有问题,需要修复,而不是逻辑上要 catch 它做某些业务。
因此,绝大多数情况下,我们并不需要继承 Error,甚至 catch Error 也不常见,只在某些需要 log,回滚数据库,清理现场等场合才需要这样做。
对错误和异常的一种实践
根据以上所述,我们提炼了一个对错误和异常处理较好的实践。
- 对于业务中不应该出现错误的地方,抛出 InternalException,而不是 Error
<?php class InternalException extends Exception { /*...*/ } function find(Array $ids) { if (empty($ids)) { throw new InternalException('ids should not be empty'); } ... }
- 只在需要清理现场的时候 catch Error
<?php try { /*...*/ } catch (Throwable $t) { // log, transaction rollback, cleanup... }
- 未捕获的 Error 和 Exception 通过 set_exception_handler 做后续清理和 log
- 其他错误仍然通过 set_error_handler 来处理,在处理的时候使用更加明确的 FriendlyErrorType,并抛出 ErrorException 记录调用栈
FriendlyErrorType:
<?php function FriendlyErrorType($type) { switch($type) { case E_ERROR: // 1 // return 'E_ERROR'; case E_WARNING: // 2 // return 'E_WARNING'; case E_PARSE: // 4 // return 'E_PARSE'; case E_NOTICE: // 8 // return 'E_NOTICE'; case E_CORE_ERROR: // 16 // return 'E_CORE_ERROR'; case E_CORE_WARNING: // 32 // return 'E_CORE_WARNING'; case E_COMPILE_ERROR: // 64 // return 'E_COMPILE_ERROR'; case E_COMPILE_WARNING: // 128 // return 'E_COMPILE_WARNING'; case E_USER_ERROR: // 256 // return 'E_USER_ERROR'; case E_USER_WARNING: // 512 // return 'E_USER_WARNING'; case E_USER_NOTICE: // 1024 // return 'E_USER_NOTICE'; case E_STRICT: // 2048 // return 'E_STRICT'; case E_RECOVERABLE_ERROR: // 4096 // return 'E_RECOVERABLE_ERROR'; case E_DEPRECATED: // 8192 // return 'E_DEPRECATED'; case E_USER_DEPRECATED: // 16384 // return 'E_USER_DEPRECATED'; } return ""; }
error_handler:
<?php function exception_error_handler($severity, $message, $file, $line) { if (!(error_reporting() & $severity)) { // This error code is not included in error_reporting return; } log FriendlyErrorType($severity); throw new ErrorException($message, 0, $severity, $file, $line); } set_error_handler("exception_error_handler");
PHP中的错误级别与具体报错信息分类 ↩
PHP 最佳实践之异常和错误 ↩ ↩2
E_ERROR 无法捕获,E_RECOVERABLE_ERROR 可以,后者默认输出 Catachable fatal error ↩
fatal error 会记录到 web 服务器的 error.log,这一点需要注意,因为这个 log 的位置往往不是 PHP 应用定义的,而是 web 服务器定义的。 ↩
PHP 中还有一个 register_shutdown_function 函数,它允许注册一个会在 PHP 中止时执行的函数,这个函数可以捕获 fatal error,毕竟是只要是脚本中断就可以捕获的。ci2 并没有使用这个方法,所以相关问题一直没有得到很好的解决,当时也没有意识到这个函数的存在,升级 PHP7 之后可以通过 catch Error 来解决,便不再需要这样处理了。 ↩
Throwable Exceptions and Errors in PHP 7 ↩ ↩2
在 PHP7 中,传入 exception_handler 的参数从 Exception 改为 Throwable,这意味着 exception_handler 可以截获 Error。 ↩