• Laravel Exceptions——异常与错误处理


    点击上方“ 码农编程进阶笔记 ”,选择“置顶或者星标”
    文末有干货,每天定时与您相约!
    

    前言

    本文 GitBook 地址:https://legacy.gitbook.com/book/leoyang90/laravel-source-analysis/details

    对于一个优秀的框架来说,正确的异常处理可以防止暴露自身接口给用户,可以提供快速追溯问题的提示给开发人员。本文会详细的介绍 laravel 异常处理的源码。

    PHP 异常处理

    本章节参考 PHP 错误异常处理详解。

    异常处理(又称为错误处理)功能提供了处理程序运行时出现的错误或异常情况的方法。

     

    异常处理通常是防止未知错误产生所采取的处理措施。异常处理的好处是你不用再绞尽脑汁去考虑各种错误,这为处理某一类错误提供了一个很有效的方法,使编程效率大大提高。当异常被触发时,通常会发生:

    • 当前代码状态被保存

    • 代码执行被切换到预定义的异常处理器函数

    • 根据情况,处理器也许会从保存的代码状态重新开始执行代码,终止脚本执行,或从代码中另外的位置继续执行脚本

    PHP 5 提供了一种新的面向对象的错误处理方法。可以使用检测(try)、抛出(throw)和捕获(catch)异常。即使用 try 检测有没有抛出(throw)异常,若有异常抛出(throw),使用 catch 捕获异常。

    一个 try 至少要有一个与之对应的 catch。定义多个 catch 可以捕获不同的对象。php 会按这些 catch 被定义的顺序执行,直到完成最后一个为止。而在这些 catch 内,又可以抛出新的异常。

    异常的抛出

    当一个异常被抛出时,其后的代码将不会继续执行,PHP 会尝试查找匹配的 catch 代码块。如果一个异常没有被捕获,而且又没用使用 set_exception_handler() 作相应的处理的话,那么 PHP 将会产生一个严重的错误,并且输出未能捕获异常 (Uncaught Exception ... ) 的提示信息。

    抛出异常,但不去捕获它:

    ini_set('display_errors', 'On');  
    error_reporting(E_ALL & ~ E_WARNING);  
    $error = 'Always throw this error';  
    throw new Exception($error);  
    // 继续执行  
    echo 'Hello World';
    

    上面的代码会获得类似这样的一个致命错误:

    Fatal error: Uncaught exception 'Exception' with message 'Always throw this error' in E:\sngrep\index.php on line 5  
    Exception: Always throw this error in E:\sngrep\index.php on line 5  
    Call Stack:  
        0.0005     330680   1. {main}() E:\sngrep\index.php:0  
    

    Try, throw 和 catch

    要避免上面这个致命错误,可以使用 try catch 捕获掉。

    处理处理程序应当包括:

    • Try - 使用异常的函数应该位于 "try" 代码块内。如果没有触发异常,则代码将照常继续执行。但是如果异常被触发,会抛出一个异常。

    • Throw - 这里规定如何触发异常。每一个 "throw" 必须对应至少一个 "catch"

    • Catch - "catch" 代码块会捕获异常,并创建一个包含异常信息的对象

    抛出异常并捕获掉,可以继续执行后面的代码:

    try {  
        $error = 'Always throw this error';  
        throw new Exception($error);  
    
    
    
    
        // 从这里开始,tra 代码块内的代码将不会被执行  
        echo 'Never executed';  
    
    
    
    
    } catch (Exception $e) {  
        echo 'Caught exception: ',  $e->getMessage(),'<br>';  
    }  
    
    
    
    
    // 继续执行  
    echo 'Hello World';  
    

    顶层异常处理器 set_exception_handler

    在我们实际开发中,异常捕捉仅仅靠 try {} catch () 是远远不够的。set_exception_handler() 函数可设置处理所有未捕获异常的用户定义函数。

    function myException($exception)  
    {  
    echo "<b>Exception:</b> " , $exception->getMessage();  
    }  
    
    
    
    
    set_exception_handler('myException');  
    throw new Exception('Uncaught Exception occurred');
    

    扩展 PHP 内置的异常处理类

    用户可以用自定义的异常处理类来扩展 PHP 内置的异常处理类。以下的代码说明了在内置的异常处理类中,哪些属性和方法在子类中是可访问和可继承的。

    class Exception  
    {  
        protected $message = 'Unknown exception';   // 异常信息  
        protected $code = 0;                        // 用户自定义异常代码  
        protected $file;                            // 发生异常的文件名  
        protected $line;                            // 发生异常的代码行号  
    
    
    
    
        function __construct($message = null, $code = 0);  
    
    
    
    
        final function getMessage();                // 返回异常信息  
        final function getCode();                   // 返回异常代码  
        final function getFile();                   // 返回发生异常的文件名  
        final function getLine();                   // 返回发生异常的代码行号  
        final function getTrace();                  // backtrace() 数组  
        final function getTraceAsString();          // 已格成化成字符串的 getTrace() 信息  
    
    
    
    
        /* 可重载的方法 */  
        function __toString();                       // 可输出的字符串  
    }  
    

    如果使用自定义的类来扩展内置异常处理类,并且要重新定义构造函数的话,建议同时调用 parent::__construct() 来检查所有的变量是否已被赋值。当对象要输出字符串的时候,可以重载 __toString() 并自定义输出的样式。

    class MyException extends Exception  
    {  
        // 重定义构造器使 message 变为必须被指定的属性  
        public function __construct($message, $code = 0) {  
            // 自定义的代码  
    
    
    
    
            // 确保所有变量都被正确赋值  
            parent::__construct($message, $code);  
        }  
    
    
    
    
        // 自定义字符串输出的样式 */  
        public function __toString() {  
            return __CLASS__ . ": [{$this->code}]: {$this->message}\n";  
        }  
    
    
    
    
        public function customFunction() {  
            echo "A Custom function for this type of exception\n";  
        }  
    } 
    

    MyException 类是作为旧的 exception 类的一个扩展来创建的。这样它就继承了旧类的所有属性和方法,我们可以使用 exception 类的方法,比如 getLine() 、 getFile() 以及 getMessage()。

    PHP 错误处理

    PHP 的错误级别

    错误的抛出

    除了系统在运行 php 代码抛出的意外错误。我们还可以利用 rigger_error 产生一个自定义的用户级别的 error/warning/notice 错误信息:

    if ($divisor == 0) {
        trigger_error("Cannot divide by zero", E_USER_ERROR);
    }
    

    顶级错误处理器

    顶级错误处理器 set_error_handler 一般用于捕捉 E_NOTICE 、E_USER_ERROR、E_USER_WARNING、E_USER_NOTICE 级别的错误,不能捕捉 E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR 和 E_COMPILE_WARNING。

    致命错误捕捉处理器 register_shutdown_function

    register_shutdown_function() 函数可实现当程序执行完成后执行的函数,其功能为可实现程序执行完成的后续操作。程序在运行的时候可能存在执行超时,或强制关闭等情况,但这种情况下默认的提示是非常不友好的,如果使用 register_shutdown_function() 函数捕获异常,就能提供更加友好的错误展示方式,同时可以实现一些功能的后续操作,如执行完成后的临时数据清理,包括临时文件等。

    可以这样理解调用条件:

    • 当页面被用户强制停止时

    • 当程序代码运行超时时

    • 当PHP代码执行完成时,代码执行存在异常和错误、警告

    我们前面说过,set_error_handler 能够捕捉的错误类型有限,很多致命错误例如解析错误等都无法捕捉,但是这类致命错误发生时,PHP 会调用 register_shutdown_function 所注册的函数,如果结合函数 error_get_last,就会获取错误发生的信息。

     

    Laravel 异常处理

    laravel 的异常处理由类 \Illuminate\Foundation\Bootstrap\HandleExceptions::class 完成:

    class HandleExceptions
    {
        public function bootstrap(Application $app)
    {
            $this->app = $app;
    
    
    
    
            error_reporting(-1);
    
    
    
    
            set_error_handler([$this, 'handleError']);
    
    
    
    
            set_exception_handler([$this, 'handleException']);
    
    
    
    
            register_shutdown_function([$this, 'handleShutdown']);
    
    
    
    
            if (! $app->environment('testing')) {
                ini_set('display_errors', 'Off');
            }
        }
    }
    

    异常转化

    laravel 的异常处理均由函数 handleException 负责。

    PHP7 实现了一个全局的 throwable 接口,原来的 Exception 和部分 Error 都实现了这个接口, 以接口的方式定义了异常的继承结构。于是,PHP7 中更多的 Error 变为可捕获的 Exception 返回给开发者,如果不进行捕获则为 Error ,如果捕获就变为一个可在程序内处理的 Exception。这些可被捕获的 Error 通常都是不会对程序造成致命伤害的 Error,例如函数不存在。

    PHP7 中,基于 /Error exception,派生了 5 个新的 engine exception:ArithmeticError / AssertionError / DivisionByZeroError / ParseError / TypeError。在 PHP7 里,无论是老的 /Exception 还是新的 /Error ,它们都实现了一个共同的 interface: /Throwable。

    因此,遇到非 Exception 类型的异常,首先就要将其转化为 FatalThrowableError 类型:

    public function handleException($e)
    {
        if (! $e instanceof Exception) {
            $e = new FatalThrowableError($e);
        }
    
    
    
    
        $this->getExceptionHandler()->report($e);
    
    
    
    
        if ($this->app->runningInConsole()) {
            $this->renderForConsole($e);
        } else {
            $this->renderHttpResponse($e);
        }
    }
    

    FatalThrowableError 是 Symfony 继承 \ErrorException 的错误异常类:

    class FatalThrowableError extends FatalErrorException
    {
        public function __construct(\Throwable $e)
    {
            if ($e instanceof \ParseError) {
                $message = 'Parse error: '.$e->getMessage();
                $severity = E_PARSE;
            } elseif ($e instanceof \TypeError) {
                $message = 'Type error: '.$e->getMessage();
                $severity = E_RECOVERABLE_ERROR;
            } else {
                $message = $e->getMessage();
                $severity = E_ERROR;
            }
    
    
    
    
            \ErrorException::__construct(
                $message,
                $e->getCode(),
                $severity,
                $e->getFile(),
                $e->getLine()
            );
    
    
    
    
            $this->setTrace($e->getTrace());
        }
    }
    

    异常 Log

    当遇到异常情况的时候,laravel 首要做的事情就是记录 log,这个就是 report 函数的作用。

    protected function getExceptionHandler()
    {
        return $this->app->make(ExceptionHandler::class);
    }
    

    laravel 在 Ioc 容器中默认的异常处理类是 Illuminate\Foundation\Exceptions\Handler:

    class Handler implements ExceptionHandlerContract
    {
        public function report(Exception $e)
    {
            if ($this->shouldntReport($e)) {
                return;
            }
    
    
    
    
            try {
                $logger = $this->container->make(LoggerInterface::class);
            } catch (Exception $ex) {
                throw $e; // throw the original exception
            }
    
    
    
    
            $logger->error($e);
        }
    
    
    
    
        protected function shouldntReport(Exception $e)
    {
            $dontReport = array_merge($this->dontReport, [HttpResponseException::class]);
    
    
    
    
            return ! is_null(collect($dontReport)->first(function ($type) use ($e) {
                return $e instanceof $type;
            }));
        }
    }
    

    异常页面展示

    记录 log 后,就要将异常转化为页面向开发者展示异常的信息,以便查看问题的来源:

    protected function renderHttpResponse(Exception $e)
    {
        $this->getExceptionHandler()->render($this->app['request'], $e)->send();
    }
    
    
    
    
    class Handler implements ExceptionHandlerContract
    {
        public function render($request, Exception $e)
    {
            $e = $this->prepareException($e);
    
    
    
    
            if ($e instanceof HttpResponseException) {
                return $e->getResponse();
            } elseif ($e instanceof AuthenticationException) {
                return $this->unauthenticated($request, $e);
            } elseif ($e instanceof ValidationException) {
                return $this->convertValidationExceptionToResponse($e, $request);
            }
    
    
    
    
            return $this->prepareResponse($request, $e);
        }
    }
    

    对于不同的异常,laravel 有不同的处理,大致有 HttpException、HttpResponseException、AuthorizationException、ModelNotFoundException、AuthenticationException、ValidationException。由于特定的不同异常带有自身的不同需求,本文不会特别介绍。本文继续介绍最普通的异常 HttpException 的处理:

    protected function prepareResponse($request, Exception $e)
    {
        if ($this->isHttpException($e)) {
            return $this->toIlluminateResponse($this->renderHttpException($e), $e);
        } else {
            return $this->toIlluminateResponse($this->convertExceptionToResponse($e), $e);
        }
    }
    
    
    
    
    protected function renderHttpException(HttpException $e)
    {
        $status = $e->getStatusCode();
    
    
    
    
        view()->replaceNamespace('errors', [
            resource_path('views/errors'),
            __DIR__.'/views',
        ]);
    
    
    
    
        if (view()->exists("errors::{$status}")) {
            return response()->view("errors::{$status}", ['exception' => $e], $status, $e->getHeaders());
        } else {
            return $this->convertExceptionToResponse($e);
        }
    }
    

    对于 HttpException 来说,会根据其错误的状态码,选取不同的错误页面模板,若不存在相关的模板,则会通过 SymfonyResponse 来构造异常展示页面:

    protected function convertExceptionToResponse(Exception $e)
    {
        $e = FlattenException::create($e);
    
    
    
    
        $handler = new SymfonyExceptionHandler(config('app.debug'));
    
    
    
    
        return SymfonyResponse::create($handler->getHtml($e), $e->getStatusCode(), $e->getHeaders());
    }
    
    
    
    
    protected function toIlluminateResponse($response, Exception $e)
    {
        if ($response instanceof SymfonyRedirectResponse) {
            $response = new RedirectResponse($response->getTargetUrl(), $response->getStatusCode(), $response->headers->all());
        } else {
            $response = new Response($response->getContent(), $response->getStatusCode(), $response->headers->all());
        }
    
    
    
    
        return $response->withException($e);
    }
    

    laravel 错误处理

    public function handleError($level, $message, $file = '', $line = 0, $context = [])
    {
        if (error_reporting() & $level) {
            throw new ErrorException($message, 0, $level, $file, $line);
        }
    }
    
    
    
    
    public function handleShutdown()
    {
        if (! is_null($error = error_get_last()) && $this->isFatal($error['type'])) {
            $this->handleException($this->fatalExceptionFromError($error, 0));
        }
    }
    
    
    
    
    protected function fatalExceptionFromError(array $error, $traceOffset = null)
    {
        return new FatalErrorException(
            $error['message'], $error['type'], 0, $error['file'], $error['line'], $traceOffset
        );
    }
    
    
    
    
    protected function isFatal($type)
    {
        return in_array($type, [E_COMPILE_ERROR, E_CORE_ERROR, E_ERROR, E_PARSE]);
    }
    

    对于不致命的错误,例如 notice 级别的错误,handleError 即可截取, laravel 将错误转化为了异常,交给了 handleException 去处理。

    对于致命错误,例如 E_PARSE 解析错误,handleShutdown 将会启动,并且判断当前脚本结束是否是由于致命错误,如果是致命错误,将会将其转化为 FatalErrorException, 交给了 handleException 作为异常去处理。

  • 相关阅读:
    centos查看端口
    APNG面向移动与未来的新动画图片格式揭秘与制作全技巧
    nginx
    maven依赖到底是啥
    docker启动mysql8
    最全!即学即会 Serverless Devs 基础入门(上)
    三大特性,多个场景,Serverless 应用引擎 SAE 全面升级
    最全!即学即会 Serverless Devs 基础入门(下)
    更省更快,如何使用 Serverless 搭建个人专属网盘?
    详解异步任务:函数计算的任务触发去重​
  • 原文地址:https://www.cnblogs.com/lxwphp/p/15452842.html
Copyright © 2020-2023  润新知