Quản lý lỗi và ghi log trong ứng dụng Laravel

Trong ngôn ngữ PHP, Exception là một class được xây dựng sẵn để quản lý lỗi kiểu hướng đối tượng, nó cho phép bạn đưa ra các cảnh báo khi một vấn đề xấu xảy ra hoặc người dùng hoạt động theo những cách thức không mong muốn (ví dụ như phép chia cho 0 chẳng hạn). Không có exception, ứng dụng của bạn có thể dừng lại khi gặp lỗi và cũng rất khó để kiểm tra lỗi.

1. Exception là gì?

Exceptions signal something outside the expected bounds of behavior of the code in question.

Exception (Ngoại lệ) báo hiệu một điều gì đó nghi ngờ về hành vi của chương trình vượt qua phạm vi mong đợi.

Martin Fowler

Đây là một định nghĩa hoàn chỉnh nhất cho Exception là gì được đưa ra bởi Martin Fowler, cũng chính là tác giả của một loạt bài viết rất hay về nguyên lý Inversion of Control.

Trong thực tế một exception là một sự kiện, nó xảy ra khi chương trình thực thi và làm gián đoạn luồng thực hiện. Khi bạn tạo ra một exception, hệ thống sẽ bắt exception này và tìm cách xử lý phù hợp cùng với các message tương ứng.

try { 
    // Các đoạn mã có thể phát sinh lỗi
} catch (Exception $e) { 
    // Nếu một exception xảy ra, đoạn mã trong catch sẽ được thực hiện
}

Gợi ý: Bạn nên tham khảo bài viết Quản lý lỗi với Exception trong PHP để có kiến thức cơ bản về Exception.

1.1 Exception nên sử dụng trong những trường hợp nào?

Sử dụng exception khi hệ thống phải đối mặt với những tình huống đặc biệt ngăn cản hệ thống giành quyền điều khiển hay nói một cách khác exception sử dụng khi hệ thống không thể xác định điều gì đã xảy ra. Nếu một lỗi là một hành vi mong đợi thì chúng ta không nên sử dụng exception, ví dụ trường hợp chúng ta cần kiểm tra dữ liệu nhập vào. Bắt exception với câu lệch catch là rất quan trọng vì nó nếu không thực hiện nó hệ thống sẽ trả về lỗi và ứng dụng dừng lại. Câu lệnh catch sẽ giúp chương trình quản lý được lỗi và vẫn hoạt động khi lỗi xảy ra.

Chú ý: Exception không nên sử dụng trong thao tác với các toán tử logic.

2. Laravel Exception là gì?

Trong framework Laravel Exception vẫn được sử dụng như class Exception thông thường của ngôn ngữ PHP và thêm vào đó, Laravel cho phép quản lý tập trung exception với class app\Exceptions\Handler. Với class này Laravel cho phép chúng ta có thể quản lý lỗi thông qua các dịch vụ bên thứ 3 và cũng cho phép điều hướng một cách phù hợp đến các trang báo lỗi.

Đi kèm với các các exception, Laravel còn sử dụng thư viện Monolog để giúp ghi lại nhật ký lỗi với rất nhiều các thiết lập cấu hình tùy chọn như sử dụng file riêng lẻ hay file log theo ngày hoặc thậm chí ghi nhận vào system log. Chúng ta sẽ cùng tìm hiểu cách thiết lập cấu hình cho quản lý lỗi và ghi log trong Laravel, tiếp đó là phần ví dụ về xây dựng một class Exception riêng.

2.1 Cấu hình

2.1.1 Bật tắt chế độ ghi log lỗi

Tùy chọn debug trong config/app.php xác định thông tin lỗi có được hiển thị cho người dùng hay không. Mặc định, tùy chọn này lấy giá trị biến môi trường APP_DEBUG trong file .env. Với phát triển cục bộ, bạn nên thiết lập biến môi trường APP_DEBUG là true, khi đưa mã nguồn lên máy chủ nên để giá trị này là false.

2.1.2 Thiết lập lưu trữ log

Laravel hỗ trợ ghi thông tin log vào các file đơn lẻ, file log theo ngày hoặc syslog và errorlog. Để cấu hình cơ chế Laravel sử dụng để lưu trữ log, bạn thay đổi tùy chọn log trong config/app.php. Ví dụ, nếu bạn muốn sử dụng cách ghi log ra file theo ngày thay cho một file riêng lẻ thì bạn thiết lập giá trị log trong app.php thành daily:

'log' => 'daily'

Khi sử dụng chế độ daily, Laravel chỉ giữ lại 5 file log của 5 ngày gần nhất, nếu bạn muốn điều chỉnh số lượng file log được lưu trữ lại, bạn có thể thay đổi giá trị log_max_files trong app.php:

'log_max_files' => 30

2.1.3 Thiết lập cấp độ lỗi cần ghi log

Khi sử dụng Monolog, các message log có rất nhiều cấp độ với độ nghiêm trọng khác nhau. Mặc định, Laravel ghi tất cả các cấp độ log, tuy nhiên, trong môi trường ứng dụng chạy thực tế, bạn nên thiết lập chỉ ghi log với các cấp độ nghiêm trọng cần thiết thông qua giá trị log_level trong file app.php.
Khi tùy chọn này được cấu hình, Laravel sẽ ghi log tất cả các cấp độ nghiêm trọng hơn hoặc bằng với cấp độ được cấu hình. Ví dụ, mặc định giá trị log_level có giá trị là error do đó Laravel sẽ ghi log các cấp độ là error, critical, alert và emergency:

'log_level' => env('APP_LOG_LEVEL', 'error'),

2.1.4 Thiết lập cấu hình Monolog riêng

Nếu bạn muốn hoàn toàn kiểm soát cách Monolog được cấu hình trong ứng dụng của bạn, bạn có thể sử dụng phương thức configureMonologUsing và thực hiện gọi đến phương thức này trong file bootstrap/app.php trước khi trả về biến $app:

$app->configureMonologUsing(function ($monolog) {
    $monolog->pushHandler(...);
});

return $app;

2.2 Quản lý Exception

2.2.1 Phương thức report

Tất cả các exception được quản lý bởi class app\Exceptions\Handler, class này chứa hai phương thức là report và render. Phương thức report được sử dụng để log exception và gửi chúng đến các dịch vụ ngoài như Bugsnag hoặc Sentry. Mặc định, phương thức report chỉ đơn giản truyền exception đến class nơi exception xảy ra. Ví dụ, nếu bạn cần báo cáo các dạng khác nhau của exception theo cách khác, bạn có thể sử dụng toán tử so sánh của PHP là instanceof:

/**
 * Report or log an exception.
 *
 * This is a great spot to send exceptions to Sentry, Bugsnag, etc.
 *
 * @param  \Exception  $exception
 * @return void
 */
public function report(Exception $exception)
{
    if ($exception instanceof CustomException) {
        //
    }

    return parent::report($exception);
}

Đôi khi chúng ta muốn bỏ qua một số exception, thuộc tính $dontReport của class app\Exceptions\Handler chứa một mảng các dạng exeception không muốn log. Ví dụ, các exception cho kết quả lỗi 404 không muốn ghi xuống file log, bạn có thể đưa các exception này vào thuộc tính $dontReport:

/**
 * A list of the exception types that should not be reported.
 *
 * @var array
 */
protected $dontReport = [
    \Illuminate\Auth\AuthenticationException::class,
    \Illuminate\Auth\Access\AuthorizationException::class,
    \Symfony\Component\HttpKernel\Exception\HttpException::class,
    \Illuminate\Database\Eloquent\ModelNotFoundException::class,
    \Illuminate\Validation\ValidationException::class,
];

2.2.2 Phương thức render

Phương thức này được sử dụng cho mục đích điều hướng, với mỗi exception cụ thể bạn muốn điều hướng người dùng đến một trang HTTP. Mặc định, exception được truyền đến base class nơi sinh ra response, tuy nhiên bạn hoàn toàn có thể kiểm tra dạng exception và trả về một response tùy ý:

/**
 * Render an exception into an HTTP response.
 *
 * @param  \Illuminate\Http\Request  $request
 * @param  \Exception  $exception
 * @return \Illuminate\Http\Response
 */
public function render($request, Exception $exception)
{
    if ($exception instanceof CustomException) {
        return response()->view('errors.custom', [], 500);
    }

    return parent::render($request, $exception);
}

2.3 HTTP Exception

Một số các exception tương ứng với một mã lỗi HTTP, ví dụ như lỗi “Trang không tìm thấy” tương ứng với mã lỗi 404 hay lỗi “Không được cấp quyền truy nhập” tương ứng với 401… Trong ứng dụng, phương thức abort giúp bạn tạo ra các exception này:

abort(404);

Helper abort sẽ ngay lập tức bung ra một exception, phương thức này cũng nhận một chuỗi text để hiển thị cho mã lỗi tương ứng.

abort(403, 'Unauthorized action.');

2.3.1 Tạo ra view riêng cho mã lỗi HTTP

Laravel cho phép tạo ra các trang thông báo lỗi riêng tương ứng với các mã lỗi. Ví dụ, nếu bạn muốn cá nhân hóa trang báo lỗi 404 “Không tìm thấy trang web” bạn có thể tạo view resources/views/errors/404.blade.php. View này sẽ được sử dụng khi lỗi 404 phát sinh trong ứng dụng. Các view trong thư mục này cần được đặt tên tương ứng với mã lỗi HTTP. Một instance của HttpException sẽ được truyền đến view trong biến $exception khi hàm abort được gọi đến.

<h2>{{ $exception->getMessage() }}</h2>

2.4 Ghi log

Laravel có một abstraction layer bao phủ thư viện Monolog, mặc định một file log được lưu trong thư mục storage/logs. Bạn hoàn toàn có thể ghi log vào đây sử dụng facade Log:

<?php

namespace App\Http\Controllers;

use App\User;
use Illuminate\Support\Facades\Log;
use App\Http\Controllers\Controller;

class UserController extends Controller
{
    /**
     * Show the profile for the given user.
     *
     * @param  int  $id
     * @return Response
     */
    public function showProfile($id)
    {
        Log::info('Showing user profile for user: '.$id);

        return view('user.profile', ['user' => User::findOrFail($id)]);
    }
}

Các nhật ký lỗi được ghi nhận với 8 cấp độ khác nhau theo tiêu chuẩn RFC 5424: emergency, alert, critical, error, warning, notice, info và debug.

Log::emergency($message);
Log::alert($message);
Log::critical($message);
Log::error($message);
Log::warning($message);
Log::notice($message);
Log::info($message);
Log::debug($message);

Trong quá trình ghi log, bạn muốn ghi thêm các thông tin khác để tăng cường thông tin ngữ cảnh của lỗi, các phương thức của facade Log ở trên đều có tham số thứ hai là một mảng thông tin. Ví dụ:

Log::info('Create product fail.', ['id' => $product->id, 'user_id' => $user->id]);
// [2017-08-29 09:49:20] local.INFO: Create product fail. {"id":1, "user_id":2}

Facade Log là một lớp trìu tượng bao phủ Monolog, đôi khi bạn muốn đối tượng từ Monolog, sử dụng phương thức getMonolog():

$monolog = Log::getMonolog();

3. Ví dụ về Laravel Eloquent Exception

Trong ví dụ này chúng ta sẽ tạo ra một lỗi Laravel sẵn có bằng cách cố tình tạo ra một Exception với phương thức firstOrFail() của Eloquent Model. Trong một project Laravel sẵn có mở file routes\web.php và thêm vào (nếu bạn chưa có môi trường thực hành Laravel tham khảo Cài đặt nhanh môi trường Laravel với Laragon):

Route::get('/user/{id}', function($id) {
    $user = User::findOrFail($id);
    return $user->toArray();
});

Khi thực hiện route này trên trình duyệt một exception ModelNotFoundException sẽ được sinh ra.

ModelNotFoundException

Bạn có thể thêm vào đoạn mã quản lý exception để trả về một thông báo lỗi riêng bằng cách thêm code vào phương thức render trong app\Exceptions\Handler:

public function render($request, Exception $e)
{
    // Kiểm tra nếu exception là một instance của class ModelNotFoundException
    if ($e instanceof ModelNotFoundException) {
        if ($request->ajax()) {
            // Nếu request ở dạng ajax trả về lỗi 404 với thông báo dạng Json
            return response()->json(['error' => 'Không tìm thấy user'], 404);
        } else {
            // Request thông thường trả về view 404
            return response()->view('errors.404_user_not_found', [], 404);
        }
    }
    return parent::render($request, $e);
}

Chúng ta thêm một view với tên 404_user_not_found.blade.php vào trong thư mục resources\views\errors. Sở dĩ tạo ra view riêng vì chúng ta muốn trang hiển thị thông báo không tìm thấy user khác với trang “Không tìm thấy trang web” 404.blade.php.

<!DOCTYPE html>
<html>
<head>
    <title>Không tìm thấy user - Allaravel.com</title>
</head>
<body>
    <p>Không tìm thấy user nào có id tương ứng.</p>
</body>
</html>

Khi truy nhập vào route user\9999 chúng ta sẽ thấy thông báo lỗi như mong muốn:

View cho exception ModelNotFoundException

Qua ví dụ về Eloquent Exception chúng ta đã biết cách quản lý Exception trong Laravel và điều hướng đến thông báo lỗi phù hợp nâng cao trải nghiệm người dùng. Trong phương thức render ở trên chúng ta không đề cập đến NotFoundHttpException là exception phát sinh khi một trang không có trong routes\web.php. Thêm vào phương thức render phần code quản lý NotFoundHttpException:

public function render($request, Exception $exception)
{
    // Kiểm tra nếu exception là một instance của class ModelNotFoundException
    if ($exception instanceof ModelNotFoundException) {
        // dd('test');
        if ($request->ajax()) {
            // Nếu request ở dạng ajax trả về lỗi 404 với thông báo dạng Json
            return response()->json(['error' => 'Không tìm thấy user'], 404);
        } else {
            // Request thông thường trả về view 404
            return response()->view('errors.404_user_not_found', [], 404);
        }
    }
    // Nếu exception là instance của NotFoundHttpException trả về trang 404
    if ($exception instanceof NotFoundHttpException) return response()->view('errors.404', [], 404);
    return parent::render($request, $exception);
}

Ở bất cứ đâu trong ứng dụng, bạn muốn tạo ra một exception NotFoundHttpException thì sử dụng hàm abort:

abort(404);

Trên đây là cách bạn quản lý Exception ở mức toàn cục của ứng dụng, nếu bạn chỉ muốn chuyển hướng đến 404_user_not_found trong những trường hợp riêng biệt, bạn hoàn toàn có thể sử dụng cặp cú pháp try catch trong những ngữ cảnh phù hợp:

try {
    $user = User::findOrFail($id);
    return view('user.show')->with('user', $user);
} catch(ModelNotFoundException $e) {
    response()->view('errors.404_user_not_found', [], 404);
}

4. Ví dụ tạo một class Exception riêng

Trong ví dụ ở phần 3 chúng ta chỉ thực hiện quản lý một Exception có sẵn là ModelNotFoundException, chúng ta hoàn toàn có thể tạo ra những class Exception riêng. Chúng ta tạo ra một class trìu tượng mở rộng từ Exception để tất cả các class Exception khác trong ứng dụng sẽ được mở rộng từ đây.

<?php namespace App\Exceptions;
 
use Exception;
 
abstract class AException extends Exception
{
    /**
     * @var string
     */
    protected $id;
 
    /**
     * @var string
     */
    protected $status;
 
    /**
     * @var string
     */
    protected $title;
 
    /**
     * @var string
     */
    protected $detail;
 
    /**
     * @param @string $message
     * @return void
     */
    public function __construct($message)
    {
        parent::__construct($message);
    }

    /**
     * Get the status
     *
     * @return int
     */
    public function getStatus()
    {
        return (int) $this->status;
    }

    /**
     * Return the Exception as an array
     *
     * @return array
     */
    public function toArray()
    {
        return [
            'id'     => $this->id,
            'status' => $this->status,
            'title'  => $this->title,
            'detail' => $this->detail
        ];
    }

    /**
     * Build the Exception
     *
     * @param array $args
     * @return string
     */
    protected function build(array $args)
    {
        $this->id = array_shift($args);
     
        $error = config(sprintf('errors.%s', $this->id));
     
        $this->title  = $error['title'];
        $this->detail = vsprintf($error['detail'], $args);
     
        return $this->detail;
    }
}

Trong phương thức build() chúng ta có sử dụng đến file cấu hình errors.php là file chứa các id, title và detail được định nghĩa sẵn cho các Exception.

<?php
 
return [
    'user_not_found' => [
        'title'  => 'Không tìm thấy user',
        'detail' => 'Không tìm thấy user nào phù hợp với điều kiện tìm kiếm.'
    ],
    ...
];

Giả sử trong ví dụ ở phần 3 chúng ta không sử dụng phương thức findOrFail() mà muốn thực hiện một truy vấn nào đó với User sau đó nếu user không tồn tại thì phát sinh UserNotFoundException là một Exception chúng ta tạo ra, vì có rất nhiều thứ NotFound nên chúng ta tạo ra class NotFoundException:

<?php namespace App\Exceptions;
 
class NotFoundException extends AException
{
    /**
     * @var string
     */
    protected $status = '404';
 
    /**
     * @return void
     */
    public function __construct()
    {
        $message = $this->build(func_get_args());
 
        parent::__construct($message);
    }
}

Và tiếp theo là class UserNotFoundException mở rộng từ NotFoundException:

<?php namespace App\Exceptions;
 
use App\Exceptions\NotFoundException;
 
class UserNotFoundException extends NotFoundException
{
 
}

Ok, chúng ta sẽ tìm user và nếu user không tồn tại sẽ throw ra UserNotFoundException:

Route::get('/user/{id}', function($id) {
    $user = User::find($id);
    if($user == null) throw new UserNotFoundException('user_not_found', $id);
    return $user->toArray();
});

Các phần quản lý Exception với phương thức render() của app\Exceptions\Handler bạn có thể xem lại ví dụ phần 3. Chú ý, nếu không muốn UserNotFoundException được ghi log thì bạn đưa class này vào thuộc tính $dontReport:

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

5. Lời kết

Framework Laravel luôn có những điều mới mẻ cho những kiến thức chuẩn trong PHP và với Exception chúng ta hoàn toàn quản lý tốt các lỗi xảy ra trong ứng dụng ở cả những ngữ cảnh đặc biệt nói riêng và quản lý toàn cục nói chung. Quản lý lỗi với Laravel Exception là rất cần thiết đảm bảo ứng dụng hoạt động thông suốt và nâng cao trải nghiệm người dùng. Chúng ta không muốn khách hàng than phiền rằng ứng dụng thỉnh thoảng bung ra những màn hình lỗi với các thông tin kỹ thuật rối rắm không thể hiểu nổi. Trong phần đầu của bài viết có nói đến các dịch vụ bên thứ 3 như Bugsnag hay Sentry là các dịch vụ ghi nhận và tổng hợp các exception giúp chúng ta quản lý lỗi ứng dụng tốt hơn. Phần ghi log trong Laravel sử dụng Monolog cũng còn rất nhiều tính ứng dụng, do khuôn khổ bài viết xin được dừng ở đây và hẹn các bạn trong những bài viết tiếp theo về vấn đề quản lý lỗi.

Add Comment