Repository Pattern trong PHP và ứng dụng với Laravel

16/10/2022 - lượt xem
Chia sẻ
 
5/5 - (1 bình chọn)

Repository Pattern là mẫu thiết kế không bị gắn liền với bât kì ngôn ngữ lập trình hay framework nào. Tuy vậy Trong khuôn khổ bài viết này, CodeTuTam sẽ hướng dẫn bạn sử dụng mẫu thiết kế này với PHP và Framework Laravel.

Các bạn cũng có thể tham khảo thêm các bài viết về mẫu thiết kế khác như

Repository Pattern là gì?

Repository nằm giữa không gian miền ứng dụng (domain) và các lớp ánh xạ dữ liệu. Chúng ta cũng có thể hiểu Repository như là tập hợp các đối tượng miền trong bộ nhớ. Dựa trên yêu cầu truy vấn từ client, Repository sẽ tiếp nhận, xử lý và trả về kết quả phù hợp. Các đối tượng có thể được thêm vào, xóa bỏ khỏi Repository. Repository chứa tập các đối tượng, ánh xạ các hành động đầu vào và xử lý nghiệp vụ ẩn đằng trong nó trước khi trả về kết quả.

Repository pattern tách biệt giữa logic truy cập dữ liệu và ánh xạ nó tới entity xử lý logic. Giao tiếp giữa các logic truy cập dữ liệu và Business logic thông qua interface.

Cách hoạt đọng của Repository Pattern
Cách hoạt động của Repository Pattern

Nói cách khác, Repository Pattern là một container xử lý các các logic truy cập vào dữ liệu. Nó ẩn các chi tiết của logic truy cập dữ liệu khỏi nghiệp vụ logic.

Việc tách biệt giữa truy cập dữ liệu và nghiệp vụ logic của Repository Pattern mang lại nhiều lợi ích như:

  • Tập trung xử lý logic dữ liệu, giúp code bảo trì dễ dàng hơn
  • Logic xử lý dữ liệu và logic nghiệp vụ có thể được kiểm thử riêng biệt
  • Giảm thiểu trùng lặp code
  • Giảm khả năng mắc lỗi lập trình

Repository Pattern làm việc thông qua các Interface

Một interface hoạt động giống như một hợp đồng chỉ định những gì lớp dẫn xuất của nó phải thực hiện. Nếu chúng ta có 2 đối tượng Actor và Film, thì các tập hợp các hành động chung của 2 đối tượng này có thể gom nhóm và đưa vào 1 interface. Các hành động được gom nhóm như sau:

  1. Lấy danh sách bản ghi
  2. Phân trang
  3. Tạo bản ghi mới
  4. Lấy bản ghi theo khóa chính
  5. Lấy bản ghi theo trường dữ liệu khác
  6. Cập nhật bản ghi
  7. Xóa bản ghi

Nếu chúng ta triển khai riêng rẽ các đối tượng, số lượng code trùng lặp là không hề nhỏ.

Chúng ta có thể tạo ra một interface chung cho việc này

interface RepositoryInterface {

    public function all($columns = array('*'));

    public function paginate($perPage = 15, $columns = array('*'));

    public function create(array $data);

    public function update(array $data, $id);

    public function delete($id);

    public function find($id, $columns = array('*'));

    public function findBy($field, $value, $columns = array('*'));
}

Cấu trúc thư mục tổ chức cho Repository Pattern

Trước khi triển khai Repository Pattern, chúng ta xem xét tới việc tổ chức các thư mục chứa code. Tham khảo cấu trúc thư mục như hình dưới đây:

Cấu trúc thư mục tổ chwucs Repository Pattern
Cấu trúc thư mục tổ chwucs Repository Pattern

Trong thư mục src, ta có 3 thư mục khác: Contracts, Eloquent, Exceptions. Trong Contracts sẽ chứa các interface. Eloquent chứa lớp Abstract và các lớp dẫn xuất của Interface. Thư mục Exceptiosn chứa các class Exception.

Để package tự động autload chúng ta cũng cần sử lại file composer.json. Trong ví dụ này được sử dụng Laravel 5.x, việc này tương tự với các phiên bản Laravel mới hơn về sau.

{
  "name": "bosnadev/repositories",
  "description": "Laravel Repositories",
  "keywords": [
    "laravel",
    "repository",
    "repositories",
    "eloquent",
    "database"
  ],
  "licence": "MIT",
  "authors": [
    {
      "name": "Mirza Pasic",
      "email": "[email protected]"
    }
  ],
  "require": {
    "php": ">=5.4.0",
    "illuminate/support": "5.*",
    "illuminate/database": "5.*"
  },
  "autoload": {
    "psr-4": {
      "Bosnadev\\Repositories\\": "src/"
    }
  },
  "autoload-dev": {
    "psr-4": {
      "Bosnadev\\Tests\\Repositories\\": "tests/"
    }
  },
  "extra": {
    "branch-alias": {
      "dev-master": "0.x-dev"
    }
  },
  "minimum-stability": "dev",
  "prefer-stable": true
}

Namespace được đặt là Bosnadev\Repository, đây chính là namespace sử dụng trong package của chúng ta.

<?php namespace Bosnadev\Repositories\Contracts;

interface RepositoryInterface {

...

}

Triển khai Repository Pattern với PHP

Triển khai mẫu thiết kế Repository
Triển khai mẫu thiết kế Repository

Mỗi dân xuất đều kế thừa từ abstract repository – lớp kế thừa từ RepositoryInterface

Ví dụ với hàm all – hàm thực hiện lấy toàn bộ dữ liệu, với tham số đầu vào là $columns – các trường dữ liệu cần lấy.

Đối với 1 thực thể cụ thể, phương thức này có thể đặt như sau

public function all($columns = array('*')) {
    return Bosnadev\Models\Actor::get($columns);
}

Nhưng chúng ta cần phải sử dụng chung hàm này, do vậy nên đoạn code sẽ được chỉnh lại là

public function all($columns = array('*')) {
    return $this->model->get($columns);
}

Trong trường hợp, $this->model là 1 instance của Bosnadev\Models\Actor thì 2 đoạn mã là tương đương nhau. Tương tự như vậy chúng ta thực thi với các hàm khác trong RepositoryInterface

<?php namespace Bosnadev\Repositories\Eloquent;

use Bosnadev\Repositories\Contracts\RepositoryInterface;
use Bosnadev\Repositories\Exceptions\RepositoryException;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Container\Container as App;

/**
 * Class Repository
 * @package Bosnadev\Repositories\Eloquent
 */
abstract class Repository implements RepositoryInterface {

    /**
     * @var App
     */
    private $app;

    /**
     * @var
     */
    protected $model;

    /**
     * @param App $app
     * @throws \Bosnadev\Repositories\Exceptions\RepositoryException
     */
    public function __construct(App $app) {
        $this->app = $app;
        $this->makeModel();
    }

    /**
     * Specify Model class name
     * 
     * @return mixed
     */
    abstract function model();

    /**
     * @return Model
     * @throws RepositoryException
     */
    public function makeModel() {
        $model = $this->app->make($this->model());

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

        return $this->model = $model;
    }
}

Trong đoạn mã trên, chúng ta đặt class là dạng abstract do có 1 hàm model() ở dạng abstract. Hàm này bắt buộc các lớp kế thừa phải triển khai nó, ví dụ như sau

<?php namespace App\Repositories;

use Bosnadev\Repositories\Contracts\RepositoryInterface;
use Bosnadev\Repositories\Eloquent\Repository;

class ActorRepository extends Repository {

    /**
     * Specify Model class name
     *
     * @return mixed
     */
    function model()
    {
        return 'Bosnadev\Models\Actor';
    }
}

Chúng ta tiếp tục implement các hàm còn lại của RepositoryInterface

<?php namespace Bosnadev\Repositories\Eloquent;

use Bosnadev\Repositories\Contracts\RepositoryInterface;
use Bosnadev\Repositories\Exceptions\RepositoryException;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Container\Container as App;

/**
 * Class Repository
 * @package Bosnadev\Repositories\Eloquent
 */
abstract class Repository implements RepositoryInterface {

    /**
     * @var App
     */
    private $app;

    /**
     * @var
     */
    protected $model;

    /**
     * @param App $app
     * @throws \Bosnadev\Repositories\Exceptions\RepositoryException
     */
    public function __construct(App $app) {
        $this->app = $app;
        $this->makeModel();
    }

    /**
     * Specify Model class name
     *
     * @return mixed
     */
    abstract function model();

    /**
     * @param array $columns
     * @return mixed
     */
    public function all($columns = array('*')) {
        return $this->model->get($columns);
    }

    /**
     * @param int $perPage
     * @param array $columns
     * @return mixed
     */
    public function paginate($perPage = 15, $columns = array('*')) {
        return $this->model->paginate($perPage, $columns);
    }

    /**
     * @param array $data
     * @return mixed
     */
    public function create(array $data) {
        return $this->model->create($data);
    }

    /**
     * @param array $data
     * @param $id
     * @param string $attribute
     * @return mixed
     */
    public function update(array $data, $id, $attribute="id") {
        return $this->model->where($attribute, '=', $id)->update($data);
    }

    /**
     * @param $id
     * @return mixed
     */
    public function delete($id) {
        return $this->model->destroy($id);
    }

    /**
     * @param $id
     * @param array $columns
     * @return mixed
     */
    public function find($id, $columns = array('*')) {
        return $this->model->find($id, $columns);
    }

    /**
     * @param $attribute
     * @param $value
     * @param array $columns
     * @return mixed
     */
    public function findBy($attribute, $value, $columns = array('*')) {
        return $this->model->where($attribute, '=', $value)->first($columns);
    }

    /**
     * @return \Illuminate\Database\Eloquent\Builder
     * @throws RepositoryException
     */
    public function makeModel() {
        $model = $this->app->make($this->model());

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

        return $this->model = $model->newQuery();
    }
}

Khi đó chúng ta có thể sử dụng ActorRepository trong Controller như sau

<?php namespace App\Http\Controllers;

use App\Repositories\ActorRepository as Actor;

class ActorsController extends Controller {

    /**
     * @var Actor
     */
    private $actor;

    public function __construct(Actor $actor) {

        $this->actor = $actor;
    }

    public function index() {
        return \Response::json($this->actor->all());
    }
}

Bổ sung các tiêu chí – điều kiện truy vấn cho Repository Pattern

Hãy tưởng tượng rằng, các hàm trên chúng ta chỉ thực hiện với các truy vấn đơn giản. Với những ứng dụng phức tạp hơn, chúng ta cần custom lại câu truy vấn cho phù hợp. Để làm điều này, chúng ta xác định những tiêu chí cần phải thực thi. Đơn giản chính là tạo ra một interface/ abstract class với duy nhất 1 hàm trong đó:

<?php namespace Bosnadev\Repositories\Criteria;

use Bosnadev\Repositories\Contracts\RepositoryInterface as Repository;
use Bosnadev\Repositories\Contracts\RepositoryInterface;

abstract class Criteria {

    /**
     * @param $model
     * @param RepositoryInterface $repository
     * @return mixed
     */
    public abstract function apply($model, Repository $repository);
}

Hàm này sẽ chứa tiêu chí truy vấn được áp dụng vào đối tượng repository cụ thể. Tiếp đến chúng ta cũng cần mở rộng lớp Repository thêm một chút. Đầu tiên, chúng ta tạo 1 interface chứa các hàm liên quan tới xử lý điều kiện truy vấn cho Repository.

<?php namespace Bosnadev\Repositories\Contracts;

use Bosnadev\Repositories\Criteria\Criteria;

/**
 * Interface CriteriaInterface
 * @package Bosnadev\Repositories\Contracts
 */
interface CriteriaInterface {

    /**
     * @param bool $status
     * @return $this
     */
    public function skipCriteria($status = true);

    /**
     * @return mixed
     */
    public function getCriteria();

    /**
     * @param Criteria $criteria
     * @return $this
     */
    public function getByCriteria(Criteria $criteria);

    /**
     * @param Criteria $criteria
     * @return $this
     */
    public function pushCriteria(Criteria $criteria);

    /**
     * @return $this
     */
    public function  applyCriteria();
}

Abstract class Repository của chúng ta cần implement interface này

<?php namespace Bosnadev\Repositories\Eloquent;

use Bosnadev\Repositories\Contracts\CriteriaInterface;
use Bosnadev\Repositories\Criteria\Criteria;
use Bosnadev\Repositories\Contracts\RepositoryInterface;
use Bosnadev\Repositories\Exceptions\RepositoryException;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Container\Container as App;

/**
 * Class Repository
 * @package Bosnadev\Repositories\Eloquent
 */
abstract class Repository implements RepositoryInterface, CriteriaInterface {

    /**
     * @var App
     */
    private $app;

    /**
     * @var
     */
    protected $model;

    /**
     * @var Collection
     */
    protected $criteria;

    /**
     * @var bool
     */
    protected $skipCriteria = false;

    /**
     * @param App $app
     * @param Collection $collection
     * @throws \Bosnadev\Repositories\Exceptions\RepositoryException
     */
    public function __construct(App $app, Collection $collection) {
        $this->app = $app;
        $this->criteria = $collection;
        $this->resetScope();
        $this->makeModel();
    }

    /**
     * Specify Model class name
     *
     * @return mixed
     */
    public abstract function model();

    /**
     * @param array $columns
     * @return mixed
     */
    public function all($columns = array('*')) {
        $this->applyCriteria();
        return $this->model->get($columns);
    }

    /**
     * @param int $perPage
     * @param array $columns
     * @return mixed
     */
    public function paginate($perPage = 1, $columns = array('*')) {
        $this->applyCriteria();
        return $this->model->paginate($perPage, $columns);
    }

    /**
     * @param array $data
     * @return mixed
     */
    public function create(array $data) {
        return $this->model->create($data);
    }

    /**
     * @param array $data
     * @param $id
     * @param string $attribute
     * @return mixed
     */
    public function update(array $data, $id, $attribute="id") {
        return $this->model->where($attribute, '=', $id)->update($data);
    }

    /**
     * @param $id
     * @return mixed
     */
    public function delete($id) {
        return $this->model->destroy($id);
    }

    /**
     * @param $id
     * @param array $columns
     * @return mixed
     */
    public function find($id, $columns = array('*')) {
        $this->applyCriteria();
        return $this->model->find($id, $columns);
    }

    /**
     * @param $attribute
     * @param $value
     * @param array $columns
     * @return mixed
     */
    public function findBy($attribute, $value, $columns = array('*')) {
        $this->applyCriteria();
        return $this->model->where($attribute, '=', $value)->first($columns);
    }

    /**
     * @return \Illuminate\Database\Eloquent\Builder
     * @throws RepositoryException
     */
    public function makeModel() {
        $model = $this->app->make($this->model());

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

        return $this->model = $model->newQuery();
    }

    /**
     * @return $this
     */
    public function resetScope() {
        $this->skipCriteria(false);
        return $this;
    }

    /**
     * @param bool $status
     * @return $this
     */
    public function skipCriteria($status = true){
        $this->skipCriteria = $status;
        return $this;
    }

    /**
     * @return mixed
     */
    public function getCriteria() {
        return $this->criteria;
    }

    /**
     * @param Criteria $criteria
     * @return $this
     */
    public function getByCriteria(Criteria $criteria) {
        $this->model = $criteria->apply($this->model, $this);
        return $this;
    }

    /**
     * @param Criteria $criteria
     * @return $this
     */
    public function pushCriteria(Criteria $criteria) {
        $this->criteria->push($criteria);
        return $this;
    }

    /**
     * @return $this
     */
    public function  applyCriteria() {
        if($this->skipCriteria === true)
            return $this;

        foreach($this->getCriteria() as $criteria) {
            if($criteria instanceof Criteria)
                $this->model = $criteria->apply($this->model, $this);
        }

        return $this;
    }
}

Khởi tạo 1 tiêu chí truy vấn mới

Với mô hình tổ chức như hiện tại, thì chúng ta có thể tùy chỉnh cho từng Repository một cách dễ dàng hơn, mà không cần mở rộng class gốc.

Cấu trúc thư mục cho tổ chức Tiêu chí tìm kiếm
Cấu trúc thư mục cho tổ chức Tiêu chí tìm kiếm

Một tiêu chí/điều kiện tìm kiếm có thể được viết như sau

<?php namespace App\Repositories\Criteria\Films;

use Bosnadev\Repositories\Contracts\CriteriaInterface;
use Bosnadev\Repositories\Contracts\RepositoryInterface as Repository;
use Bosnadev\Repositories\Contracts\RepositoryInterface;

class LengthOverTwoHours implements CriteriaInterface {

    /**
     * @param $model
     * @param RepositoryInterface $repository
     * @return mixed
     */
    public function apply($model, Repository $repository)
    {
        $query = $model->where('length', '>', 120);
        return $query;
    }
}

Sử dụng Tiêu chí trong các controller

Để truy vấn dữ liệu thông qua Repository có sử dụng các tiêu chí tìm kiếm vừa tạo, chúng ta có thể thực hiện một  trong 2 cách sau.

<?php namespace App\Http\Controllers;

use App\Repositories\Criteria\Films\LengthOverTwoHours;
use App\Repositories\FilmRepository as Film;

class FilmsController extends Controller {

    /**
     * @var Film
     */
    private $film;

    public function __construct(Film $film) {

        $this->film = $film;
    }

    public function index() {
        $this->film->pushCriteria(new LengthOverTwoHours());
        return \Response::json($this->film->all());
    }
}

Sử dụng hàm pushCriteria hoặc sử dụng hàm getByCriteria như sau

<?php namespace App\Http\Controllers;

use App\Repositories\Criteria\Films\LengthOverTwoHours;
use App\Repositories\FilmRepository as Film;

class FilmsController extends Controller {

    /**
     * @var Film
     */
    private $film;

    public function __construct(Film $film) {

        $this->film = $film;
    }

    public function index() {
        $criteria = new LengthOverTwoHours();
        return \Response::json($this->film->getByCriteria($criteria)->all());
    }
}

Tổng kết

Sử dụng Repository Pattern mang lại nhiều lợi ích khác nhau cho dự án bạn triển khai. Đơn giản nhất có thể thấy là giảm thiểu mã trùng lặp, sau đó là giúp cho ứng dụng dễ dàng mở rộng, kiểm thử hay bảo trì.

Về mặt kiến trúc, bạn đã tách biệt giữa phần xử lý nghiệp vụ logic và xử lý dữ liệu. Controller của bạn không cần biết bạn lưu dữ liệu ở đâu, hay lưu trữ như thế nào. Ví dụ như bạn có thể thay đổi nền tảng cơ sở dữ liệu từ Mysql sang Sqlserver mà không gặp nhiều vấn đề.

Bài viết này được dịch dựa trên bài viết gốc Repository Pattern các bạn có thể đọc để tìm hiểu rõ hơn

Nếu bạn có bất kì thắc mắc nào có thể để lại comment dưới bài viết để cùng trao đổi. Nếu bạn thấy bài viết về Repository Pattern có ích, đừng quên like và chia sẻ bài viết giúp Codetutam bạn nhé

 

 

 

    Liên hệ với chúng tôi

    Để lại thông tin để nhận được các bài viết khác

    5/5 - (1 bình chọn)

    Xem thêm nhiều bài tin mới nhất về Kiến thức

    Xem thêm