Toidicode.com

Toidicode.com

BASIC TO ADVANCE

Bài 11: Xử lý lỗi và Logging trong Laravel 13

Ở bài trước Laravel đã tự bắt ValidationException và biến nó thành response. Tiếp tục với series, bài này mình sẽ giới thiệu với mọi người về xử lý lỗi và logging trong Laravel 13: trang lỗi, abort(), cách ghi log và tùy biến exception trong bootstrap/app.php.

1. Chế độ debug.

Khi có exception không được bắt, Laravel sẽ ghi nó vào log (report) rồi chuyển thành response (render). Trang lỗi chi tiết đến đâu do APP_DEBUG trong .env quyết định:

  • true: hiện message, file, dòng code, stack trace và thông tin request, rất tiện khi code ở local.
  • false: chỉ hiện trang "500 | Server Error" (JSON là {"message": "Server Error"}).

Chú ý: Trên production luôn để APP_DEBUG=false để không lộ cấu hình và thông tin nhạy cảm của ứng dụng.

Dù bật hay tắt debug, lỗi vẫn được ghi vào log kèm stack trace.

2. Hàm abort() và HTTP exception.

Với các lỗi HTTP như không tìm thấy (404), không có quyền (403), các bạn dùng hàm abort() với cú pháp:

abort($code, $message, $headers);

Trong đó:

  • $code là mã HTTP, $message là thông báo lỗi, $headers là mảng header thêm vào response. Hai tham số sau có thể bỏ trống.

abort() ném ra HttpException (404 là NotFoundHttpException) nên code phía sau không chạy nữa. abort_if($condition, ...) dừng khi điều kiện đúng, abort_unless() thì ngược lại.

VD: Từ Bài 6, project không tồn tại vẫn trả về status 200. Mình sửa thành lỗi 404:

- app/Http/Controllers/ProjectController.php

public function show(Request $request, string $id): View
{
    $project = $this->projects->find((int) $id);

    // không có project thì dừng lại với lỗi 404
    abort_if($project === null, 404, 'Không tìm thấy project.');

    return view('projects.show')
        ->with('project', $project)
        ->with('tab', $request->query('tab', 'overview'));
}

View projects/show.blade.php bỏ được khối @if ($project === null) vì $project giờ luôn có giá trị. Lúc này /projects/99 trả về 404. Laravel không ghi HttpException vào log vì đây không phải lỗi code, chỉ có middleware LogRequests ở Bài 9 ghi dòng GET /projects/99 404 2ms.

3. Trang lỗi tùy biến.

Để thay trang lỗi mặc định, các bạn tạo view trong resources/views/errors với tên là mã lỗi: 404.blade.php, 500.blade.php... View nhận biến $exception chứa message đã truyền cho abort().

VD: Trang 404 của TaskFlow dùng component <x-layout> ở Bài 7.

- resources/views/errors/404.blade.php

<x-layout title="Không tìm thấy trang">
    <div class="py-16 text-center">
        <p class="text-6xl font-bold text-gray-300">404</p>

        <h1 class="my-4 text-2xl font-bold">{{ $exception->getMessage() ?: 'Trang bạn tìm không tồn tại.' }}</h1>
        <a href="{{ route('projects.index') }}" class="text-blue-600">Quay lại danh sách project</a>
    </div>
</x-layout>

Lúc này /projects/99 hiện "Không tìm thấy project.". Mình cũng đổi fallback route ở Bài 4 thành abort(404) để /khong-co hiện "Trang bạn tìm không tồn tại." thay vì message "The route khong-co could not be found." của router.

Route::fallback(function () {
    abort(404);
});

Lệnh php artisan vendor:publish --tag=laravel-errors chép các view lỗi của Laravel ra thư mục này để sửa. Ngoài ra 4xx.blade.php, 5xx.blade.php dùng chung cho các mã lỗi chưa có view riêng (trừ các mã Laravel đã có trang sẵn như 403, 404, 500).

4. Ghi log với Log facade.

Facade Log (facade sẽ nói ở Bài 25) có 8 method ứng với 8 level từ nghiêm trọng nhất trở xuống: emergency, alert, critical, error, warning, notice, info, debug. Tham số thứ hai là mảng context, ghi kèm dạng JSON:

Log::debug('Bắt đầu xử lý');
Log::warning('Project sắp hết hạn', ['id' => 1]);
Log::error('Không gửi được email');

// [2026-11-14 14:48:32] local.DEBUG: Bắt đầu xử lý
// [2026-11-14 14:48:32] local.WARNING: Project sắp hết hạn {"id":1}
// [2026-11-14 14:48:32] local.ERROR: Không gửi được email

Muốn mọi dòng log phía sau trong request đều có thêm một thông tin, các bạn dùng Log::withContext():

Log::withContext(['request_id' => 'a1b2c3']);

Log::info('Mở trang project', ['id' => 1]);
// local.INFO: Mở trang project {"request_id":"a1b2c3","id":1}

VD: Ghi log mỗi khi TaskFlow tạo project mới.

- app/Http/Controllers/ProjectController.php

use Illuminate\Support\Facades\Log;

// trong store(), sau khi thêm project
Log::info('Đã tạo project', [
    'id' => $project['id'],
    'name' => $project['name'],
    'ip' => $request->ip(),
]);

// local.INFO: Đã tạo project {"id":3,"name":"Blog cá nhân","ip":"127.0.0.1"}
// local.INFO: POST /projects 302 45ms

Dòng thứ hai là của LogRequests, ghi sau khi đã có response.

Lưu ý: IP, email là dữ liệu cá nhân, chỉ ghi khi thật sự cần và không giữ log quá lâu (VD channel daily ở phần 5).

5. Channel và cấu hình logging.

Log được ghi đi đâu là do channel quyết định, khai báo trong config/logging.php. Các channel hay dùng:

  • stack (mặc định): gom nhiều channel, ghi một lần vào tất cả.
  • single: ghi vào file storage/logs/laravel.log.
  • daily: mỗi ngày một file như laravel-2026-11-14.log, giữ 14 file gần nhất (đổi bằng LOG_DAILY_DAYS).
  • stderr: ghi ra stderr, hợp khi chạy trong Docker.

Channel nào được dùng do các biến trong .env:

LOG_CHANNEL=stack
LOG_STACK=single
LOG_LEVEL=debug

LOG_CHANNEL là channel mặc định, LOG_STACK là các channel trong stack (cách nhau bởi dấu phẩy, VD single,stderr), LOG_LEVEL là level thấp nhất được ghi: đặt warning thì các dòng debug, info, notice bị bỏ qua.

Để ghi vào channel khác, các bạn dùng Log::channel(), hoặc Log::stack() cho nhiều channel:

Log::channel('daily')->info('Ghi vào file theo ngày');
Log::stack(['single', 'daily'])->error('Ghi vào cả hai');

Để xem log, ngoài tail -f storage/logs/laravel.log còn có php artisan pail mà composer run dev ở Bài 2 đã chạy kèm (cần extension PCNTL): hiện log theo thời gian thực, lọc theo level với --level=error, -vv để xem cả stack trace.

6. Tùy biến xử lý exception.

Laravel 13 không còn class App\Exceptions\Handler như bản cũ, mọi tùy biến nằm trong withExceptions() của bootstrap/app.php. Dòng đầu có sẵn trong TaskFlow, còn lại là ví dụ:

use App\Exceptions\ProjectLockedException;
use Illuminate\Support\Facades\Log;
use Psr\Log\LogLevel;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

// ...
->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->shouldRenderJsonWhen(
        fn (Request $request) => $request->is('api/*') || $request->expectsJson(),
    );

    // lỗi database: ghi vào channel daily, không ghi log mặc định nữa
    $exceptions->report(function (PDOException $e) {
        Log::channel('daily')->critical('Lỗi database: '.$e->getMessage());
    })->stop();

    // lỗi 404 của các đường dẫn api/*
    $exceptions->render(function (NotFoundHttpException $e, Request $request) {
        if ($request->is('api/*')) {
            return response()->json(['message' => 'Không tìm thấy dữ liệu.'], 404);
        }
    });

    $exceptions->dontReport([ProjectLockedException::class]);
    $exceptions->level(RuntimeException::class, LogLevel::CRITICAL);
})

Trong đó:

  • shouldRenderJsonWhen(): khi nào lỗi được render dạng JSON, ở đây là đường dẫn api/* hoặc header Accept: application/json.
  • report(): closure chạy khi exception có kiểu trùng type-hint được report. Laravel vẫn ghi log như thường, trừ khi thêm ->stop() hoặc closure return false.
  • render(): tự tạo response, closure không trả về gì thì Laravel render như mặc định.
  • dontReport(): không report các class này. level(): đổi level khi ghi log.

Ngoài ra throttle() giới hạn số exception được report, còn hàm report($e) dùng trong catch để ghi exception mà request vẫn chạy tiếp.

Exception tự viết.

Exception tự viết có thể tự khai báo report() và render(), tạo bằng lệnh php artisan make:exception ProjectLockedException --render --report:

- app/Exceptions/ProjectLockedException.php

class ProjectLockedException extends Exception
{
    public function report(): void
    {
        Log::warning('Sửa project đã khóa: '.$this->getMessage());
    }

    public function render(Request $request): RedirectResponse
    {
        return to_route('projects.index')->with('status', $this->getMessage());
    }
}

Khi throw new ProjectLockedException('Project #1 đã bị khóa.'), Laravel gọi report() thay cho ghi log mặc định, rồi trả về response của render(): về danh sách project kèm thông báo.

7. Lời kết.

Như vậy chúng ta đã tìm hiểu xong xử lý lỗi và logging trong Laravel 13. Các bạn xem thêm tài liệu về xử lý lỗi và logging. Project của TaskFlow vẫn đang lưu tạm trong session, bài sau mình sẽ giới thiệu về database và migration để tạo bảng lưu dữ liệu thật.

Đăng ký nhận tin.

Chúng tôi chỉ gửi tối đa 2 lần trên 1 tháng. Tuyên bố không spam mail!

Vũ Thanh Tài

About author
The best way to learn is to share
Xem tất cả bài đăng

0 Comments

Bài viết chưa có ai bình luận, hãy là người đầu tiên đi bạn!

Bình luận

Captcha