Repository Pattern trong Laravel
Design Pattern là kỹ thuật lập trình cung cấp cho chúng ta các mẫu thiết kế để áp dụng vào các trường hợp cụ thể để giải quyết các bài toán dễ dàng hơn. Các mẫu thiết kế này không phụ thuộc vào ngôn ngữ lập trình, vấn đề là bạn hiểu nguyên lý và áp dụng nó vào code mà thôi. Repository Pattern ...
Design Pattern là kỹ thuật lập trình cung cấp cho chúng ta các mẫu thiết kế để áp dụng vào các trường hợp cụ thể để giải quyết các bài toán dễ dàng hơn. Các mẫu thiết kế này không phụ thuộc vào ngôn ngữ lập trình, vấn đề là bạn hiểu nguyên lý và áp dụng nó vào code mà thôi. Repository Pattern là một mẫu thiết kế trong design pattern, và trong bài viết này mình sẽ giới thiệu mẫu thiết kế này với các bạn trong Laravel Framework.
Repository Pattern hoạt động như thế nào?
- Repository Pattern là lớp trung gian giữa tầng Business Logic và Data Access, giúp cho việc truy cập dữ liệu chặt chẽ và bảo mật hơn.
- Repository đóng vai trò là một lớp kết nối giữa tầng Business và Model của ứng dụng.
- Các phần truy xuất, giao tiếp với database năm rải rác ở trong code, khi bạn muốn thực hiện một thao tác lên database thì phải tìm trong code cũng như tìm các thuộc tính trong bảng để xử lý. Điều này gây lãng phí thời gian và công sức rất nhiều, vì thế với Repository design pattern, thì việc thay đổi ở code sẽ không ảnh hưởng quá nhiều công sức chúng ra chỉnh sửa.
- Những lý do ta nên sử dụng mẫu Repository Pattern:
- Thay đổi quyền truy cập dữ liệu cũng như xử lý dữ liệu chỉ ở một nơi nhất định.
- Việc chịu trách nhiệm cho việc mapping các bảng vào object cùng ở một nơi nhất định.
- Tăng tính bảo mật và rõ ràng cho code.
- Rất dễ dàng để thay thế một Repository với một implementation giả cho việc testing, vì vậy bạn không cần chuẩn bị một cơ sở dữ liệu có sẵn.
Các ví dụ cụ thể
Giả sự bây giờ chúng ta sẽ có một bảng là posts (với các cột cơ bản là title, content, is_published) và model như sau:
<?php namespace App; use IlluminateDatabaseEloquentModel; class Post extends Model { protected $fillable = [ 'title', 'content', 'is_published' ]; }
Và bây giờ chúng ta sẽ có các trường hợp cụ thể về cách thiết lặp các Controller, ở mỗi ví dụ mình sẽ chỉ ra 4 action đơn giản đó là:
- index để lấy tất cả các post
- show để lấy một bài post bất kỳ
- store để tạo bài viết mới
- update để cập nhật bài viết
- destroy để xóa bài viết
Controller không áp dung Repository Pattern
Với trường hợp không áp dụng Repository Pattern, chúng ta phải viết code điều hướng controller và xử lý lấy dữ liệu từ Database chung một chỗ, điều này rất khó để kiểm soát và có thể phải viết đi viết lại code nhiều lần (ở User Pannel, ở Admin Pannel chẳng hạn). Chúng sẽ như thế này:
<?php namespace AppHttpControllers; use IlluminateHttpRequest; use AppPost; class PostController extends Controller { /** * Show all post * * @return IlluminateHttpResponse */ public function index() { $posts = Post::all(); return view('home.posts', compact('posts')); } /** * Show single post * * @param $id int Post ID * @return IlluminateHttpResponse */ public function show($id) { $post = Post::findOrFail($id); return view('home.post', compact('post')); } /** * Create single post * * @param $request IlluminateHttpRequest * @return IlluminateHttpResponse */ public function store(Request $request) { $data = $request->all(); //... Validation here $post = Post::createOrFail($data); return view('home.post', compact('post')); } /** * Update single post * * @param $request IlluminateHttpRequest * @param $id int Post ID * @return IlluminateHttpResponse */ public function update(Request $request, $id) { $data = $request->all(); //... Validation here $post = Post::findOrFail($id); $post->update($data); return view('home.post', compact('post')); } /** * Delete single post * * @param $id int Post ID * @return IlluminateHttpResponse */ public function destroy($id) { $post = Post::findOrFail($id); $post->delete(); return view('home.post', compact('post')); } }
Ở ví dụ trên, ta thấy mỗi lần lấy một bài viết bất kỳ ta đều dựa trên Model, bây giờ lỡ như khách hàng yêu cầu chỉ lấy bài Post mà được public thôi thì sao? ta phải sửa tất cả các Action trên !?
Controller áp dung Repository Pattern
Để giải quyết ví dụ trên, ta sẽ áp dụng mẫu Repository vào, cụ thể ta sẽ tạo class trung gian giữa controller và model để giải quyết việc giao tiếp cơ sở dữ liệu tại một nơi. Ờ mà khoan, suy nghĩ chút... giả sử chúng ta làm 1 class Repository rồi mà dùng Eloquent đột ngột khách dở chứng đòi chuyển sang Redis hay MongoDB thì sao?! Bây giờ phải sửa toàn bộ repository sao, rồi mỗi lần đòi chuyển ta ta phải ngồi mò và viết lại tất cả sao. Trên thực tế có khá nhiều trường hợp éo le hơn như thế, mình có một giải pháp như thế này:
- Tạo ra một thư mục Repositories trong thư mục app để quản lý các Repository.
- Đầu tiên ta sẽ tạo một interface mang tên RepositoryInterface chung cho các mẫu repository bắt buộc các repository theo một chuẩn chung;
- Tiếp theo, ta sẽ xây dựng một abstract class tên là EloquentRepository cho driver Eloquent implements từ RepositoryInterface đưa ra các phương thức cơ bản bắt buộc Repository nào cũng phải có (sẽ có nhiều class driver khác nhau để lấy cơ sở dữ liệu như MongoDB, Redis, AWS, v.v..).
- Bây giờ, tạo một mẫu PostPostRepositoryInterface để định nghĩa các phương thức chỉ có trong Post Repository.
- Tiếp tục, tạo một class Post Repository thuộc driver Eloquent PostPostEloquentRepository kế thừa từ class EloquentRepository và implements từ PostPostRepositoryInterface.
- Bây giờ ta chỉ việc inject mẫu PostRepositoryInterface bind PostEloquentRepository trong AppServiceProvider là xong, bây giờ ta có thể gọi nó trong controller để xử dụng rồi (đây là kỹ thuật Dependency Injection, nếu bạn chưa biết hãy xem ví dụ bên dưới hoặc đọc tài liệu chính thức của Laravel tại https://laravel.com/docs/master/container).
Quy trình là như thế bây giờ ta sẽ thực hiện từ bước một. Ta xem qua cấu trúc file mà ta sẽ tạo trước đã:
Đầu tiên, ta sẽ tạo RepositoryInterface trước:
<?php namespace AppRepositories; interface RepositoryInterface { /** * Get all * @return mixed */ public function getAll(); /** * Get one * @param $id * @return mixed */ public function find($id); /** * Create * @param array $attributes * @return mixed */ public function create(array $attributes); /** * Update * @param $id * @param array $attributes * @return mixed */ public function update($id, array $attributes); /** * Delete * @param $id * @return mixed */ public function delete($id); }
Tiếp theo là abstract class EloquentRepository
<?php namespace AppRepositories; abstract class EloquentRepository implements RepositoryInterface { /** * @var IlluminateDatabaseEloquentModel */ protected $_model; /** * EloquentRepository constructor. */ public function __construct() { $this->setModel(); } /** * get model * @return string */ abstract public function getModel(); /** * Set model */ public function setModel() { $this->_model = app()->make( $this->getModel() ); } /** * Get All * @return IlluminateDatabaseEloquentCollection|static[] */ public function getAll() { return $this->_model->all(); } /** * Get one * @param $id * @return mixed */ public function find($id) { $result = $this->_model->find($id); return $result; } /** * Create * @param array $attributes * @return mixed */ public function create(array $attributes) { return $this->_model->create($attributes); } /** * Update * @param $id * @param array $attributes * @return bool|mixed */ public function update($id, array $attributes) { $result = $this->find($id); if($result) { $result->update($attributes); return $result; } return false; } /** * Delete * * @param $id * @return bool */ public function delete($id) { $result = $this->find($id); if($result) { $result->delete(); return true; } return false; } }
Sau khi có abstract class rồi, bây giờ mới tiến hành tạo interface cho Post, ngoài các phương thức bắt buộc phải có thì Post cần có thêm method findOnlyPublished và getAllPublished để lọc các bài đã đăng thôi (ví dụ vậy), nó sẽ như thế này:
<?php namespace AppRepositoriesPost; interface PostRepositoryInterface { /** * Get all posts only published * @return mixed */ public function getAllPublished(); /** * Get post only published * @return mixed */ public function findOnlyPublished(); }
Rồi, bây giờ ta tạo cái Repository Eloquent cho thằng Post thôi, nên nhớ nó sẽ kế thừa từ EloquentRepository và implements từ PostRepositorty :
<?php namespace AppRepositoriesPost; use AppRepositoriesEloquentRepository; class PostEloquentRepository extends EloquentRepository implements PostRepositoryInterface { /** * get model * @return string */ public function getModel() { return AppModelsPost::class; } /** * Get all posts only published * @return mixed */ public function getAllPublished() { $result = $this->_model->where('is_published', 1)->get(); return $result; } /** * Get post only published * @param $id int Post ID * @return mixed */ public function findOnlyPublished($id) { $result = $this ->_model ->where('id', $id) ->where('is_published', 1) ->first(); return $result; } }
Yeah, hoàn tất phần tạo repository cho Post rồi, bây giờ ta inject nó. Các bán sẽ mở tập tin /app/Providers/AppServiceProvider.php và thêm vào method register() như sau:
public function register() { $this->app->singleton( AppRepositoriesPostPostRepositoryInterface::class, AppRepositoriesPostPostEloquentRepository::class ); }
Ở trên, mỗi lần ta muốn đổi driver ví dụ từ Eloquent sang Redis, hay Mongo chỉ việc thay đổi AppRepositoriesPostPostEloquentRepository::class tương ứng, quá đơn giản phải không nào, bây giờ công việc cuối cùng là Inject vào controller và sử dụng thôi:
<?php namespace AppHttpControllers; use IlluminateHttpRequest; use AppRepositoriesPostPostRepositoryInterface; class PostController extends Controller { /** * @var PostRepositoryInterface|AppRepositoriesRepositoryInterface */ protected $postRepository; public function __construct(PostRepositoryInterface $postRepository) { $this->postRepository = $postRepository; } /** * Show all post * * @return IlluminateHttpResponse */ public function index() { $posts = $this->postRepository->getAll(); return view('home.posts', compact('posts')); } /** * Show single post * * @param $id int Post ID * @return IlluminateHttpResponse */ public function show($id) { $post = $this->postRepository->find($id); return view('home.post', compact('post')); } /** * Create single post * * @param $request IlluminateHttpRequest * @return IlluminateHttpResponse */ public function store(Request $request) { $data = $request->all(); //... Validation here $post = $this->postRepository->create($data); return view('home.post', compact('post')); } /** * Update single post * * @param $request IlluminateHttpRequest * @param $id int Post ID * @return IlluminateHttpResponse */ public function update(Request $request, $id) { $data = $request->all(); //... Validation here $post = $this->postRepository->update($id, $data); return view('home.post', compact('post')); } /** * Delete single post * * @param $id int Post ID * @return IlluminateHttpResponse */ public function destroy($id) { $this->postRepository->delete($id); return view('home.post'); } }
Từ bây giờ bạn có thể lấy dữ liệu, cập nhật dữ liệu thông qua repository mà không cần biết bên trong nó làm gì, như thế sẽ khiến code của chúng ta vừa gọn ràng, vừa dễ quản lý, bảo trì dễ dàng và tăng tính bảo mật nữa.
Lời kết
Việc áp dụng các mẫu thiết kế Design Patterns sẽ giúp các bạn tiết kiệm thời gian đồng thời cũng tăng hiệu suất, chất lượng code. Như mình đã nói ở trên, các mẫu thiết kế này không ràng buộc bởi ngôn ngữ lập trình, chỉ cần nắm rõ "mẫu thiết kế" thì bạn có thể xây dựng ở bất cứ ngôn ngữ nào, mình chỉ lấy Laravel để dễ diễn đạt thôi. Chúc các bạn thành công. Tham
Đọc thêm các bài viết của mình tại: https://dinhquochan.com/
khảo thêm:
- https://laravel.com/docs/master/container