0

Series Repository Pattern | Bài 2: Base Repository - Tuyệt chiêu "Viết 1 lần, xài 100 lần"

Chào anh em! Ở Bài 1, chúng ta đã tạo ra một OrderRepository tuyệt đẹp để chứa logic truy vấn cho đơn hàng. Nhưng hãy thử tưởng tượng, dự án của bạn có thêm User, Product, Category, Ticket...

Mỗi cái Model đó đều cần các thao tác cơ bản: Lấy tất cả (all()), tìm theo ID (find()), tạo mới (create()), cập nhật (update()), và xóa (delete()). Nếu cứ mỗi Model bạn lại tạo một Repository và hì hục gõ lại 5 cái hàm này, bạn đang vi phạm nguyên lý DRY nghiêm trọng. Code của bạn không "Clean" chút nào, mà nó đang phình to một cách vô ích.

Giải pháp ở đây là gì? Chúng ta sẽ tạo ra một Base Repository (Kho chứa cốt lõi) mang tính trừu tượng (Generic). Nó sẽ chứa toàn bộ logic CRUD cơ bản, và các Repository con chỉ việc kế thừa lại để hưởng sái.

1. Khai báo "Hợp đồng chung" (Base Interface)

Đầu tiên, chúng ta cần định nghĩa một Interface chuẩn mực chứa các thao tác mà bất kỳ Model nào cũng phải có.

<?php
// app/Repositories/Contracts/BaseRepositoryInterface.php

namespace App\Repositories\Contracts;

interface BaseRepositoryInterface
{
    public function all();
    public function find($id);
    public function create(array $attributes);
    public function update($id, array $attributes);
    public function delete($id);
}

Từ bây giờ, OrderRepositoryInterface hay bất kỳ Interface nào khác sẽ kế thừa lại ông nội này:

// app/Repositories/Contracts/OrderRepositoryInterface.php
namespace App\Repositories\Contracts;

interface OrderRepositoryInterface extends BaseRepositoryInterface
{
    // Chỉ chứa các hàm ĐẶC THÙ của riêng Order
    public function getCompletedOrdersByUser($userId, $perPage = 10);
}

2. Xây dựng "Người cha vĩ đại" (Abstract Base Repository)

Tiếp theo, chúng ta tạo một class trừu tượng (abstract class) để implement cái Base Interface bên trên. Class này sẽ chứa logic thực sự của các hàm CRUD.

Điểm mấu chốt ở đây là: BaseRepository không biết nó đang làm việc với Model nào cả. Chúng ta sẽ ép các Repository con phải khai báo Model cụ thể cho nó thông qua hàm getModel().

<?php
// app/Repositories/Eloquent/BaseRepository.php

namespace App\Repositories\Eloquent;

use App\Repositories\Contracts\BaseRepositoryInterface;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Container\Container as App;

abstract class BaseRepository implements BaseRepositoryInterface
{
    protected $model;
    protected $app;

    public function __construct(App $app)
    {
        $this->app = $app;
        $this->makeModel();
    }

    // Hàm trừu tượng ép các class con phải khai báo class name của Model (vd: \App\Models\Order::class)
    abstract public function getModel();

    // Khởi tạo Model từ IoC Container
    public function makeModel()
    {
        $model = $this->app->make($this->getModel());

        if (!$model instanceof Model) {
            throw new \Exception("Class {$this->getModel()} must be an instance of Illuminate\\Database\\Eloquent\\Model");
        }

        return $this->model = $model;
    }

    // --- BẮT ĐẦU CÁC HÀM CRUD DÙNG CHUNG ---
    
    public function all()
    {
        return $this->model->all();
    }

    public function find($id)
    {
        return $this->model->find($id);
    }

    public function create(array $attributes)
    {
        return $this->model->create($attributes);
    }

    public function update($id, array $attributes)
    {
        $record = $this->model->find($id);
        if ($record) {
            $record->update($attributes);
            return $record;
        }
        return false;
    }

    public function delete($id)
    {
        $record = $this->model->find($id);
        if ($record) {
            return $record->delete();
        }
        return false;
    }
}

3. Tận hưởng phép thuật ở Repository Con

Bây giờ, hãy quay lại OrderRepository ở Bài 1 và xem nó lột xác trở nên gọn gàng như thế nào:

<?php
// app/Repositories/Eloquent/OrderRepository.php

namespace App\Repositories\Eloquent;

use App\Models\Order;
use App\Repositories\Contracts\OrderRepositoryInterface;

class OrderRepository extends BaseRepository implements OrderRepositoryInterface
{
    // Bắt buộc phải khai báo Model nào sẽ được dùng
    public function getModel()
    {
        return Order::class;
    }

    // Ở đây, bạn KHÔNG CẦN viết lại các hàm create(), update(), find()... nữa!
    // Chúng đã được kế thừa sẵn từ BaseRepository.

    // Chỉ tập trung viết các nghiệp vụ phức tạp đặc thù
    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);
    }
}

4. Sử dụng trong Controller

Không có gì thay đổi với Controller của bạn cả. IoC Container vẫn tự động tiêm OrderRepository vào. Nhưng giờ đây, bạn có trong tay toàn bộ sức mạnh của CRUD cơ bản lẫn các hàm query phức tạp.

public function store(Request $request)
{
    // Hàm create() này bản chất là được gọi từ BaseRepository!
    $order = $this->orderRepo->create($request->all());
    
    return response()->json($order);
}

Tổng kết Bài 2

Bằng cách áp dụng Base Repository, chúng ta đã giải quyết được triệt để bài toán lặp code. Từ giờ, mỗi khi thêm một bảng mới vào database, việc tạo một Repository cho nó chỉ mất đúng 30 giây (Tạo class và khai báo getModel()). Code backend của anh em giờ đã cực kỳ chuẩn Form, dễ maintain và chuẩn bị sẵn sàng cho việc scale lên quy mô lớn.

Tuy nhiên, có một bài toán hóc búa khác xuất hiện: Nếu trang danh sách Đơn hàng của bạn có một cái bộ lọc (Filter) cực kỳ phức tạp (Lọc theo ngày, theo status, theo giá, theo tên khách hàng...), bạn sẽ phải truyền cả chục tham số vào trong một hàm của Repository ư? Đừng lo, ở Bài 3, chúng ta sẽ kết hợp Repository Pattern với Criteria Pattern (Mẫu tiêu chí) để tạo ra những bộ lọc dữ liệu linh hoạt, mạnh mẽ mà không làm bẩn code!


All Rights Reserved

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