Laravel异常: 如何捕获、处理和创建自己的异常

2,552 阅读3分钟
原文链接: phpcasts.org

很多时候,web开发人员并不关心错误。 如果出现问题,你经常会看到默认的Laravel默认提示,例如 Whoops, something went wrong,或者更糟糕的是,异常代码,这对访问者根本没有任何帮助。 所以我决定写一篇关于如何以优雅的方式处理错误并向访问者提供适当的错误信息的分步文章。

提示:本文还将展示使用依赖注入创建自己的服务以及处理服务抛出的异常的示例。

准备:用户搜索任务

所以我们有一个非常简单的例子 - 通过他们的ID来查找用户的表单。

search-user-view-ui

新建两个路由

Route::get('/users', 'UserController@index')->name('users.index');
Route::post('/users/search', 'UserController@search')->name('users.search');

在控制器中新建两个方法


class UserController extends Controller
{

    public function index()
    {
        return view('users.index');
    }

    public function search(Request $request)
    {
        $user = User::find($request->input('user_id'));
        return view('users.search',  compact('user'));
    }

}

最后来完善下模板文件

// resources/views/users/index.blade.php

<form action="{{ route('users.search') }}" method="POST">
    @csrf

    <div class="form-group">
        <input name="user_id" type="text" id="user_id" placeholder="User ID" 
            class="form-control" value="{{ old('user_id') }}">
    </div>

    <input type="submit" class="btn btn-info" value="Search">
</form>

如果我们搜索现有用户并找到它,我们会看到以下结果:

search-user-normal-result

搜索文件模板

// resources/views/users/search.blade.php:

<h3 class="page-title text-center">User found: {{ $user->name }}</h3>

<b>Email</b>: {{ $user->email }}
<br />
<b>Registered on</b>: {{ $user->created_at }}

以上,这是我们理想的场景。但是如果找不到用户呢?

异常处理

让我们走出理想的世界。我们不检查用户是否存在,我们只是在Controller中执行此操作:

$user = User::find($request->input('user_id'));

如果用户没有找到,我们会看到:

search-user-exception-view

当然,我们可以用 APP_DEBUG = false 设置 .env 文件,然后浏览器只显示空白的 Whoops, looks like something went wrong.。但是这仍然没有给我们的访问者提供任何有价值的信息。

我们可以做的另一个快速解决方法是使用 User::findOrFail() 而不是 find() - 然后如果找不到用户,Laravel将显示带有文本的404页面 Sorry, the page you are looking for could not be found.。 但是,这是整个项目的默认404页面,因此对用户不是很有帮助,不是吗?

所以我们需要捕捉错误,处理它们,然后用实际可以理解的错误信息重定向到表单。

我们需要知道它将返回的异常类型和类名。在 findOrFail() 的情况下,它会抛出Eloquent异常ModelNotFoundException,所以我们需要这样做:

public function search(Request $request)
{
    try {
        $user = User::findOrFail($request->input('user_id'));
    } catch (ModelNotFoundException $exception) {
        return back()->withError($exception->getMessage())->withInput();
    }
    return view('users.search', compact('user'));
}

现在,让我们在Blade中实际显示一个错误:


<h3 class="page-title text-center">Search for user by ID</h3>

@if (session('error'))
    <div class="alert alert-danger">{{ session('error') }}</div>
@endif

<form action="{{ route('users.search') }}" method="POST">
...

结果

search-user-result1

太棒了,我们会显示错误信息!但它仍然不理想,对吧?而不是 $exception->getMessage(),我们需要显示我们自己的消息:


return back()->withError('User not found by ID ' . $request->input('user_id'))->withInput();

最终效果:

search-user-result1-with-nice-tips

将错误消息处理转移到服务Service中

现在,我们已经在控制器中采用了一个非常简单的一个动作示例 - 只需找到用户即可。 在实际的应用程序中,它变得更加复杂,通常控制器正在调用某种外部服务或打包方法,这些服务或打包方法可能会因各种错误而失败。

让我们创建我们自己的service,它本质上会做同样的事情,但会抛出异常,所以控制器甚至不需要知道消息文本。

让我们将我们的逻辑移动到 app/Services/UserService.php


namespace App\Services;

use App\User;
use Illuminate\Database\Eloquent\ModelNotFoundException;

class UserService
{

    public function search($user_id)
    {
        $user = User::find($user_id);
        if (!$user) {
            throw new ModelNotFoundException('User not found by ID ' . $user_id);
        }
        return $user;
    }

}

在Controller中,我们需要调用这个服务。首先,我们将它注入到 __construct() 方法中:


use App\Services\UserService;

class UserController extends Controller
{

    private $userService;

    public function __construct(UserService $userService)
    {
        $this->userService = $userService;
    }

// ...

如果你不熟悉依赖注入(DI)以及Laravel IOC容器的工作原理,请参阅官方文档或有关它的好文章。

现在,下面是我们的 search() 方法是这样子的:


public function search(Request $request)
{
    try {
        $user = $this->userService->search($request->input('user_id'));
    } catch (ModelNotFoundException $exception) {
        return back()->withError($exception->getMessage())->withInput();
    }
    return view('users.search', compact('user'));
}

请注意,我们可以再次使用 $exception->getMessage(),并且所有错误验证或消息逻辑都在service中发生 - 这是分离这些操作的目的之一,控制器不应执行它。

更进一步:创建我们自己的异常类

本文的最后部分 - 当service引发与特定错误有关的异常时,甚至更好的体系结构,并且根据错误可能会有多个异常类。

那么我们如何创建自己的异常类呢?很简单,用Artisan命令:

php artisan make:exception UserNotFoundException

以下是它会在 app/Exceptions/UserNotFoundException.php 中生成的内容:


namespace App\Exceptions;

use Exception;

class UserNotFoundException extends Exception
{
    //
}

这里什么也没有,对吧?让我们用一些逻辑来填充我们的异常。 这个类可以有两种方法:

  • report() 如果你想要做一些额外的日志记录,则使用report(), 比如将错误发送到电子邮件,Slack等。
  • render() 如果你想直接从Exception类重定向错误或返回HTTP响应(如自己的 Blade 文件),则使用render()

所以,在这个例子中,我们填充了 render() 方法:


namespace App\Exceptions;

use Exception;

class UserNotFoundException extends Exception
{
    /**
     * Report or log an exception.
     *
     * @return void
     */
    public function report()
    {
        \Log::debug('User not found');
    }
}

最后,这就是我们如何从控制器中调用这个异常:


public function search(Request $request)
{
    try {
        $user = $this->userService->search($request->input('user_id'));
    } catch (UserNotFoundException $exception) {
        report($exception);
        return back()->withError($exception->getMessage())->withInput();
    }
    return view('users.search', compact('user'));
}

所以,这就是我想向你展示异常处理和使用service的一个方面。

当然这个例子是非常简单的,其他人可以用不同的方式使用Exceptions ,但我希望这篇文章能概述一般的异常情况,并说明为什么你应该使用它们,向访问者以优雅的方式显示错误。

关于异常和错误处理的更多信息,请查看 Laravel官方文档