目录

PHP 中的错误和异常处理

PHP 当然也有自己的异常处理,不过那已经是 PHP5 以后的事了。就我自己这些年写程序的现状看,我基本上就没有真正明白什么是异常处理,经常把异常和错误处理混为一谈,关于代码中的那些写法,不是写错了,就是写的太特么烂了。恰好最近在写一些类时用到异常处理了,顺便就把这个整理下,但是这个仅代表我个人的一些理解和使用,也可能是错误的,还请谨慎阅读。

概述

错误处理定义

错误是指导致系统不能按照用户意图工作的一切原因、事件。在程序设计过程中,由于某些错误的存在,致使程序无法正常运行,处理这些错误以使程序正确运行就称为错误处理。错误处理功能是衡量编译器性能的重要方面,它在帮助程序员尽快修改程序方面起到了非常重要的作用。

错误类型:

  • 语法错误
  • 语义错误
  • 逻辑错误

异常处理定义

编程语言或计算机硬件里的一种机制,用于处理软件或信息系统中出现的异常状况(即超出程序正常执行流程的某些特殊条件)

预定义的错误代码

当然你也可以参见官网的手册 预定义常量,我抄一遍纯粹是为了加深记忆。至于每种错误在什么情况下产生,你可以参看 PHP的错误机制总结

常量取值说明备注
E_ERROR1致命的运行时错误。这类错误一般是不可恢复的情况,例如内存分配导致的问题。后果是导致脚本终止不再继续运行
E_WARNING2运行时警告(非致命错误)。仅给出提示信息,但是脚本不会终止运行
E_PARSE4编译时语法解析错误。解析错误仅仅由分析器产生
E_NOTICE8运行时通知。表示脚本遇到可能会表现为错误的情况,但是在可以正常运行的脚本里面也可能会有类似的通知
E_CORE_ERROR16在 PHP 初始化启动过程中发生的致命错误。该错误类似 E_ERROR,但是是由 PHP 引擎核心产生的since PHP 4
E_CORE_WARNING32PHP 初始化启动过程中发生的警告(非致命错误)。类似 E_WARNING,但是是由 PHP 引擎核心产生的since PHP 4
E_COMPILE_ERROR64致命编译时错误。类似 E_ERROR,但是是由 Zend 脚本引擎产生的since PHP 4
E_COMPILE_WARNING128编译时警告(非致命错误)。类似 E_WARNING,但是是由 Zend 脚本引擎产生的since PHP 4
E_USER_ERROR256用户产生的错误信息。类似 E_ERROR,但是是由用户自己在代码中使用 PHP 函数 trigger_error() 来产生的since PHP 4
E_USER_WARNING512用户产生的警告信息。类似 E_WARNING,但是是由用户自己在代码中使用 PHP 函数 trigger_error() 来产生的since PHP 4
E_USER_NOTICE1024用户产生的通知信息。类似 E_NOTICE,但是是由用户自己在代码中使用 PHP 函数 trigger_error() 来产生的since PHP 4
E_STRICT2048启用 PHP 对代码的修改建议,以确保代码具有最佳的互操作性和向前兼容性since PHP 5
E_RECOVERABLE_ERROR4096可被捕捉的致命错误。它表示发生了一个可能非常危险的错误,但是还没有导致 PHP 引擎处于不稳定的状态。如果该错误没有被用户自定义句柄捕获(参见 set_error_handler()),将成为一个 E_ERROR 从而脚本会终止运行since PHP 5.2.0
E_DEPRECATED8192运行时通知。启用后将会对在未来版本中可能无法正常工作的代码给出警告since PHP 5.3.0
E_USER_DEPRECATED16384用户产少的警告信息。类似 E_DEPRECATED,但是是由用户自己在代码中使用 PHP 函数 trigger_error() 来产生的since PHP 5.3.0
E_ALL30719E_STRICT 出外的所有错误和警告信息30719 in PHP 5.3.x, 6143 in PHP 5.2.x, 2047 previously
警告
  • 可以使用按位运算符来组合这些值或者屏蔽某些类型的错误。在 php.ini 之中,仅 |, ~, !, ^, & 这些操作符会被正确解析
  • 以下级别的错误不能由用户定义的函数来处理:E_ERRORE_PARSEE_CORE_ERRORE_CORE_WARNINGE_COMPILE_ERRORE_COMPILE_WARNING 和在调用 set_error_handler() 函数所在文件中产生的大多数 E_STRICT

相关文章

早有人把 PHP 各个版本的错误和异常处理类树整理出来供我等参观。如果在本地执行此代码,依据本地对 PHP 安装的扩展的不同,会把扩展中的相关异常处理类一并打印出来

错误处理

  • Note that other types of errors such as warnings and notices remain unchanged in PHP 7. Only fatal and recoverable errors throw exceptions.
  • Prior to PHP 7 alpha-2, the exception hierarchy in PHP 7 was different. Fatal and recoverable errors threw instances of EngineException, which did not inherit from Exception. Both Exception and EngineException inherited from BaseException. The hierarchy was revised with the RFC I authored, Throwable Interface. I felt switching to Throwable and Error was important to avoid confusion from classes using the suffix Exception that did not extend Exception, as well as being more concise and appealing names.

E_ERRORE_RECOVERABLE_ERROR 级别的错误在 PHP7 之前是不能被捕获到的,也就是说,你无法使用 try...catch... 这样的语句捕获到这种级别的错误,但不管是 PHP7 还是 PHP5 对于未捕获的异常依然是一个致命错误

在 PHP5 中

1
2
3
4
5
6
7
<?php

$object = null;
$object->method();

// 结果
Fatal error: Call to a member function method() on null in

在 PHP7 中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<?php

$object = null;

try {
    $object->method();
} catch (Error $error) {
    echo $error->getMessage();
}

// 结果
Call to a member function method() on null

是的,PHP7 内置了一个名为 Error 的类来捕获一些致命错误和异常了,其中 ErrorException 类实现了 Throwable 接口。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Throwable 接口
Throwable {
    /* Methods */
    abstract public string getMessage ( void )
    abstract public int getCode ( void )
    abstract public string getFile ( void )
    abstract public int getLine ( void )
    abstract public array getTrace ( void )
    abstract public string getTraceAsString ( void )
    abstract public Throwable getPrevious ( void )
    abstract public string __toString ( void )
}
警告
开发者不能直接实现 Throwable 接口,必须通过 Error 或者 Exception 等类去扩展

也可以用以下的方式去扩展自己的错误和异常类。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?php

interface TestThrowable extends \Throwable
{
}

class TestException extends \Exception implements TestThrowable
{
}

class DemoError extends \Error implements TestThrowable
{
}

异常处理

由于 ErrorException 都实现了 Throwable 接口,那么问题就来了,在程序中我们究竟要用哪个?通过一些最佳实践我们知道,错误一般是去约束程序员和代码语法、语义及逻辑方面的,而异常是程序在运行过程出现的一些不合期望的情况。因此,在代码中我们应当使用异常而不是错误。

PHP 7.2.0 - 7.2.3 版本中提供的内置异常:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
Exception
  ClosedGeneratorException
  DOMException
  ErrorException
  IntlException
  LogicException
    BadFunctionCallException
      BadMethodCallException
    DomainException
    InvalidArgumentException
    LengthException
    OutOfRangeException
  PharException
  ReflectionException
  RuntimeException
    OutOfBoundsException
    OverflowException
    PDOException
    RangeException
    UnderflowException
    UnexpectedValueException
  SodiumException

兼容 PHP5 和 PHP7 的异常处理

1
2
3
4
5
6
7
8
9
<?php

try {
    // Code that may throw an Exception or Error.
} catch (Throwable $t) {
    // Executed only in PHP 7, will not match in PHP 5.x.
} catch (Exception $e) {
    // Executed only in PHP 5.x, will not be reached in PHP 7.
}

自定义捕获错误和异常

  • 未捕获的 ErrorException 通过 set_exception_handler() 做后续清理和日志记录
  • 其他错误仍然通过 set_error_handler() 来处理,在处理的时候使用更加明确的 E_* 类型,并抛出 ErrorException 记录调用栈

以下实例中使用 set_error_handler() 函数去捕获错误,设置一个用户自定义的错误处理函数,这里我就粗暴的抛出 了 ErrorException 异常,当然官方文档也明确说了这个函数不能捕获 E_ERRORE_PARSEE_CORE_ERRORE_CORE_WARNINGE_COMPILE_ERRORE_COMPILE_WARNING 和在调用 set_error_handler() 函数所在文件中产生的大多数 E_STRICT 异常,言外之语也就是仅能捕获用户定义的错误和异常。使用 set_exception_handler() 函数去设置默认的异常处理程序,用在没有用 try/catch 块来捕获的异常,也就是说不管你抛出的异常有没有人捕获,如果没有人捕获就会进入到该方法中,并且在回调函数调用后异常会中止。使用 register_shutdown_function() 函数去捕获 PHP 的错误:Fatal or Parse Error 等,正由于这个函数是 PHP 脚本执行结束前的最后一个有效调用,所以我们才用它来完成一些不可思议的事情。一般情况下,都会在这个函数的回调函数中使用 error_get_last() 函数来获取执行产生的错误,顺便说一下,像语法这样的错误是捕获不到的

正常情况时的异常捕获

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<?php

class ErrorHandler
{
    public static function register()
    {
        error_reporting(E_ALL);
        set_error_handler([__CLASS__, 'handleError']);
        set_exception_handler([__CLASS__, 'handleException']);
        register_shutdown_function([__CLASS__, 'handleShutdown']);
    }

    public static function handleError($code, $message, $file, $line)
    {
        throw new \ErrorException($message, $code, 1, $file, $line);
    }

    public static function handleException($e)
    {
        self::handle($e);
    }

    public static function handleShutdown()
    {
        $error = error_get_last();

        if (null !== $error && self::isFatal($error['type'])) {
            self::handleException($error);
        }
    }

    /**
     * @param \Exception|\Throwable $e
     */
    public static function handle($e)
    {
        $message = $e->getMessage() . PHP_EOL;
        echo $message;

        error_log($message, 3, __DIR__ . '/run.log');
    }

    protected static function isFatal($type): bool
    {
        $types = [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE];

        return in_array($type, $types, true);
    }
}

ErrorHandler::register();

$object = null;
$object->toArray();

// 结果
Call to a member function toArray() on null
此时错误已经记录到日志文件中

写错语法时的异常捕获

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<?php

// 此处略去 ErrorHandler 类代码,详见上面
ErrorHandler::register();

$object = null;

// 这儿故意不写分号
$object->toArray()

// 结果
PHP Parse error:  syntax error, unexpected end of file in ...
PHP
PHP Dump $_SERVER
PHP

Parse error: syntax error, unexpected end of file in ...

事情进展到这里,按理来说,我应该就此停笔收尾,弄个总结之类的一忽悠一下正在阅读的你。但是呢,我又忽然想起来 PHP 那么多的框架是如何处理这个事情的,我想你也好奇,不妨我们去看看

框架中的错误异常处理

很遗憾的告诉你,我不会剖析框架源码,我只想做个 demo 得瑟一下,剩下全交给你。此处我以 symfony debug 为例

1
2
// 安装 symfony/debug
$ composer require symfony/debug
1
2
3
4
5
6
7
8
9
<?php

require __DIR__ . '/vendor/autoload.php';

\Symfony\Component\Debug\Debug::enable();
\Symfony\Component\Debug\ErrorHandler::register();

$object = null;
$object->toArray();

https://inotes.oss-cn-beijing.aliyuncs.com/php/201812/php-error-exception-handler.png