Das ursprüngliche PHP hatte nur Fehler, aber keine Ausnahmen. Wenn Sie sich einige alte Dokumente ansehen, können Sie sehen, dass viele Fehlerausgaben direkt an das HTML-Tag zurückgegeben werden. Moderne Frameworks haben bereits alles zusammengefasst, und Sie können eine schönere Fehleranzeigeseite erhalten, indem Sie direkt eine Ausnahme auslösen, z. B. die besseren Fehler von Rails. Natürlich haben auch moderne Frameworks für PHP gute Arbeit geleistet, wie zum Beispiel Laravel. Allerdings verwendet unser Unternehmen derzeit noch Codeigniter 2 und dessen Fehler- und Ausnahmebehandlung ist noch relativ grob. durch ein Upgrade auf Die Chance von PHP7 bestand darin, die Fehler- und Ausnahmebehandlungsmechanismen von PHP zu klären.
Empfohlenes Tutorial: „PHP-Tutorial“
PHP-Fehler und Ausnahmen
PHP5 hat eine Ausnahmebehandlung implementiert, die sich nicht von anderen Big-Sprachen unterscheidet , es ist nichts anderes als versuchen, fangen, nicht gefangen, drücken Sie nicht darauf, lassen Sie uns zuerst über den Fehler sprechen.
PHP-Fehler
Neben Ausnahmen kommt es in PHP5 am häufigsten vor, dass Fehler ausgelöst werden. Die Definitionen aller Fehler finden Sie in der offiziellen Dokumentation. Diese Fehler können grob in WARNUNG, FEHLER (schwerwiegender Fehler), HINWEIS usw. unterteilt werden. 1. Der Artikel Zusammenfassung des PHP-Fehlermechanismus enthält die Szenarien, in denen jeder Fehler auftritt.
E_DEPRECATED(8192) Wenn die Laufzeitbenachrichtigung aktiviert ist, wird eine Warnung für Code ausgegeben, der in zukünftigen Versionen möglicherweise nicht ordnungsgemäß funktioniert.
E_USER_DEPRECATED(16384) wird vom Benutzer mithilfe der PHP-Funktion trigger_error() im Code
E_NOTICE(8) Laufzeitbenachrichtigung generiert. Zeigt an, dass das Skript auf eine Situation stößt, die als Fehler erscheinen kann
E_USER_NOTICE(1024) ist eine Benachrichtigungsmeldung, die vom Benutzer mithilfe der PHP-Funktion trigger_error() im Code generiert wird
E_WARNING(2) Laufzeitwarnung (nicht schwerwiegender Fehler)
E_USER_WARNING(512) Wird vom Benutzer mithilfe der PHP-Funktion „trigger_error()“ im Code generiert
E_CORE_WARNING(32) PHP-Initialisierungswarnungen wird vom PHP-Engine-Kern während des Startvorgangs generiert
E_COMPILE_WARNING(128) Die Zend-Skript-Engine hat Warnungen zur Kompilierungszeit generiert
E_ERROR(1) Schwerwiegender Laufzeitfehler
E_USER_ERROR(256) Wird vom Benutzer mit der PHP-Funktion trigger_error() im Code generiert
E_CORE_ERROR(16) Schwerwiegender Fehler, der vom PHP-Engine-Kern während des PHP-Initialisierungs-Startvorgangs generiert wird
E_COMPILE_ERROR( 64) Schwerwiegender Kompilierungsfehler, der von der Zend-Skript-Engine generiert wird
E_PARSE(4) Syntax-Parsing-Fehler bei der Kompilierung. Analysefehler werden nur vom Analysator generiert
E_STRICT(2048) Aktiviert PHPs Vorschläge für Codeänderungen, um die beste Interoperabilität und Vorwärtskompatibilität des Codes sicherzustellen
E_RECOVERABLE_ERROR(4096) Schwerwiegender Fehler, der abgefangen werden kann. Es weist darauf hin, dass ein potenziell gefährlicher Fehler aufgetreten ist, der jedoch nicht zu einer Instabilität der PHP-Engine geführt hat. Wenn der Fehler nicht von einem benutzerdefinierten Handler abgefangen wird (siehe set_error_handler() ), wird er zu einem E_ERROR und das Skript wird beendet.
E_ALL(30719) Alle Fehler- und Warnmeldungen (im Handbuch steht, dass es E_STRICT nicht enthält, aber nach dem Testen enthält es tatsächlich E_STRICT).
Übliche sind:
<?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
Aus historischen Gründen gibt es in diesem alten ci2-Framework viele unzumutbare Dinge, wie z. B. das Lesen nicht vorhandener Protokolldateien. Wir haben auch einige unregelmäßige Verwendungen von PHP, wie zum Beispiel:
<?php $req = []; $user_id = $req['user_id']; // PHP error: Undefined offset if (null === $user_id) { /* do something */}
Viele Stellen in unserem Code verlassen sich auf die Leistung, Null zu erhalten, wenn ein nicht vorhandener Schlüssel abgerufen wird, und jedes Mal, wenn er auf diese Weise verwendet wird, tritt ein E_NOTICE-Fehler auf. Sie können zwar auch array_exists verwenden, wenn dies nicht der Fall ist, aber es ist doch problematischer. Nach PHP7 können Sie klare Datenstrukturen wie Map, Set und Vector über Datenstruktur-Plug-Ins verwenden, um dieses Problem besser zu lösen.
PHP-Fehlerbehandlung
Wenn keine Konfiguration vorgenommen wird, werden PHP-Fehler direkt gedruckt. Alte PHP-Anwendungen haben dies tatsächlich getan. Aber moderne Anwendungen können dies offensichtlich nicht tun. 2:
Stellen Sie sicher, dass PHP Fehler meldet Entwicklungsumgebung, Anzeigefehler;
kann Fehler in Produktionsumgebungen nicht anzeigen;
Fehler sowohl in der Entwicklungs- als auch in der Produktionsumgebung protokollieren.
In einer Produktionsumgebung können Fehler nicht direkt gedruckt werden. Sie sollten in der Protokolldatei aufgezeichnet und eine allgemeine Fehlermeldung an den Benutzer zurückgegeben werden. Die Funktion set_error_handler dient zum Festlegen einer benutzerdefinierten Fehlerbehandlungsfunktion zur Behandlung von Fehlern, die im Skript auftreten. Mit dieser Funktion können wir die Fehlerinformationen in die Protokolldatei schreiben und die Fehlerinformationen einheitlich zurückgeben.
本来这个函数是搭配 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。 ↩