Repository Pattern Trong Laravel 11: Thiết Kế Chuẩn Enterprise

VMas-Dev-AnHuynh

Repository Pattern Trong Laravel 11: Hướng Dẫn Chi Tiết Từ A-Z

Bẫy “Fat Controller” và sự cần thiết của mẫu thiết kế Repository Pattern

So sánh kiến trúc Controller truyền thống và Repository Pattern trong Laravel

Hãy tưởng tượng bạn đang mở một file UserController.php trong dự án Laravel của mình. Bên trong đó, ngoài việc xử lý request và trả về response, bạn còn thấy hàng tá câu lệnh Eloquent Builder phức tạp:

$users = User::where('status', 'active')
    ->whereHas('orders', function ($query) {
        $query->where('total', '>', 1000);
    })
    ->with(['profile', 'roles.permissions'])
    ->orderBy('created_at', 'desc')
    ->paginate(20);

Và rồi đoạn code đó được nhân bản ở 3-4 controller khác nhau. Đó chính là “Fat Controller” – cái bẫy mà nhiều lập trình viên Laravel mắc phải khi mới bắt đầu.

Tác hại của việc nhồi nhét logic truy vấn trong Controller

  1. Vi phạm Single Responsibility Principle (SRP) – Controller có quá nhiều trách nhiệm: nhận request, xử lý logic nghiệp vụ, truy vấn dữ liệu, trả response.
  2. Khó tái sử dụng – Logic truy vấn bị rải rác khắp nơi, mỗi khi cần thay đổi query bạn phải sửa ở nhiều file.
  3. Khó kiểm thử (Unit Test) – Không thể mock được tầng database, bạn phụ thuộc vào database thật khi test.
  4. Khó thay đổi nguồn dữ liệu – Nếu một ngày bạn muốn chuyển từ MySQL sang MongoDB hoặc sử dụng Elasticsearch cho module tìm kiếm, bạn sẽ phải sửa hàng loạt controller.

Repository Pattern ra đời để giải quyết triệt để những vấn đề trên. Nó tạo ra một lớp trừu tượng giữa tầng business logic (Controller/Service) và tầng truy xuất dữ liệu (Eloquent/Query Builder), giúp mã nguồn trở nên sạch sẽ, dễ bảo trì và dễ kiểm thử hơn.


1. Các thành phần cốt lõi trong cấu trúc Repository Pattern

Cấu trúc thư mục Repository Pattern trong Laravel 11

Trước khi bắt tay vào code, chúng ta cần hiểu rõ kiến trúc và vai trò của từng thành phần trong Repository Pattern.

Tầng giao ước: UserRepositoryInterface

Interface đóng vai trò như một bản hợp đồng (contract) giữa tầng business logic và tầng dữ liệu. Nó định nghĩa các phương thức mà bất kỳ repository nào cũng phải triển khai.

Tầng triển khai cụ thể: EloquentUserRepository

Đây là nơi hiện thực hóa các phương thức đã được khai báo trong Interface, sử dụng Eloquent ORM (hoặc Query Builder, raw SQL, API…).

Cách tổ chức cấu trúc thư mục app/Repositories gọn gàng

app/
├── Repositories/
│   ├── Contracts/           # Interfaces (tầng giao ước)
│   │   └── UserRepositoryInterface.php
│   ├── Eloquent/            # Triển khai bằng Eloquent
│   │   └── EloquentUserRepository.php
│   └── BaseRepository.php   # Class trừu tượng chứa CRUD cơ bản

Ví dụ code chi tiết

Bước 1: Tạo Interface

<?php

declare(strict_types=1);

namespace App\Repositories\Contracts;

use App\Models\User;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;

interface UserRepositoryInterface
{
    /**
     * Lấy tất cả người dùng.
     *
     * @return Collection<int, User>
     */
    public function all(): Collection;

    /**
     * Tìm người dùng theo ID.
     *
     * @param int $id
     * @return User|null
     */
    public function find(int $id): ?User;

    /**
     * Tìm người dùng theo email.
     *
     * @param string $email
     * @return User|null
     */
    public function findByEmail(string $email): ?User;

    /**
     * Lấy danh sách người dùng theo vai trò, có phân trang.
     *
     * @param string $role
     * @param int $perPage
     * @return LengthAwarePaginator
     */
    public function getUsersByRole(string $role, int $perPage = 15): LengthAwarePaginator;

    /**
     * Tạo mới người dùng.
     *
     * @param array $data
     * @return User
     */
    public function create(array $data): User;

    /**
     * Cập nhật người dùng.
     *
     * @param int $id
     * @param array $data
     * @return bool
     */
    public function update(int $id, array $data): bool;

    /**
     * Xóa người dùng.
     *
     * @param int $id
     * @return bool
     */
    public function delete(int $id): bool;
}

Bước 2: Tạo lớp triển khai Eloquent

<?php

declare(strict_types=1);

namespace App\Repositories\Eloquent;

use App\Models\User;
use App\Repositories\Contracts\UserRepositoryInterface;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;

class EloquentUserRepository implements UserRepositoryInterface
{
    public function all(): Collection
    {
        return User::all();
    }

    public function find(int $id): ?User
    {
        return User::find($id);
    }

    public function findByEmail(string $email): ?User
    {
        return User::where('email', $email)->first();
    }

    public function getUsersByRole(string $role, int $perPage = 15): LengthAwarePaginator
    {
        return User::whereHas('roles', function ($query) use ($role) {
            $query->where('name', $role);
        })->with(['profile'])->paginate($perPage);
    }

    public function create(array $data): User
    {
        return User::create($data);
    }

    public function update(int $id, array $data): bool
    {
        $user = $this->find($id);
        if (!$user) {
            return false;
        }
        return $user->update($data);
    }

    public function delete(int $id): bool
    {
        $user = $this->find($id);
        if (!$user) {
            return false;
        }
        return $user->delete();
    }
}

Bước 3: Tạo BaseRepository để tránh lặp code (Best Practice)

<?php

declare(strict_types=1);

namespace App\Repositories;

use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Pagination\LengthAwarePaginator;

abstract class BaseRepository
{
    protected Model $model;

    public function __construct(Model $model)
    {
        $this->model = $model;
    }

    public function all(): Collection
    {
        return $this->model->all();
    }

    public function find(int $id): ?Model
    {
        return $this->model->find($id);
    }

    public function create(array $data): Model
    {
        return $this->model->create($data);
    }

    public function update(int $id, array $data): bool
    {
        $record = $this->find($id);
        if (!$record) {
            return false;
        }
        return $record->update($data);
    }

    public function delete(int $id): bool
    {
        $record = $this->find($id);
        if (!$record) {
            return false;
        }
        return $record->delete();
    }

    public function paginate(int $perPage = 15): LengthAwarePaginator
    {
        return $this->model->paginate($perPage);
    }
}

Sau đó, EloquentUserRepository có thể kế thừa BaseRepository để tái sử dụng các phương thức CRUD cơ bản:

<?php

declare(strict_types=1);

namespace App\Repositories\Eloquent;

use App\Models\User;
use App\Repositories\BaseRepository;
use App\Repositories\Contracts\UserRepositoryInterface;
use Illuminate\Pagination\LengthAwarePaginator;

class EloquentUserRepository extends BaseRepository implements UserRepositoryInterface
{
    public function __construct(User $model)
    {
        parent::__construct($model);
    }

    public function findByEmail(string $email): ?User
    {
        return $this->model->where('email', $email)->first();
    }

    public function getUsersByRole(string $role, int $perPage = 15): LengthAwarePaginator
    {
        return $this->model->whereHas('roles', function ($query) use ($role) {
            $query->where('name', $role);
        })->with(['profile'])->paginate($perPage);
    }
}

💡 Giải thích code: Interface định nghĩa các phương thức với kiểu trả về rõ ràng (Collection, ?User, LengthAwarePaginator). Lớp EloquentUserRepository hiện thực tất cả các phương thức đó bằng Eloquent. Việc sử dụng declare(strict_types=1); giúp ép kiểu nghiêm ngặt, tránh lỗi ngầm định. BaseRepository giúp tái sử dụng các phương thức CRUD phổ biến, giảm thiểu code trùng lặp.


2. Cấu hình Dependency Injection qua AppServiceProvider trong Laravel 11

Luồng Dependency Injection từ Controller qua AppServiceProvider đến Repository trong Laravel 11

⚠️ Lưu ý quan trọng về Laravel 11

Laravel 11 đã có những thay đổi lớn về cấu trúc ứng dụng. Theo tài liệu chính thức từ Laravel, các service provider mặc định đã được loại bỏ, chỉ còn lại AppServiceProvider trong thư mục app/Providers.

Nhiều bài viết cũ hướng dẫn tạo thêm file RepositoryServiceProvider riêng lẻ – cách làm này không còn phù hợp với phong cách tối giản (minimalist) của Laravel 11. Chúng ta sẽ chỉ sử dụng AppServiceProvider để đăng ký binding, vừa đơn giản vừa đúng chuẩn Laravel 11.

Chỉnh sửa phương thức register() trong AppServiceProvider

<?php

declare(strict_types=1);

namespace App\Providers;

use App\Repositories\Contracts\UserRepositoryInterface;
use App\Repositories\Eloquent\Eloq‌uentUserRepository;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        // Bind Interface với Concrete Class
        $this->app->bind(
            UserRepositoryInterface::class,
            EloquentUserRepository::class
        );
    }

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        //
    }
}

📖 Theo tài liệu Laravel 11: “Trong phương thức register, bạn chỉ nên bind các dịch vụ vào service container. Không bao giờ được đăng ký event listeners, routes, hay bất kỳ chức năng nào khác trong phương thức register”.

Inject trực tiếp Interface vào Controller thông qua hàm khởi tạo __construct

<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Repositories\Contracts\UserRepositoryInterface;
use Illuminate\Http\Request;
use Illuminate\View\View;

class UserController extends Controller
{
    private UserRepositoryInterface $userRepository;

    /**
     * Sử dụng Constructor Property Promotion của PHP 8.x
     * để rút gọn code inject dependency.
     */
    public function __construct(
        private UserRepositoryInterface $userRepository
    ) {}

    public function index(): View
    {
        $users = $this->userRepository->getUsersByRole('admin', 20);
        return view('users.index', compact('users'));
    }

    public function show(int $id): View
    {
        $user = $this->userRepository->find($id);

        if (!$user) {
            abort(404, 'User not found');
        }

        return view('users.show', compact('user'));
    }

    public function store(Request $request)
    {
        $validated = $request->validate([
            'name' => 'required|string|max:255',
            'email' => 'required|email|unique:users',
            'password' => 'required|min:8',
        ]);

        $user = $this->userRepository->create($validated);

        return redirect()->route('users.show', $user->id)
            ->with('success', 'User created successfully!');
    }
}

💡 Giải thích code: Sử dụng Constructor Property Promotion của PHP 8.x giúp khai báo và gán property ngay trong parameter của constructor, rút gọn đáng kể số dòng code. Laravel Service Container sẽ tự động phân giải dependency dựa trên type-hint UserRepositoryInterface. Vì đã bind interface với class cụ thể trong AppServiceProvider, container biết phải inject EloquentUserRepository vào controller.


3. Góc nhìn phản biện: Khi nào KHÔNG nên lạm dụng Repository Pattern?

Biểu đồ quyết định giúp xác định khi nào nên áp dụng Repository Pattern trong Laravel

Là một kỹ sư Senior, tôi phải thẳng thắn: Repository Pattern không phải là “viên đạn bạc” cho mọi dự án. Lạm dụng nó có thể dẫn đến over-engineering – thiết kế quá mức cần thiết, làm dự án nhỏ trở nên cồng kềnh và khó bảo trì hơn.

Bản chất Eloquent đã là một Active Record Pattern mạnh mẽ

Laravel Eloquent ORM bản thân nó đã là một Active Record Pattern – mỗi Model vừa đại diện cho một bảng trong database, vừa chứa các phương thức để tương tác với dữ liệu. Eloquent cung cấp một API cực kỳ mạnh mẽ và trực quan:

// Viết trực tiếp trong Controller bằng Eloquent vẫn rất sạch
$activeUsers = User::where('status', 'active')->get();
$user->posts()->where('published', true)->get();

Đối với các dự án CRUD đơn giản, việc thêm Repository Pattern chỉ tạo ra thêm một lớp trừu tượng không thực sự cần thiết, làm tăng số lượng file và độ phức tạp của codebase.

Nhận diện dự án CRUD đơn giản để tránh Over-engineering

Nên sử dụng Repository Pattern khi:

  • Dự án có logic truy vấn phức tạp, nhiều điều kiện where, join, with.
  • Dự án có khả năng phải thay đổi nguồn dữ liệu trong tương lai (ví dụ: từ MySQL sang MongoDB hoặc Elasticsearch).
  • Dự án yêu cầu Unit Test ở mức độ cao, cần mock tầng database.
  • Dự án có quy mô lớn, nhiều team cùng phát triển, cần phân định rõ ràng trách nhiệm giữa các tầng.

Không cần Repository Pattern khi:

  • Dự án là CRUD đơn giản (blog, landing page, admin cơ bản).
  • Dự án không có kế hoạch mở rộng hoặc thay đổi database.
  • Team nhỏ, thời gian phát triển gấp rút.

💡 Lời khuyên: Hãy luôn đặt câu hỏi “Mình có thực sự cần điều này không?” trước khi áp dụng bất kỳ design pattern nào. Đôi khi sự đơn giản lại là giải pháp tốt nhất.


Lỗi thường gặp khi triển khai Repository Pattern

❌ Lỗi: Target [App\Repositories\Contracts\UserRepositoryInterface] is not instantiable

Nguyên nhân: Quên khai báo binding trong Service Provider, hoặc viết sai chính tả tên class/namespace khiến Laravel Service Container không biết tìm class cụ thể nào để nạp.

Cách khắc phục:

  1. Kiểm tra file app/Providers/AppServiceProvider.php:

    public function register(): void
    {
     $this->app->bind(
         UserRepositoryInterface::class,
         EloquentUserRepository::class  // Đảm bảo namespace chính xác
     );
    }
  2. Kiểm tra use statements đã import đúng namespace:

    use App\Repositories\Contracts\UserRepositoryInterface;
    use App\Repositories\Eloquent\Eloq‌uentUserRepository;
  3. Chạy lệnh xóa cache cấu hình:

    php artisan config:clear
    php artisan cache:clear

Best Practices khi dùng Repository Pattern

Luôn tạo BaseRepository chung chứa các hàm CRUD cơ bản (all, find, create, update, delete) để tránh lặp lại code ở các repository con.

Tuyệt đối không trả về Eloquent Builder ra ngoài Repository – chỉ được phép trả về dữ liệu cuối cùng (Collection, Model, LengthAwarePaginator). Điều này giữ trọn vẹn lớp trừu tượng và đảm bảo controller không phụ thuộc vào Eloquent.

Kết hợp Repository Pattern với Service Pattern – Controller gọi Service, Service gọi Repository. Service chứa business logic, Repository chỉ chịu trách nhiệm truy xuất dữ liệu.

Sử dụng strict types – Luôn khai báo declare(strict_types=1); và ép kiểu trả về rõ ràng để tránh lỗi ngầm định.

Đặt tên method rõ ràng, có ý nghĩagetActiveUsersByRole tốt hơn getUsersByStatus.


Câu hỏi thường gặp (FAQ)

Repository Pattern có làm giảm hiệu năng của Laravel do sinh ra nhiều file trung gian không?

Trả lời: Không đáng kể. PHP và Laravel được tối ưu hóa để xử lý hàng nghìn class trong một request. Chi phí tạo thêm một vài lớp trừu tượng là không đáng kể so với lợi ích về khả năng bảo trì và kiểm thử. Tuy nhiên, nếu bạn lo lắng về hiệu năng, hãy sử dụng caching (Redis/Memcached) ở tầng Repository để giảm số lượng truy vấn database.

Làm cách nào để viết Unit Test mock dữ liệu cho một Controller sử dụng Repository Interface?

Trả lời: Với Repository Pattern, việc mock dữ liệu trở nên cực kỳ dễ dàng. Bạn có thể sử dụng Mockery hoặc PHPUnit để tạo mock object:

<?php

namespace Tests\Unit;

use App\Http\Controllers\UserController;
use App\Repositories\Contracts\UserRepositoryInterface;
use Mockery;
use Tests\TestCase;

class UserControllerTest extends TestCase
{
    public function test_index_returns_users(): void
    {
        // Tạo mock repository
        $mockRepo = Mockery::mock(UserRepositoryInterface::class);
        $mockRepo->shouldReceive('getUsersByRole')
            ->once()
            ->with('admin', 20)
            ->andReturn(collect([
                (object) ['id' => 1, 'name' => 'Admin User']
            ]));

        // Inject mock vào controller
        $controller = new UserController($mockRepo);
        $response = $controller->index();

        $this->assertViewHas('users');
        $this->assertCount(1, $response->getData()['users']);
    }
}

Theo tài liệu Laravel 11, Laravel cung cấp các helper methods cho việc mocking thông qua Mockery, giúp việc viết Unit Test trở nên thuận tiện hơn.


Kết luận

Repository Pattern là một công cụ mạnh mẽ giúp bạn tổ chức mã nguồn Laravel theo hướng Enterprise, tách biệt rõ ràng giữa business logic và data access logic. Trong Laravel 11, với cấu trúc tối giản chỉ còn AppServiceProvider, việc triển khai Repository Pattern trở nên gọn gàng và dễ dàng hơn bao giờ hết.

Tuy nhiên, hãy luôn nhớ rằng thiết kế phần mềm là nghệ thuật của sự cân bằng. Đừng áp dụng pattern một cách máy móc – hãy đánh giá nhu cầu thực tế của dự án để quyết định có nên sử dụng hay không.


Tham khảo thêm:

Chia sẻ bài viết này
By VMas-Dev-AnHuynh Software Engineer
Follow:
SOFTWARE ENGINEER / FULL-STACK DEVELOPER