+2

Khai Mở Sức Mạnh __construct Trong Laravel: Ma Thuật "Tiêm Phụ Thuộc" & Những Cú Lừa

Chuyện kể rằng, có một bạn Junior dev được giao viết tính năng Đăng ký tài khoản. Bạn ấy viết một cái UserController, bên trong cần dùng UserService để xử lý logic, và MailService để gửi email chào mừng.

Và đây là cách bạn ấy code:

class UserController extends Controller
{
    protected $userService;
    protected $mailService;

    public function __construct()
    {
        // TƯ DUY THỢ GÕ: Tự tay khởi tạo mọi thứ bằng từ khóa 'new'
        $this->userService = new UserService();
        $this->mailService = new MailService();
    }

    public function register(Request $request)
    {
        $user = $this->userService->createUser($request->all());
        $this->mailService->sendWelcomeEmail($user);
    }
}

Đoạn code này chạy ngon ơ. Nhưng dưới góc nhìn của một Vibe Coder, nó bốc mùi Tight Coupling (Kết dính chặt).

  • Lỡ ngày mai MailService cần truyền thêm một cái API Key vào hàm khởi tạo của nó (new MailService($apiKey)) thì sao? Bạn sẽ phải đi lục tìm TẤT CẢ các Controller đang gọi new MailService() để sửa lại. Chắc chắn sẽ sót bug!
  • Khi viết Unit Test, làm sao bạn có thể test UserController mà không gửi email thật? Bạn không thể "chặn" cái new MailService() kia được.

1. Ma Thuật Của Laravel: Dependency Injection (DI)

Laravel có một bộ não trung tâm gọi là Service Container. Bạn cứ tưởng tượng nó như một cái Nhà máy lắp ráp vạn năng.

Khi Laravel gọi UserController, nó không chạy new UserController() một cách ngu ngốc. Nó sẽ nhìn vào hàm __construct của bạn để xem bạn đang "xin" cái gì.

class UserController extends Controller
{
    protected $userService;
    protected $mailService;

    // TƯ DUY VIBE CODER: Chỉ cần "Khai báo" thứ mình cần
    public function __construct(UserService $userService, MailService $mailService)
    {
        $this->userService = $userService;
        $this->mailService = $mailService;
    }
}

Bạn không hề dùng từ khóa new. Bạn chỉ khai báo kiểu dữ liệu (Type-hint). Laravel Service Container sẽ tự động hiểu: "À, thằng Controller này cần UserServiceMailService. Để tao tự tạo 2 cái object đó, nạp đầy đủ cấu hình API Key các kiểu, rồi TIÊM (Inject) thẳng vào tay cho nó xài!".

Code của bạn giờ đây hoàn toàn Loose Coupling (Kết dính lỏng). Bạn không cần quan tâm MailService được tạo ra như thế nào, bạn chỉ việc xài. Test cũng cực kỳ dễ vì bạn có thể "tiêm" một cái MailService giả (Mock) vào lúc chạy Test.

2. Đẳng cấp PHP 8: Constructor Property Promotion

Nếu bạn nhìn đoạn code Vibe Coder ở mục 1, bạn vẫn thấy nó hơi "lặp từ". Khai báo biến 1 lần, viết trong tham số 1 lần, rồi gán $this->var = $var 1 lần nữa.

Kể từ PHP 8 (và Laravel 8+ trở đi), một Vibe Coder thực thụ sẽ rút gọn toàn bộ hàm __construct đó chỉ còn đúng... 1 dòng duy nhất nhờ tính năng Constructor Property Promotion.

Nhìn đây:

class UserController extends Controller
{
    // Đẳng cấp tối thượng: Khai báo biến, nhận DI, và gán giá trị cùng 1 lúc!
    public function __construct(
        protected UserService $userService,
        private MailService $mailService
    ) {}

    public function register(Request $request)
    {
        // Xài luôn!
        $this->userService->createUser($request->all());
    }
}

Nhét luôn các từ khóa protected/private vào trong ngoặc tròn của __construct. PHP sẽ tự động tạo biến class và gán giá trị cho bạn. Ngắn gọn, sạch sẽ, nhìn vào là mê!

3. Cú Lừa Kinh Điển: Gọi Auth::user() trong __construct

Đây là cái hố chôn 99% anh em mới học Laravel. Bạn muốn check xem user đang đăng nhập là ai để khởi tạo một cái gì đó dùng chung cho cả Controller, bạn hồn nhiên viết:

class PostController extends Controller
{
    protected $currentUser;

    public function __construct()
    {
        // CÚ LỪA! Nó sẽ luôn luôn trả về NULL
        $this->currentUser = Auth::user(); 
    }
}

Tại sao nó lại bằng null dù bạn đã đăng nhập thành công? Bởi vì vòng đời (Lifecycle) của Laravel chạy theo thứ tự:

  1. Tạo Controller (chạy hàm __construct).
  2. Chạy Middleware (nơi bóc tách Session/Cookie để biết User là ai).
  3. Chạy hàm logic của bạn (ví dụ index(), store()).

Như vậy, lúc __construct chạy, Middleware CÒN CHƯA ĐƯỢC KÍCH HOẠT, hệ thống hoàn toàn mù tịt về danh tính của bạn.

Giải pháp của Vibe Coder: Bọc nó vào một Middleware Closure ngay bên trong __construct.

class PostController extends Controller
{
    protected $currentUser;

    public function __construct()
    {
        // Báo cho Laravel: "Ê, chờ Middleware chạy xong rồi mới lấy user cho tao nhé!"
        $this->middleware(function ($request, $next) {
            $this->currentUser = Auth::user();
            return $next($request);
        });
    }
}

Lời kết

__construct trong Laravel không chỉ là nơi khởi tạo Object, nó là một "trạm thu phát tín hiệu" để báo cho Service Container biết bạn đang cần vũ khí gì để chinh chiến. Hiểu và lạm dụng Dependency Injection thông qua hàm khởi tạo sẽ giúp kiến trúc dự án của bạn vững như bàn thạch, dù có mở rộng thêm 100 tính năng nữa.

Chủ đề tiếp theo: Service Provider - Nơi Ma Thuật Được Chế Tạo

Chúng ta đã biết Service Container tự động tiêm UserService vào cho Controller. Nhưng nếu cái UserService đó của bạn cần cấu hình một cái API_KEY lấy từ bên thứ 3 thì sao? Container sẽ không tự biết cách truyền API_KEY đó vào đâu.

Đó là lúc chúng ta phải tự tay "dạy" cho Nhà máy Service Container cách lắp ráp các cỗ máy phức tạp. Ở bài viết tới, mình sẽ đàm đạo về trái tim thực sự của framework Laravel: Service Providers. Đây là khái niệm phân định rõ ràng giữa người dùng framework (User) và người làm chủ framework (Master). Anh em có muốn mình viết bài bóc tách nó thật dễ hiểu không?


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.