Series Repository Pattern | Bài 1: Repository Pattern là gì? Giải cứu Controller khỏi "mớ bòng bong" Eloquent
Chào anh em, đối với các anh em hệ chơi laravel, Eloquent ORM có lẽ là một trong những tính năng tuyệt vời nhất. Nó giúp chúng ta thao tác database nhanh chóng, cú pháp gọn gàng và rất dễ đọc. Tuy nhiên sự tiện lợi đó là con dao 2 lưới khi dự án phình to việc nhét hàng tá câu các câu query Eloquent phức tạp trực tiếp vào trong Controller sẽ biến code của anh em thành một cơn ác mộng khi cần bảo trì, thay đổi database hoặc viết Unit Test.
Hôm nay, chúng ta sẽ bắt đầu tìm hiểu về Repository Pattern – một mẫu thiết kế sinh ra để làm lớp khiên bảo vệ Controller, tách biệt hoàn toàn logic truy xuất dữ liệu ra khỏi logic nghiệp vụ.
1. Nỗi đau khi Controller "ôm đồm" quá nhiều
Hãy nhìn vào một đoạn code Controller vô cùng quen thuộc khi chúng ta lấy danh sách đơn hàng (Order) của một User:
<?php
namespace App\Http\Controllers;
use App\Models\Order;
use Illuminate\Http\Request;
class OrderController extends Controller
{
public function getUserOrders($userId)
{
// Bad Practice: Controller trực tiếp chọc vào Eloquent Model
$orders = Order::where('user_id', $userId)
->where('status', 'completed')
->orderBy('created_at', 'desc')
->with(['payment', 'tickets'])
->paginate(10);
return response()->json($orders);
}
}
Nhìn có vẻ ngắn gọn, nhưng nó chứa đầy rủi ro tiềm ẩn:
- Lặp code (Code Duplication): Nếu một ngày đẹp trời, hàm xuất file Excel ở ExportController cũng cần lấy đúng danh sách order với các điều kiện y hệt (completed, sắp xếp giảm dần...), bạn sẽ phải copy-paste đoạn query này sang đó. Sửa một chỗ là phải nhớ sửa chỗ kia.
- Khó bảo trì: Nếu sếp yêu cầu đổi tên cột status thành order_status, bạn sẽ phải dùng tính năng "Find and Replace" đi mò mẫm sửa khắp mọi file Controller trong project.
- Vi phạm nguyên lý SOLID: Controller sinh ra chỉ để nhận Request và trả về Response, không phải là nơi để chứa logic query database.
2. Repository Pattern xuất hiện: Người trung gian đắc lực
Repository hiểu đơn giản là một "cái kho". Thay vì Controller tự đi vào database bới móc dữ liệu, nó sẽ đưa yêu cầu cho ông thủ kho (Repository). Ông thủ kho này biết chính xác cách sắp xếp, tìm kiếm và lấy đồ ra sao, sau đó gói gọn lại và đưa cho Controller.
Lợi ích mang lại:
- Tách biệt mối quan tâm (Separation of Concerns): Controller không cần biết bạn đang xài MySQL, MongoDB hay đọc từ file tĩnh. Nó chỉ cần gọi hàm.
- Tái sử dụng (Reusability): Một câu query chỉ viết đúng một lần ở Repository và dùng ở hàng chục Controller khác nhau.
- Mocking dễ dàng: Cực kỳ dễ dàng để viết Unit Test (sẽ kết hợp với Dependency Injection như chúng ta đã biết).
3. Khởi tạo Repository cơ bản
Để áp dụng Repository Pattern chuẩn xác, chúng ta không gọi trực tiếp Class mà sẽ giao tiếp thông qua Interface (Tuân thủ Dependency Inversion Principle).
Bước 1: Khai báo Bản hợp đồng (Interface)
Tạo một thư mục app/Repositories/Contracts.
<?php
// app/Repositories/Contracts/OrderRepositoryInterface.php
namespace App\Repositories\Contracts;
interface OrderRepositoryInterface
{
public function getCompletedOrdersByUser($userId, $perPage = 10);
}
Bước 2: Xây dựng kho chứa thật (Concrete Class)
Tạo thư mục app/Repositories/Eloquent. Lớp này sẽ làm việc trực tiếp với Eloquent Model.
<?php
// app/Repositories/Eloquent/OrderRepository.php
namespace App\Repositories\Eloquent;
use App\Models\Order;
use App\Repositories\Contracts\OrderRepositoryInterface;
class OrderRepository implements OrderRepositoryInterface
{
protected $model;
// Inject Model vào thông qua hàm khởi tạo
public function __construct(Order $model)
{
$this->model = $model;
}
public function getCompletedOrdersByUser($userId, $perPage = 10)
{
return $this->model->where('user_id', $userId)
->where('status', 'completed')
->orderBy('created_at', 'desc')
->with(['payment', 'tickets'])
->paginate($perPage);
}
}
Bước 3: Binding trong IoC Container
Đừng quên dặn vị quản gia của Laravel: "Nếu ai cần OrderRepositoryInterface, hãy đưa cho họ cái OrderRepository". (Cấu hình trong AppServiceProvider).
public function register()
{
$this->app->bind(
\App\Repositories\Contracts\OrderRepositoryInterface::class,
\App\Repositories\Eloquent\OrderRepository::class
);
}
Bước 4: Tận hưởng thành quả ở Controller
Bây giờ, Controller của bạn trở nên "sạch bóng" logic database và cực kỳ thanh lịch.
<?php
namespace App\Http\Controllers;
use App\Repositories\Contracts\OrderRepositoryInterface;
class OrderController extends Controller
{
// Tiêm Repository thông qua Dependency Injection
public function __construct(protected OrderRepositoryInterface $orderRepo) {}
public function getUserOrders($userId)
{
// Controller chỉ làm đúng nhiệm vụ điều phối
$orders = $this->orderRepo->getCompletedOrdersByUser($userId);
return response()->json($orders);
}
}
Tổng kết Bài 1
Chỉ với vài bước tái cấu trúc, chúng ta đã tách hẳn được tầng Database (Data Access Layer) ra khỏi tầng Giao tiếp (Controller). Code của bạn giờ đây đã có tính module cao hơn và sẵn sàng cho những thay đổi nghiệp vụ phức tạp trong tương lai.
Tuy nhiên, có một điểm yếu lộ ra: Nếu hệ thống của bạn có 50 Models (User, Order, Ticket, Payment...), chẳng lẽ bạn lại phải viết 50 cái hàm findById(), create(), update(), delete() lặp đi lặp lại ở 50 cái Repository khác nhau?
Để giải quyết bài toán lặp code ở mức độ cao hơn, ở Bài 2, chúng ta sẽ tìm hiểu cách xây dựng Base Repository (Repository Cốt lõi) – viết một lần, dùng cho mọi Model. Hẹn gặp lại anh em!
All rights reserved