Hướng dẫn handle exception laravel

  • October 29, 2022
  • 1600

Hôm nay chúng ta sẽ tìm hiểu handle exception trong laravel, giúp code gọn gàng, dễ dàng debug khi có lỗi phát sinh. Tất cả các exceptions sẽ được bắt và xử lý response trả về trong class App\Exceptions\Handler không cần xử lý try, catch exception trong service, controller...

Error Handling Laravel

Tất cả các exception đều handle được trong class App\Exceptions\Handler


<?php

namespace App\Exceptions;

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

class Handler extends ExceptionHandler
{
    /**
     * A list of exception types with their corresponding custom log levels.
     *
     * @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
     */
    protected $levels = [
        //
    ];

    /**
     * A list of the exception types that are not reported.
     *
     * @var array<int, class-string<\Throwable>>
     */
    protected $dontReport = [
        //
    ];

    /**
     * A 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.
     *
     * @return void
     */
    public function register(): void
    {
        $this->reportable(function (Throwable $e) {
            //
        });
    }

    /**
     * Render an exception into an HTTP response.
     *
     * @param Request $request   is request
     * @param Throwable                $e is exception
     *
     * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\Response|\Symfony\Component\HttpFoundation\Response
     */
    public function render($request, Throwable $e)
    {
        $statusCode = Response::HTTP_BAD_REQUEST;
        if ($e->getCode() >= Response::HTTP_INTERNAL_SERVER_ERROR) {
            $statusCode = $e->getCode();
        }
        
        $message = __('messages.errors.bad_request');
        $errors = null;
        switch (true) {
            case $e instanceof AuthenticationException:
                $message = __('messages.errors.unauthorized');
                $statusCode = Response::HTTP_UNAUTHORIZED;
                break;
               
            case $e instanceof NotFoundHttpException:
                $message = __('messages.errors.page_not_found');
                $statusCode = Response::HTTP_NOT_FOUND;
                break;

            case $e instanceof ModelNotFoundException:
                $message = __('messages.errors.model_not_found');
                $statusCode = Response::HTTP_NOT_FOUND;
                break;
            
            case $e instanceof ApiException:
                $message    = $e->getMessage();
                $statusCode = $e->getCode();
                $errors = $e->getData();
                break;

            default:
                break;
        }

        if ($request->is('*api*')) {
            return $this->makeErrorResponse($statusCode, $message, $errors);
        }
        
        return response($message, Response::HTTP_BAD_REQUEST);
    }

    /**
     * @param int $code
     * @param string $message
     * @param array|null $errors
     * @param mixed|null $data
     * @return Response
     */
    protected function makeErrorResponse(int $code, string $message, ?array $errors = null, $data = null): Response
    {
        $response = [
            'status' => false,
            'message' => $message,
            'data' => $errors
        ];

        if (!empty($data)) {
            $response['data'] = $data;
        }

        return response()->json($response, $code);
    }
}

Tìm hiểu 1 số hàm và thuộc tính quan trọng trong class Handler

1. Exception Log Levels

Khai báo log level cho excecption, ví dụ như chỉ xử lý PDOException với level CRITICAL


use PDOException;
use Psr\Log\LogLevel;
 
/**
 * A list of exception types with their corresponding custom log levels.
 *
 * @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
 */
protected $levels = [
    PDOException::class => LogLevel::CRITICAL,
];

2. Ignoring Exceptions

Bỏ qua exception khai báo trong thuộc tính dontReport


use App\Exceptions\InvalidOrderException;
 
/**
 * A list of the exception types that are not reported.
 *
 * @var array<int, class-string<\Throwable>>
 */
protected $dontReport = [
    InvalidOrderException::class,
];

3. Reporting Exceptions

Hàm register() là nơi đăng kí các exception mà bạn viết


use App\Exceptions\InvalidOrderException;
 
/**
 * Register the exception handling callbacks for the application.
 *
 * @return void
 */
public function register()
{
    // Report exection sang 1 bên thứ 3
    $this->reportable(function (InvalidOrderException $e) {
        //
    });
     
    // Định nghĩa response trả về khi xảy ra lỗi
    $this->renderable(function (NotFoundHttpException $e, $request) {
        if ($request->is('api/*')) {
            return response()->json([
                'message' => 'Record not found.'
            ], 404);
        }
    });
}

Trong bài vết này mình chỉ handle response trả về khi gặp lỗi lên mình sẽ không sử dụng hàm register, mà chỉ cần sử dụng hàm render() để xử lý response trả về cho mọi exception.


/**
     * Render an exception into an HTTP response.
     *
     * @param Request $request   is request
     * @param Throwable                $e is exception
     *
     * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\Response|\Symfony\Component\HttpFoundation\Response
     */
    public function render($request, Throwable $e)
    {
        $statusCode = Response::HTTP_BAD_REQUEST;
        if ($e->getCode() >= Response::HTTP_INTERNAL_SERVER_ERROR) {
            $statusCode = $e->getCode();
        }
        
        $message = __('messages.errors.bad_request');
        $errors = null;
        switch (true) {
            case $e instanceof AuthenticationException:
                $message = __('messages.errors.unauthorized');
                $statusCode = Response::HTTP_UNAUTHORIZED;
                break;
               
            case $e instanceof NotFoundHttpException:
                $message = __('messages.errors.page_not_found');
                $statusCode = Response::HTTP_NOT_FOUND;
                break;

            case $e instanceof ModelNotFoundException:
                $message = __('messages.errors.model_not_found');
                $statusCode = Response::HTTP_NOT_FOUND;
                break;
            
            case $e instanceof ApiException:
                $message    = $e->getMessage();
                $statusCode = $e->getCode();
                $errors = $e->getData();
                break;

            default:
                break;
        }

        if ($request->is('*api*')) {
            return $this->makeErrorResponse($statusCode, $message, $errors);
        }
        
        return response($message, Response::HTTP_BAD_REQUEST);
    }

Create custom exception laravel

Ở phần trên, chúng ta đã xử lý việc bắt và xử lý response trả về khi ứng dụng có exceptions, giờ chúng ta sẽ viết 1 ApiExecption để định nghĩa các exectpions cơ bản của 1 api.


<?php

namespace App\Exceptions;

use RuntimeException;
use Symfony\Component\HttpFoundation\Response;
use Throwable;

class ApiException extends RuntimeException
{
    /** @var mixed|null  */
    private $data;

    /**
     * ApiException constructor.
     *
     * @param int $httpCode
     * @param string $message
     * @param null $data
     * @param Throwable|null $previous
     */
    public function __construct(int $httpCode, string $message, $data = null, Throwable $previous = null)
    {
        $this->data = $data;
        parent::__construct($message, $httpCode, $previous);
    }

    /**
     * @return mixed|null
     */
    public function getData()
    {
        return $this->data;
    }

    /**
     * @param string $message
     * @param null $data
     * @return ApiException
     */
    public static function serviceUnavailable(string $message = 'Service unavailable', $data = null): ApiException
    {
        return new ApiException(Response::HTTP_SERVICE_UNAVAILABLE, $message, $data);
    }

    /**
     * @param string $message
     * @param null $data
     * @return ApiException
     */
    public static function badRequest(string $message = "Bad request", $data = null): ApiException
    {
        return new ApiException(Response::HTTP_BAD_REQUEST, $message, $data);
    }

    /**
     * @param string $message
     * @param null $data
     * @return ApiException
     */
    public static function forbidden(string $message = "forbidden", $data = null): ApiException
    {
        return new ApiException(Response::HTTP_FORBIDDEN, $message, $data);
    }

    /**
     * @param string $message
     * @param null $data
     * @return ApiException
     */
    public static function notFound(string $message = "Not found", $data = null): ApiException
    {
        return new ApiException(Response::HTTP_NOT_FOUND, $message, $data);
    }

    /**
     * @param string $message
     * @param null $data
     * @return ApiException
     */
    public static function conflict(string $message = "Conflict", $data = null): ApiException
    {
        return new ApiException(Response::HTTP_CONFLICT, $message, $data);
    }

    /**
     * @param string $message
     * @param null $data
     * @return ApiException
     */
    public static function validation(string $message = "Request invalid", $data = null): ApiException
    {
        return new ApiException(Response::HTTP_UNPROCESSABLE_ENTITY, $message, $data);
    }
}

Using custom exception laravel

Giờ chúng ta sẽ sử dụng ApiException vừa tạo nhé. Khai báo 1 route liệt kê tất cả các users


use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\V1\UserController;

Route::group(['prefix' => 'v1'], function () {
    Route::get('users', [UserController::class, 'index']);
});

Viết UserController để lấy danh sách các user.


<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Controllers\Controller;
use App\Exceptions\ApiException;

class UserController extends Controller
{
    public function index()
    {
        throw ApiException::badRequest();
    }
}

Chúng ta sẽ thử throw một badRequest() exception từ controller xem class Handler có bắt được exception và trả về đúng response không? chúng ta sẽ dùng postman để check. Thật tuyệt vời, chúng ta đã xử lý được exception.
laravel exception

Tổng Kết

Như vậy là chúng ta đã handle được tất cả các exception trong ứng dụng sử dụng laravel, sử dụng cách này giúp chúng ta không bị lặp code khi thực hiện try, catch từ controller, giúp ứng dụng mạnh mẽ hơn rất nhiều rồi phải không nào? Thanks for reading...