元の PHP にはエラーのみがあり、例外はありませんでした。いくつかの古いドキュメントを見ると、多くのエラー出力が html タグに直接エコーされていることがわかります。最新のフレームワークはすでにすべてをまとめており、Rails のより優れたエラーなどの例外を直接スローすることで、より美しいエラー表示ページを作成できます。もちろん、laravel などの最新の PHP フレームワークも良い仕事をしています。ただし、当社では現在でも codeigniter 2 を使用しており、そのエラーおよび例外処理はまだ比較的粗雑です。にアップグレードすることで PHP7 のきっかけは、PHP のエラーおよび例外処理メカニズムを整理することでした。
推奨チュートリアル: 「PHP チュートリアル 」
PHP エラーと例外
PHP5 には例外処理が実装されており、これは他の言語と変わりません。 、それは試してみる、捕まえる、捕まえられない、押し下げないでください、最初にエラーについて話しましょう。
PHP エラー
例外に加えて、PHP5 で最も一般的なのはエラーをスローすることです。すべてのエラーの定義は公式ドキュメントで確認できます。これらのエラーは、警告、エラー (致命的なエラー)、通知などに大別できます。 「PHP のエラー メカニズムの概要」という記事には、各エラーが発生するシナリオが記載されています。
E_DEPRECATED(8192) 実行時通知を有効にすると、将来のバージョンで正しく動作しなくなる可能性があるコードについて警告が表示されます。#E_NOTICE(8) ランタイム通知です。スクリプトがエラーとして表示される可能性のある状況に遭遇したことを示します。E_USER_DEPRECATED(16384) は、コード内で PHP 関数 trigger_error() を使用してユーザーによって生成される
E_USER_NOTICE(1024) は、コード内で PHP のtrigger_error() 関数を使用してユーザーによって生成された通知メッセージです。
#E_WARNING(2) 実行時警告 (致命的ではないエラー)
E_USER_WARNING(512) コード内で PHP のtrigger_error() 関数を使用してユーザーによって生成されましたE_CORE_WARNING(32) PHP 初期化警告起動時に PHP エンジン コアによって生成されるE_COMPILE_WARNING(128) Zend スクリプト エンジンはコンパイル時警告を生成します E_ERROR(1) 致命的なランタイム エラー
E_USER_ERROR(256) ユーザーはコード内で PHP のtrigger_error() 関数を使用して E_CORE_ERROR(16) PHP の初期化起動プロセス中に PHP エンジン コアによって生成された致命的なエラーE_COMPILE_ERROR (64) Zend スクリプト エンジンによって生成された致命的なコンパイル時エラー E_PARSE(4) コンパイル時の構文解析エラー。解析エラーはアナライザーによってのみ生成されます
#E_STRICT(2048) PHP がコードへの変更を提案できるようにして、コードの最高の相互運用性と上位互換性を確保します
E_RECOVERABLE_ERROR(4096) 捕らえられる可能性のある致命的なエラー。これは、潜在的に危険なエラーが発生したが、PHP エンジンが不安定になる原因ではなかったことを示します。エラーがユーザー定義のハンドラー ( set_error_handler() を参照) によって捕捉されない場合、エラーは E_ERROR となり、スクリプトは終了します。
E_ALL(30719) すべてのエラーおよび警告メッセージ (マニュアルには E_STRICT が含まれていないと記載されていますが、テスト後、実際には E_STRICT が含まれています)。
一般的なものは次のとおりです:コード内の多くの場所で、存在しないキーの取得と null の取得というこの種のパフォーマンスに依存しており、これが使用されるたびに E_NOTICE エラーが発生します。 array_exists を使用して if else を実行することもできますが、結局のところ、その方が面倒です。 PHP7 以降では、データ構造プラグインを通じて Map、Set、Vector などの明確なデータ構造を使用して、この問題をより適切に解決できるようになりました。 PHP エラー処理設定が行われていない場合、PHP エラーが直接出力されます。古い PHP アプリケーションは実際にこれを行っていました。しかし、最新のアプリケーションでは明らかにこれができません。最新のアプリケーションのエラーは次のルールに従う必要があります<?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歴史的な理由により、この古い ci2 フレームワークには、存在しないログ ファイルを読み取るなど、多くの不合理な点があり、また、次のような PHP の変則的な使用法もあります。
2
:必ず PHP にエラーを報告させてください;開発環境、エラーの表示;
運用環境ではエラーを表示できません; 開発環境と運用環境の両方でエラーを記録します。運用環境では、エラーを直接出力することはできません。エラーはログ ファイルに記録され、一般的なエラー メッセージがユーザーに返されます。 set_error_handler 関数は、スクリプト内で発生したエラーを処理するためのユーザー定義のエラー処理関数を設定します。この関数ではエラー情報をログ ファイルに書き込み、エラー情報を一律に返すことができます。
本来这个函数是搭配 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。 ↩