laravel 11 如何更优雅的处理异常

想能兼容web和api两种、web返回常规页面错误,api就返回json格式信息

《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
最佳答案
<?php

use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        //
    })
    ->withExceptions(function (Exceptions $exceptions) {
        if (request()->expectsJson()) {
            $exceptions->render(function (Exception $e) {
                $msg = $e->getMessage().' '.$e->getFile().' '.$e->getLine();
                $state = -2;
                $code = 500;

                switch (true) {
                    case $e instanceof ValidationException:
                        $msg = $e->getMessage();
                        $code = 400;
                        break;
                    case $e instanceof AuthenticationException:
                        $msg = 'Not authenticated (login required)';
                        $state = $code = 401;
                        break;
                    case $e instanceof ModelNotFoundException:
                    case $e instanceof NotFoundHttpException:
                        $msg = '404(NOT FOUND)';
                        $state = $code = 404;
                        break;
                    case $e instanceof MethodNotAllowedHttpException:
                        $msg = '405(Method Not Allowed)';
                        $state = $code = 405;
                        break;
                    case $e instanceof UnauthorizedHttpException:
                        $msg = 'Verification failed';
                        $state = $code = 422;
                        break;
                    case $e instanceof HttpException:
                        $msg = $e->getMessage();
                        switch ($e->getStatusCode()) {
                            case 401:
                                $state = $code = 401;
                                break;
                            default:
                                $code = $e->getStatusCode();
                                break;
                        }
                }

                return responseHelper(false, $msg, $state, $code);
            });
        }
    })->create();
3个月前 评论
Talentisan (作者) (楼主) 3个月前
讨论数量: 39

这就开始投入项目了吗

4个月前 评论
Talentisan (楼主) 4个月前
ShiKi

文档中的错误处理

4个月前 评论
Talentisan (楼主) 4个月前
Mutoulee
4个月前 评论
Talentisan (楼主) 4个月前

基本就是替换默认的异常处理类,判断是api还是web吧。重新定义这个 github.com/deatil/larke-admin/blob...

4个月前 评论
Talentisan (楼主) 4个月前
Talentisan (楼主) 4个月前
  1. 通过 make:exception 命令创建自定义异常

    $ php artisan make:exception InvalidRequestException
  2. 定义 render 方法

    <?php
    namespace App\Exceptions;
    use Exception;
    use Illuminate\Http\Request;
    class InvalidRequestException extends Exception
    {
     // 自定义错误码
     protected $errorCode;
     public function __construct(string $message = "", int $errorCode = 0, int $code = 400)
     {
         $this->errorCode = $errorCode;
         parent::__construct($message, $code);
     }
    
     public function render(Request $request)
     {
         $data = ['message' => $this->message, 'error_code' => $this->errorCode];
         // 返回 json 格式信息
         if ($request->expectsJson()) {
             return response()->json($data, $this->code);
         }
         // 返回错误页面
         return view('pages.error', $data);
     }
    }
4个月前 评论
Talentisan (楼主) 4个月前

自带 ajax 请求识别呢

添加一个请求头就行了

var xhr = new XMLHttpRequest();
xhr.open("GET", "ta", true);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');

xhr.onreadystatechange = function() {
  if (xhr.readyState == 4 && xhr.status == 200) {
    // 请求成功
    var response = JSON.parse(xhr.responseText);
    console.log(response);
  }
};

xhr.send();

file

4个月前 评论
Talentisan (楼主) 4个月前
kis龍 (作者) 4个月前
Talentisan (楼主) 4个月前

错误处理《Laravel 10 中文文档》 此处接管异常 实现方法很多,比如通过 $request->is('api/*') 判断是不是api模块,如果是就自定义错误响应

4个月前 评论
Talentisan (楼主) 4个月前

1、创建中间件 ForceJsonResponse

handle() 方法中添加:

$request->headers->set('Accept', 'application/json');

2、在 bootstrap/app.php 中使用中间件

->withMiddleware(function (Middleware $middleware) {
    $middleware->prependToGroup('api', [
        ForceJsonResponse::class
    ]);
})
4个月前 评论
Talentisan (楼主) 4个月前
sanders

我是在 app/Exceptions/Handler.php 里面定义, $request->wantsJson() 用来获取是否要返回 JSON 格式数据。

<?php

namespace App\Exceptions;

use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use InvalidArgumentException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Throwable;

class Handler extends ExceptionHandler
{
    /**
     * The list of the inputs that are never flashed to the session on validation exceptions.
     *
     * @var array<int, string>
     */
    protected $dontFlash = [
        'current_password',
        'password',
        'password_confirmation',
    ];

    /**
     * Register the exception handling callbacks for the application.
     */
    public function register(): void
    {
        // 数据不存在异常
        $this->renderable(function (ModelNotFoundException $e, ?Request $request = null) {
            if ($request && $request->wantsJson()) {
                $message = __(
                    'messages.model_not_found',
                    [
                        'model' => __('messages.model.'.$e->getModel()),
                    ]
                );

                return response()->json([
                    'code' => $e->getCode(),
                    'message' => $message,
                    'model' => $e->getModel(),
                    'ids' => $e->getIds(),
                ], 404);
            }

            return response()->view('errors.404', [], 404);
        });
// ...
4个月前 评论
Talentisan (楼主) 4个月前

file

if ($request->is('api/*')) {

}
4个月前 评论
Talentisan (楼主) 3个月前
WHOAMI_ (作者) 3个月前

封装个异常处理类,这样就不用频繁修改app.php了,例如

<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
    )
    ->withMiddleware(function (Middleware $middleware) {
        //
    })
    ->withExceptions(function (Exceptions $exceptions) {
        (new \App\Exceptions\Handler($exceptions))->handle();
    })->create();

然后在Hanler类里面处理,可以参考下:

<?php

namespace App\Exceptions;

use App\Helpers\ApiHelper;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Contracts\Http\Kernel as HttpKernel;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Traits\ForwardsCalls;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Throwable;

class Handler
{
    use ForwardsCalls;

    protected $dontReport = [
        XXXX::class,
    ];

    public function __construct(public Exceptions $exceptions) {}

    public function handle() {

        // 不需要报告的异常
        $this->dontReport($this->dontReport);

        // 异常附带context
        $this->context(fn (Throwable $e, array $context) => $this->withContext($e, $context));

        // 是否需要响应Json
        $this->shouldRenderJsonWhen(fn(Request $request, Throwable $e) => $this->shouldReturnJson($request, $e));

        // 处理异常响应
        $this->respond(function(Response $response, Throwable $e, Request $request) {
            if ( !$this->shouldReturnJson($request, $e) ) {
                return $response;
            }

            $data = ['xxxxxx'];

            $apiResponse = match(true) {
                $e instanceof AuthenticationException => ApiHelper::failed(message: $e->getMessage(), code: 401, extends: $data),
                $e instanceof HttpExceptionInterface => ApiHelper::failed(message: $e->getMessage(), code: $e->getStatusCode(), extends: $data),
                $e instanceof ValidationException => ApiHelper::validation(messages: $e->errors(), message: $e->getMessage(), extends: $data),
                // APP定义异常
                $e instanceof AppException => ApiHelper::failed($e->getMessage(), code: $e->getCode(), extends: $data),
                default => ApiHelper::error(message: 'Server Error', e: $e, code: 500),
            };

            return $apiResponse;
        });
    }

    protected function shouldReturnJson(Request $request, Throwable $e)
    {
        if ($request->is('api/*')) {
            return true;
        }
        return $request->expectsJson();
    }

    /**
     * 附带日志信息
     * @param  Throwable $e       异常
     * @param  array     $context 异常附带的信息
     * @return [type]             [description]
     */
    protected function withContext(Throwable $e, array $context): array
    {
        $request = app('request');

        try {
            return !App::runningInConsole() || isset($_SERVER['LARAVEL_OCTANE']) ? [
                'userId' => Auth::check() ? Auth::id() : null,
                'ip' => $request->ip(),
                'requestId' => $request->attributes->get('request_id'),
                'requestUrl' => $request->fullUrl(),
                'requestMethod' => $request->method(),
            ] : [];
        } catch (\Throwable $e) {
            return [];
        }
    }

    public function __call(string $method, array $parameters)
    {
        return $this->forwardCallTo($this->exceptions, $method, $parameters);
    }
}
4个月前 评论
Talentisan (楼主) 4个月前

我这安装11报错了,是要更新compsoer的源么

composer create-project laravel/laravel:^11.0 lara11

Creating a "laravel/laravel:^11.0" project at "./lara11" Installing laravel/laravel (v11.0.3)

  • Installing laravel/laravel (v11.0.3): Extracting archive Created project in /var/www/laravels/lara11

    @php -r "file_exists('.env') || copy('.env.example', '.env');" Loading composer repositories with package information Updating dependencies Your requirements could not be resolved to an installable set of packages.

    Problem 1

    • laravel/framework[v11.0.0, ..., v11.0.8] require fruitcake/php-cors ^1.3 -> found fruitcake/php-cors[dev-feat-setOptions, dev-master, dev-main, dev-test-8.2, v0.1.0, v0.1.1, v0.1.2, v1.0-alpha1, ..., 1.2.x-dev (alias of dev-master)] but it does not match the constraint.
    • Root composer.json requires laravel/framework ^11.0 -> satisfiable by laravel/framework[v11.0.0, ..., v11.0.8].
4个月前 评论
ononl 4个月前
Talentisan (楼主) 4个月前
aliongkk (作者) 4个月前
aliongkk (作者) 4个月前

请求头不是有类似 accept: application/json 这种么?Laravel 会自动判断响应格式呀? 另,为什么要把异常丢给浏览器?浏览器该接收的应该是数据不是异常呀。

4个月前 评论
<?php

use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        //
    })
    ->withExceptions(function (Exceptions $exceptions) {
        if (request()->expectsJson()) {
            $exceptions->render(function (Exception $e) {
                $msg = $e->getMessage().' '.$e->getFile().' '.$e->getLine();
                $state = -2;
                $code = 500;

                switch (true) {
                    case $e instanceof ValidationException:
                        $msg = $e->getMessage();
                        $code = 400;
                        break;
                    case $e instanceof AuthenticationException:
                        $msg = 'Not authenticated (login required)';
                        $state = $code = 401;
                        break;
                    case $e instanceof ModelNotFoundException:
                    case $e instanceof NotFoundHttpException:
                        $msg = '404(NOT FOUND)';
                        $state = $code = 404;
                        break;
                    case $e instanceof MethodNotAllowedHttpException:
                        $msg = '405(Method Not Allowed)';
                        $state = $code = 405;
                        break;
                    case $e instanceof UnauthorizedHttpException:
                        $msg = 'Verification failed';
                        $state = $code = 422;
                        break;
                    case $e instanceof HttpException:
                        $msg = $e->getMessage();
                        switch ($e->getStatusCode()) {
                            case 401:
                                $state = $code = 401;
                                break;
                            default:
                                $code = $e->getStatusCode();
                                break;
                        }
                }

                return responseHelper(false, $msg, $state, $code);
            });
        }
    })->create();
3个月前 评论
Talentisan (作者) (楼主) 3个月前

我是这么写的,没有使用系统自定义的422状态,而是全部返回200状态码。

<?php

use Illuminate\Support\Arr;
use Illuminate\Foundation\Application;
use Illuminate\Validation\ValidationException;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->validateCsrfTokens([
            '*'
        ]);
    })
    ->withExceptions(function (Exceptions $exceptions) {
      // 返回自定义错误
        $exceptions->renderable(function (ValidationException $exception) {
            return response_error([], Arr::first($exception->errors())[0]);
        });
    })->create();
1个月前 评论

主要还是看你对api的定义,比如get请求就是web页面。非get就是api 那就简单了,直接验证类型返回就行,如果是混合的只能把get请求做请求方式验证,如果ajax或其它特别的定义则响应为api的json,否则就认为是web 你想要的主要目的就是自动识别是 web与api 至于响应处理与规范那都是简单的
验证是否json响应laravel自带有在集合你有啥自定义的规则
官方的参考json响应验证:

Laravel

1个月前 评论

Laravel 11x的将中间件和异常放在了bootstrap/app.php下,这使得更加方便,而不必像以前一样要到kernel和providers文件里搞东搞西,关于全局异常处理其实跟以前有点类似,下面是bootstrap/app.php:

<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Support\Facades\Route;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
        then: function () {
            Route::middleware('admin')
                ->prefix(config('admin.admin_base_path','admin'))
                ->group(base_path('routes/admin.php'));
        }
    )
    ->withMiddleware(function (Middleware $middleware) {
        //中间件
        $middleware->appendToGroup('admin', [
            \Illuminate\Session\Middleware\StartSession::class,
            \Illuminate\Cookie\Middleware\EncryptCookies::class,
            \Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class,
            \App\Http\Middleware\PermissionAuthMiddleware::class,
        ]);
    })
    ->withExceptions(function (Exceptions $exceptions) {
        //全局异常统一处理
        (new \App\Exceptions\Handler($exceptions))->handle();

    })->create();

app/Exceptions/Handler.php代码:

<?php

namespace App\Exceptions;


use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Throwable;

class Handler
{

    /**
     * A list of the exception types that are not reported.
     *不做日志记录的异常错误
     * @var array
     */
    protected $dontReport = [
        ValidationException::class,
        ApiException::class
    ];

    /**
     * A list of the inputs that are never flashed for validation exceptions.
     *认证异常时不被flashed的数据
     * @var array
     */
    protected $dontFlash = [
        'current_password',
        'password',
        'password_confirmation',
    ];

    public function __construct(public Exceptions $excepHandler)
    {
    }

    public function handle()
    {
        $this->excepHandler->dontReport($this->dontReport);
        $this->excepHandler->dontFlash($this->dontFlash);

        // 异常处理
        $this->excepHandler->render(function (Throwable $e) {
            //验证器异常统一处理
            $msg = '';
            $statusCode = 200;

            switch (true) {
                case $e instanceof ValidationException: //验证器类型异常
                    $msg = array_values($e->errors())[0][0];
                    break;
                case $e instanceof HttpException:
                    // 给客户端返回自定义的错误信息,同时将具体错误记录日志
                    // 具体报错信息开发人员可到laravel.log中查看
                    Log::error($e->getMessage());
                    $statusCode = $e->getStatusCode() ? (string)$e->getStatusCode() : '0';
                    $msg = Status::getReasonPhrase($statusCode);
                    break;
            }

            //api返回json,web返回页面
            if (request()->ajax()) {
                $return = [
                    'msg' => $msg
                ];
                return response()->json($return,$statusCode);
            }else{
                return response()->view('errors.error',['errCode'=>$statusCode,'msg'=>$msg]);
            }
        });
    }
}
3周前 评论

接上面的内容, 由于HttpException返回的信息都是英文,我做了一个http状态码对应信息的对应文件app/Exceptions/Status.php

<?php


namespace App\Exceptions;


class Status
{
    // Informational 1xx
    const CODE_CONTINUE = 100;
    const CODE_SWITCHING_PROTOCOLS = 101;
    // Success 2xx
    const CODE_OK = 200;
    const CODE_CREATED = 210;
    const CODE_ACCEPTED = 202;
    const CODE_NON_AUTHORITATIVE_INFORMATION = 203;
    const CODE_NO_CONTENT = 204;
    const CODE_RESET_CONTENT = 205;
    const CODE_PARTIAL_CONTENT = 206;
    // Redirection 3xx
    const CODE_MULTIPLE_CHOICES = 300;
    const CODE_MOVED_PERMANENTLY = 301;
    const CODE_MOVED_TEMPORARILY = 302;
    const CODE_SEE_OTHER = 303;
    const CODE_NOT_MODIFIED = 304;
    const CODE_USE_PROXY = 305;
    const CODE_TEMPORARY_REDIRECT = 307;
    // Client Error 4xx
    const CODE_BAD_REQUEST = 400;
    const CODE_UNAUTHORIZED = 401;
    const CODE_PAYMENT_REQUIRED = 402;
    const CODE_FORBIDDEN = 403;
    const CODE_NOT_FOUND = 404;
    const CODE_METHOD_NOT_ALLOWED = 405;
    const CODE_NOT_ACCEPTABLE = 406;
    const CODE_PROXY_AUTHENTICATION_REQUIRED = 407;
    const CODE_REQUEST_TIMEOUT = 408;
    const CODE_CONFLICT = 409;
    const CODE_GONE = 410;
    const CODE_LENGTH_REQUIRED = 411;
    const CODE_PRECONDITION_FAILED = 412;
    const CODE_REQUIRED_ENTITY_TOO_LARGE = 413;
    const CODE_REQUEST_URI_TOO_LONG = 414;
    const CODE_UNSUPPORTED_MEDIA_TYPE = 415;
    const CODE_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
    const CODE_EXPECTATION_FAILED = 415;
    const CODE_UNPROCESSABLE_ENTITY = 422;
    // Server Error 5xx
    const CODE_INTERNAL_SERVER_ERROR = 500;
    const CODE_NOT_IMPLEMENTED = 501;
    const CODE_BAD_GATEWAY = 502;
    const CODE_SERVICE_UNAVAILABLE = 503;
    const CODE_GATEWAY_TIMEOUT = 504;
    const CODE_HTTP_VERSION_NOT_SUPPORTED = 505;
    const CODE_BANDWIDTH_LIMIT_EXCEEDED = 509;

    private static $phrases = [
        '0' => '未知错误',
        '100' => '继续请求',
        '101' => '切换协议',
        '102' => '处理中',
        '200' => '请求成功',
        '201' => '已创建',
        '202' => '已接受',
        '203' => '非权威信息',
        '204' => '无内容',
        '205' => '重置内容',
        '206' => '部分内容',
        '207' => '多状态',
        '208' => '已上报',
        '226' => 'IM已使用',
        '300' => '多种选择',
        '301' => '已永久移动',
        '302' => '临时移动',
        '303' => '见其他',
        '304' => '未修改',
        '305' => '使用代理',
        '307' => '临时重定向',
        '308' => '永久重定向',
        '400' => '请求错误',
        '401' => '未授权',
        '402' => '需要付款',
        '403' => '禁止',
        '404' => 'URL地址错误',
        '405' => '请求方法不允许',
        '406' => '无法接受',
        '407' => '需要代理验证',
        '408' => '请求超时',
        '409' => '冲突',
        '410' => '不可用',
        '411' => '长度要求',
        '412' => '前提条件未满足',
        '413' => '请求实体过大',
        '414' => 'URI太长了',
        '415' => '不支持的媒体类型',
        '416' => '请求范围不符合',
        '417' => '期望不满足',
        '418' => '我是一个茶壶',
        '419' => '认证已过期',
        '421' => '错误的请求',
        '422' => '不可处理的实体',
        '423' => '锁定',
        '424' => '失败的依赖',
        '425' => '太早了',
        '426' => '需要升级',
        '428' => '前提要求',
        '429' => '请求太多',
        '431' => '请求标头字段太大',
        '444' => '连接关闭无响应',
        '449' => '重试',
        '451' => '法律原因不可用',
        '499' => '客户端关闭请求',
        '500' => '服务器内部错误',
        '501' => '未实现',
        '502' => '网关错误',
        '503' => '服务不可用',
        '504' => '网关超时',
        '505' => 'HTTP版本不支持',
        '506' => '变体协商',
        '507' => '存储空间不足',
        '508' => '检测到环路',
        '509' => '超出带宽限制',
        '510' => '未延期',
        '511' => '需要网络验证',
        '520' => '未知错误',
        '521' => 'Web服务器已关闭',
        '522' => '连接超时',
        '523' => '原点无法到达',
        '524' => '发生超时',
        '525' => 'SSL握手失败',
        '526' => '无效的SSL证书',
        '527' => '轨道炮错误',
        '598' => '网络读取超时',
        '599' => '网络连接超时',
        'unknownError' => '未知错误',
    ];

    static function getReasonPhrase($statusCode): string
    {
        if (isset(self::$phrases[$statusCode])) {
            return self::$phrases[$statusCode];
        } else {
            return '未知错误';
        }
    }

}
3周前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!