Interface and Repository in Laravel

Bùi Huy Cường
4 min readApr 19, 2018

Bài viết nói về việc cách sử dụng Interface và mẫu thiết kế Repository (Repository pattern).

Đi thẳng luôn vào ví dụ luôn nhé.

Trong laravel, giả thiết mình đã có một model tên là Contact, mình muốn lấy danh sách tất cả các contact có trong db thì mình làm như sau:

class ContactsController extends BaseController
{
public function all()
{
$contacts = \Contact::all();
return View::make('contacts.index', compact('contacts'));
}
}

Cách viết trên là cách viết thông thường dành cho những người mới bắt đầu, và nó chẳng hề sai chút nào, nhưng về thiết kế nó là một ý tưởng …. tồi. Lý do vì sao:

  • Model bị gắn chặt vào Controller
  • Rất khó để test
  • khó khăn trong bảo trì, nếu muốn sửa đổi model Contact, thì hầu như trong Controller, chỗ nào có sử dụng model này, ta đều phải sửa lại, và đó thật là điều khó đỡ.

Ý tưởng ở đây để giải quyết vấn đề này là ta sẽ sử dụng Repository để tách Model và Controller ra, để khi có thay đổi của Model thì ta chỉ cần thay đổi ở Repo mà thôi.

Class ContactRepository
{
public function all(){
return \Contact::all();
}
public function find($id){
return \Contact::find($id);
}
}

Xong, ý tưởng trên khá oke, ta chỉ cần viết trong Controller như sau:

class ContactsController extends BaseController
{
public function all(){
$contactsRepo = new ContactRepository()
$contacts = $contactsRepo->all();
return View::make('contacts.index', compact('contacts'));
}
}

Nhưng…có một số vấn đề trong đoạn code trên:

  • Trong trường hợp này, bạn nên tránh việc sử dụng từ khóa “new” nhiều nhất có thể
  • Bạn vẫn gọi đến một Repo cụ thể.

Giải pháp:

  • Sử dụng Dependency Injection, thay vì bạn tạo trực tiếp đối tượng “new ContactRepository”, thì bạn có thể inject trực tiếp vào constructor.
  • Sử dụng Interface

Ví dụ việc sử dụng Interface:

interface ContactInterface
{
public function all();
public function store($data);
}
class ContactRepository implements ContactInterface
{
public function all(){
// some awesome code to retrieve all records
}
public function store($data){
// some awesome code to store a new contact
}
}

Sử dụng Dependency Injection:

class ContactsController extends BaseController
{
protected $contacts;
public function __construct(ContactInterface $contacts){
$this->contacts = $contacts;
}
}

Bạn tự hỏi? Làm sao chúng ta có thể inject ContactInterface vào construct thế kia được. Laravel đã giúp bạn tới tận răng rồi.

namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
//
}

public function register()
{
$this->app->bind('ContactInterface','ContactRepository');
}
}

Và đây là ý tưởng xuất sắc hơn ở trên:

class ContactsController extends BaseController
{
protected $contacts;
public function __construct(ContactInterface $contacts){
$this->contacts = $contacts;
}
public function all(){
$contacts = $this->contacts->all();
return View::make('contacts.index', compact('contacts'));
}
}

Đã dừng lại được chưa ??? Nhìn vào giải pháp trên thì thấy khá tuyệt phải không.

Hãy để mình thêm một ví dụ nữa:

Viết lại ContactInterface ở trên:

interface ContactInterface
{
public function all();
public function paginate($count);
public function find($id);
public function store($data);
public function update($id, $data);
public function delete($id);
public function findBy($field, $value);
}

Hệ thống nào thì hệ thống, cũng phải có quản lý user chứ nhỉ, thêm cái UserInterface cho biết mặt:

interface UserInterface
{
public function all();
public function paginate($count);
public function find($id);
public function store($data);
public function update($id, $data);
public function delete($id);
public function findBy($field, $value);
}

Nhân tiện có User rồi, mình thêm 1 JobInterface nữa chẳng hạn:

interface JobInterface
{
public function all();
public function paginate($count);
public function find($id);
public function store($data);
public function update($id, $data);
public function delete($id);
public function findBy($field, $value);
}

Khoan đã….

Đậu má, Tại sao lại lặp lại như vậy?

À hay rồi, mình sẽ lại tạo thêm 1 cái BaseInterface để cho bọn nàyExtends là được chứ gì:

interface BaseInterface
{
public function all();
public function paginate($count);
public function find($id);
public function store($data);
public function update($id, $data);
public function delete($id);
public function findBy($field, $value);
}

Bây giờ thì Extends nào:

interface ContactInterface extends BaseInterface {}
interface UserInterface extends BaseInterface {}
interface JobInterface extends BaseInterface {}
Interface FooInterface extends BaseInterface {}

Nếu vậy thì tuyệt vời hơn là mình sẽ tạo thêm một cái BaseRepository, dĩ nhiên rồi đúng không:

class BaseRepository
{
protected $modelName;
public function all(){
$instance = $this->getNewInstance();
return $instance->all();
}
public function find($id){
$instance = $this->getNewInstance();
return $instance->find($id);
}
protected function getNewInstance(){
$model = $this->modelName;
return new $model;
}
}

Không thể chờ thêm được nữa, extends ngay nào, và điều tuyệt vời nhất là đây:

class ContactRepository extends BaseRepository
{
protected $modelName = 'Contact';
}
class UserRepository extends BaseRepository
{
protected $modelName = 'User';
}

Không thể tin được, các bạn có thấy nếu mỗi lần có thêm một Model mới, ta chỉ cần tạo biến, thay vì viết lại các hàm như ở trên không ? Awesome

Trong Laravel, có một vấn đề mà các bạn chắc chắn đã gặp, hoặc chưa

Đó là vấn đề về N+1. Vấn đề được Laravel giải quyết bằng eager loading.

Vậy thực hiện trong Repo thế nào đây?

Đừng vội, có ví dụ ngay đây

class BaseRepository
{
public function find($id, $relations = []){
$instance = $this->getNewInstance();
return $instance->with($relations)->find($id);
}
}
class ContactsController extends BaseController
{
public function find($id){
$contact = $this->contacts->find($id, ['orders']);
return View::make('contacts.show', compact('contact'));
}
}

Quá tuyệt phải không ? No đã khiến bạn yêu nó ngay từ cái nhìn đầu tiên chưa?

Chắc chưa đâu, mới là cảm nắng thôi mà. Chờ bài tiếp theo nhé, mình nghĩ sẽ đủ sức mang đến “tình yêu sét đánh” cho bạn đấy. 😁

Bye 😀

19/04/2018

--

--