1. Mở đầu
Cách đây vài tháng trước khi loay hoay chọn một nền tảng để xây dựng một hệ thống mới, mình cũng tìm hiểu sơ qua về một số PHP framework và các kiến trúc bố trí để xem độ phù hợp cho dự án mới. Ai cũng biết là Laravel hiện tại đang nằm top trending trong PHP Frameworks thế hệ mới bởi nhiều lý do: độ thông dụng, các bảng cập nhật và liên tục thay đổi, tài liệu tốt, phong phú và một cộng đồng mạnh về nhiều mặt (từ xây dựng 1 cộng đồng giảng dạy, các hội nghị các nhà lập trình, cho đến cộng đồng hỗ trợ)
Để lựa chọn một framework mới có thể là quyết định khó ở thời điểm hiện tại, vì một số modern framework khác có nhiều điểm tương đồng và hoàn toàn khả năng tích hợp với các packages bên thứ 3 dễ dàng. Tuy nhiên dựa vào một số tiêu chí về thời gian và khả năng tích hợp nhanh các bên thứ 3, khả năng làm việc tốt (một số anh em trong team đã từng có thời gian làm việc với framework này trong thời gian ngắn), mình đã quyết định chọn Laravel để triển khai cho dự án mới.
Trong serial này, mình sẽ chia sẻ thêm về hướng tiếp cận framework Laravel, cũng như chia sẻ thêm một số kinh nghiệm trong quá trình triển khai, mong rằng sẽ giúp ích được cho mọi người. Mục tiêu của serial này
– Hiểu thêm về kiến trúc hệ thống Laravel
– Khả năng linh hoạt và tái sử dụng các business logic
2. Vì sao là Laravel và lý do cần áp dụng một kiến trúc vào Laravel?
Laravel khá nổi trội trong 5 năm trở lại đây khi Taylor release phiên bản Laravel 4 với một cách tiếp cận khác: Viết lại từ đầu. Cùng thời gian này Composer, một trình quản lý các package, trở nên thông dụng và thành một tiêu chuẩn mới. Taylor đã sớm thấy điều này và phân tách các thành phần của Laravel thành một bộ sưu tập các components và kết hợp nó bằng Composer. Với việc lấy phần lớn mã nguồn từ Symfony Components, không đồng nghĩa 2 framework này tiếp cận vấn đề giống nhau, Laravel đã bổ sung nhiều Component của riêng mình và kết hợp nhiều Design Parttern hiện đại để mang lại một Laravel thực sự khác biệt.
Vậy Laravel có gì đặc biệt? Hãy nhìn qua triết lý mà Taylor muốn truyền đạt qua Laravel: Tăng tốc độ phát triển sản phẩm và theo đuổi hạnh phúc nhà phát triển.
1 framework giúp các nhà phát triển dễ dàng sử dụng và nhanh chóng biến ý tưởng và giúp sản phẩm hoàn thành nhanh nhất hẳn là yếu tố hàng đầu của người thiết kế hệ thống và các lập trình viên.
“Happy developers make the best code” – một khái niệm được viết trong tài liệu hướng dẫn của Laravel. Thông điệp mạnh mẽ này trở thành mục tiêu chính và là hướng đi của Laravel. Còn gì hạnh phúc hơn việc bạn cảm thấy hài lòng khi lập trình với Laravel hàng ngày và cảm thấy sự tiện dụng trong từng dòng code. Các Components mang lại sự thích thú vì tính hiệu quả, đơn giản và đẹp mắt. Cả hai thứ ấy hẳn là điều bạn cảm nhận được khi làm việc với Laravel.
Vâỵ thì cần quái gì phải thiết kế thêm một kiến trúc vào Laravel, cứ add package bạn muốn và triển khai thôi?
Bạn nói không sai, với sự tiện dụng trên, khi làm việc với một hệ thống nhỏ và vòng đời phát triển nhanh, bạn không cần quan tâm đến sự việc tái sử dụng các code business logic của mình, việc sử dụng những thứ vốn có của Laravel đã đủ đáp ứng nhu cầu.
Nhưng nếu bạn là một junior trở lên và bắt đầu quan tâm nhiều hơn đến kiến trúc hệ thống. Câu hỏi thường trực của bạn khi tiếp cận một dự án mới là: Tôi nên đặt business logic ở đâu? Làm sao để việc tái sử dụng business logic hay các components cao nhất có thể, và thời gian bảo trì thấp nhất, hạn chế được các lỗi xảy ra trong quá trình nâng cấp một tính năng có sẵn. Và bài toán lúc này đặt ra là xây dựng kiến trúc trong Laravel như thế nào để tăng tính linh hoạt và khả năng tái sử dụng cao.
Quay lại câu hỏi: Tôi phải đặt business logic ở đâu? Nếu bạn đang đặt tất cả chúng thẳng vào controllers/models. Bạn sẽ gặp trường hợp khá kinh điển: controllers/models bắt đầu phình to ra và trở lên cồng kềnh và lộn xộn khi dự án của bạn thêm nhiều chức năng. Hoặc một số case study điển hình khác: Package bạn đang sử dụng ngừng phát triển và bạn tìm kiếm một giải pháp thay thế? Lúc này bạn hầu như phải tìm kiếm tất cả các chỗ đang sử dụng package này và chỉnh sửa lại toàn bộ. Hoặc trường hợp khác: Khi bạn thay thế giải pháp lưu trữ MySQL thành NoSQL ? Bạn sẽ phải thay thế hoàn toàn mã code tại models?
Bạn thấy đó, lúc này chi phí hay công sức để thay thế hay nâng cấp, kiểm thử sẽ tốn gấp bội phần.
Trước khi bàn đến việc áp dụng một kiến trúc để phân tách và tăng tính linh hoạt cho Laravel, chúng ta hãy quay lại một chút với kiến trúc mặc định của nó. Cụ thể, hãy xem qua vòng đời của 1 request trong ứng dụng Laravel.
Request Lifecycle in Laravel
Vòng đời từ khi người dùng thực hiện một thao tác gửi một request đến ứng dụng Laravel cho đến khi nhận được kết quả trả về (response) từ server.
Khởi tạo public/index.php
Tất cả các request gửi lên từ clients đều phải thông qua thư mục public và vào thẳng index.php. Hãy xem sơ qua index.php:
require __DIR__.'/../vendor/autoload.php'; $app = require_once __DIR__.'/../bootstrap/app.php';
Nhiệm vụ của hai dòng code trên khá đơn giản:
– Load tất cả các thư viện cần thiết trong autoload.php (file này đặt trong vendor khi bạn cài mới hoặc update một thư viện mới, việc cài đặt composer thông qua composer.json)
– Một instance app được khởi tạo ra trong bootstrap/app.php để chuẩn bị cho việc khởi động ứng dụng. Một số interfaces quan trọng sẽ được binding tại đây từ trong Service Container (Mình sẽ đề cập chi tiết hơn về Service Container ở bài sau)
Kernel sẽ được khởi tạo để handle request thông qua đoạn code
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); $response = $kernel->handle( $request = Illuminate\Http\Request::capture() ); $response->send();
HTTP KERNEL & CONSOLE KERNEL.
Tiếp theo, request được gửi đến Kernel (HTTP hoặc Console), tùy thuộc vào môi trường đang thực thi là webapp hay console.
Như bạn thấy trên, Kernel tập trung rất nhiều middleware liên quan. Nhiệm vụ của Kernel là khai báo tất cả các middleware mà request cần phải pass qua (xác thực hoặc đơn giản chỉ là ghi log – logging) trước khi request được tiếp tục xử lý phần logic.
Không dừng lại ở đó, Kernel còn tập hợp các bootstrappers cần phải chạy trước khi request được tiếp tục xử lý: cấu hình xử lý lỗi (error handling), ghi log (logging)…
Về cơ bản, Kernel có nhiệm vụ khá đơn giản là một chiếc hộp đen nhận request và trả response để tiếp tục vòng đời request. Và một nhiệm vụ tối thượng khác của Kernel là load tất cả các Service Providers.
Service Providers
Các service provider được cấu hình trong file config/app.php, Kernel sẽ giúp tải trước các bootstraper nào cần dùng (các service providers core của Laravel và các thư viện bên thứ 3 mình sẽ sử dụng). Quá trình load các service provider trải qua 2 bước:
- Đăng ký service provider (Register service provider)
- Khởi động service provider (Bootstrap service provider)
Service Provider sẽ giúp khởi động các core service của Laravel như router, validation, database… Đây là lý do nó là trái tim của Laravel.
Route
Sau khi load Service Provider, request sẽ tiếp tục gửi đến Route. Nhiệm vụ của Route dễ hiểu như chính cái tên của nó: điều hướng tất cả các request được gửi đến và quyết định hướng xử lý, có 2 hướng rẽ:
- Route -> Middleware -> Controller/Action
- Route -> Controller/Action
Trong Route, chúng ta có thể ràng buộc request đi qua chốt chặn Middleware tự tạo, hoặc đi thẳng vào Controller/Action.
Middleware
Middleware là một giải pháp khá hay của Laravel để filtering các request một lần nữa trước khi bạn xử lý thuần logic. Một ví dụ đơn giản: bạn cần chuyển hướng người dùng sang đến màn hình đăng nhập khi chưa login, và nếu user đã login, request sẽ tiếp tục xử lý tùy thuộc vào controller/action. Nếu như ở một số kiến trúc cũ, đa phần việc này được viết thẳng ở Controllers (trong before action hoặc xử lý ở request trước khi vào controller), thay vì đó trong Laravel bạn có thể lập một Middleware để thực hiện xác thực việc này. Với mô hình phân tách này, code xử lý khá dễ hiểu và đơn giản hơn nhiều.
Ngoài ra, Middleware còn được sử dụng cho nhiều mục đích khác.
Controller/Action, Views và phần còn lại
Quay trở lại mô hình LifeCycle, chúng ta có thể thấy response có thể trả về người dùng từ 2 cách: response thông qua View hoặc không thông qua View. View trong Laravel chứa các template sẵn, và cần xử lý kèm theo các biến. Các phần còn lại chắc hẳn rất quen thuộc với bạn, nên mình sẽ không đề cập thêm trong bài này.
Hãy cùng xem qua lại một vòng đời của một request qua cấu trúc Laravel thực tế:
3. Sử dụng Repository Pattern và Service Layer vào Laravel
Trở lại vấn đề về giải pháp cho một cấu trúc để làm cho Laravel linh hoạt hơn, tái sử dụng code nhiều hơn. Trong phần này mình sẽ giới thiệu thêm về 2 mô hình đang trở thành các best pratice trong cộng đồng Laravel. Sau khi tham khảo và thấy nó giải quyết được hầu hết các nhu cầu cơ bản trong việc tái sử dụng code
Repository Pattern
Repository Pattern là mẫu thiết kế khá phổ biến, nếu bạn đã thực sự hiểu nguyên tắc thiết kế Respository Pattern là gì thì không quan trọng ngôn ngữ lập trình là gì bạn vẫn có thể áp dụng được nó ở bất kì đâu. Bạn sẽ tạo ra một tầng Repository ở giữa Business Logic và lớp Model giúp phân tách lớp xử lý truy xuất dữ liệu.
Hiểu đơn giản, để viết các hàm thao tác dữ liệu với database, thay vì bạn viết nó thẳng vào Controller, bạn sẽ làm việc với Repository. Controller sẽ không còn một dòng code nào liên quan đến Model, thay vì đó bạn có thể inject vào thông qua __construct
Nhìn lại workflow một chút khi thêm Repository vào:
Controller -> Repository -> Model
Dependency Injection
Trước khi đi vào cách sử dụng như thế nào trong controller, chúng ta xem qua một chút về mẫu Dependency Injection, một mẫu thiết kế dùng khá phổ biến trong Laravel thể hiện cho sự linh hoạt. Một ví dụ đơn giản về Dependency như sau: Trong nhà bạn có hàng loạt các bóng đèn của một hãng A, không may một bóng bị cháy, nhưng bạn ra cửa tiệm không tìm thấy bóng đèn của hãng A (vì Covid nên việc nhập khẩu bóng bị ảnh hưởng :P). Vậy là bạn tìm một bóng đèn thay thế, để gắn một bóng đèn của hãng khác, bạn phải tìm một hãng tương tự với 1 chuôi đèn có chuẩn kết nối tương tự. Đây là keyword, bóng đèn phụ thuộc vào chuôi đèn với 1 chuẩn chung, vậy để thay thế nó bạn cứ tìm một nhà cung cấp có chuẩn tương tự.
Tương tự vậy, việc xây dựng các kết nối giữa các components cũng nên có một mối quan hệ có tính linh hoạt cao, sự phụ thuộc giữa các components nên theo kiểu dependency thông qua các chuẩn kết nối (chuẩn kết nối ở đây là các Interface). Các mối quan hệ dependency thông qua các chuẩn sẽ giúp bảo trì và thay thế dễ dàng bởi 1 component khác mà ít gây ảnh hưởng đến component khác, miễn là tuân theo các chuẩn phương thức. Trong Laravel, Service Container có thể được sử dụng ở mọi nơi, vì vậy bạn có thể inject dependency vào bên trong method của controller.
Một ví dụ nhỏ, sử dụng Repository Pattern, kết hợp với Dependency Injection.Chi tiết về cách thực thi một Repository trong Laravel sẽ được bàn trong bài viết sau. Giờ chúng ta tiếp tục xem qua về mẫu design Service Layer
Service Layer
Việc phân tách tầng Repository như trên chỉ mới giúp giảm một phần sự phụ thuộc quá lớn của Controller vào Model, rất nhiều các dự án mình từng biết qua vẫn đặt business logic phần lớn giữa Controller và Repository dẫn đến nhiều hệ lụy khác trong quá trình bảo trì và nâng cấp. Tầng business logic lúc này nằm phân tán và không tập trung. Nếu đặt quá nhiều business logic vào Repository e rằng không đúng nguyên tắc thiết kế ban đầu chỉ là nơi xử lý dữ liệu kèm một số business logic phụ có liên quan đến thao tác dữ liệu.
Service Layer là một mẫu thiết kế thông dụng trong phần mềm giúp phân tách tiếp tầng business logic. Một giải pháp giúp chuyển khối nặng của business logic sang 1 tầng khác, giúp ích rất lớn cho việc tái sử dụng code ở bất kì nơi nào. Service Layer thường được đặt giữa Controller và Repository. Như vậy mô hình của chúng ta lúc này khá hợp lý và giải quyết được yêu cầu ban đầu chúng ta đặt ra:
Controller -> Service -> Repository -> Model
Không những thế, Service có thể được inject vào bất kì class thông dụng khác.
Quay lại ví dụ về Post bên trên, lúc này chúng ta có thêm 1 folder Services nơi đặt các class services như sau:
PostController và PostService
Với sức mạnh của Service Container có thể giúp inject Service vào bất kì nơi nào. Nhưng liệu có sử dụng được trong một static method. Ví dụ bạn có 1 class GoogleNewsHelper và có 1 static method dùng để lấy tin tức từ Google News về máy chủ của bạn, lúc này nhu cầu của bạn là làm sao sử dụng được method PostService->find() bên trong một static method.
Rất may Laravel có Facade, giúp chúng ta có thể truy cập đến các hàm bên trong Service bằng cách gọi các hàm static. Thử tạo một PostFacade và khai báo PostService bên trong method getFacadeAccessor()
Và cách hàm static getFeed bên trong class GoogleNewsHelper gọi Post Service như sau:
Như vậy làm việc với một Post Facades thực tế là làm việc với PostService. Việc gọi hàm static của PostFacade thực tế được xử lý trong magic method __callStatic rồi chuyển qua lời gọi hàm bình thường. 2 cách gọi lúc này sẽ tương đương nhau:
//in common controller $this->postService->find($id); //in the static method(via Facade) PostFacade::find($id);
4. Lời kết
Trong khuôn khổ bài viết này, mình chỉ đi khái quát về kiến trúc cơ bản của Laravel và việc áp dụng thêm một số mẫu thiết kế để hoàn thiện cấu trúc một dự án để chinh chiến lâu dài. Các serial tiếp theo sẽ đề cập cụ thể hơn về một số thành phần bên trong Laravel. Hẹn gặp lại các bạn ở những phần sau. Happy Coding!