Xử lý dữ liệu trong Laravel
Hầu hết các MVC frameworks ngày nay đều chứa 3 thư mục chính: Models, Views và Controllers. Laravel không nằm ngoại lệ. Thông thường, chúng ta sẽ hiểu rằng Models là nơi chứa dữ liệu, Controllers sẽ xử lý dữ liệu và Views là nơi để hiển thị dữ liệu đó ra cho người dùng. Liệu điều đó có đúng hoàn ...
Hầu hết các MVC frameworks ngày nay đều chứa 3 thư mục chính: Models, Views và Controllers. Laravel không nằm ngoại lệ. Thông thường, chúng ta sẽ hiểu rằng Models là nơi chứa dữ liệu, Controllers sẽ xử lý dữ liệu và Views là nơi để hiển thị dữ liệu đó ra cho người dùng. Liệu điều đó có đúng hoàn toàn? Trong bài viết này, tôi xin chia sẻ làm thế nào để xử lý dữ liệu một cách hợp lý thông qua 3 cách:
Cách dễ dàng nhất để xử lý code logic là "tống" thẳng chúng trực tiếp vào bên trong Controller. Ví dụ, ta có một đoạn code xử lý công việc tạo mới một bài viết và hiển thị chúng:
class PostController extends Controller { public function store(Request $request) { // Validation $data = $request->validate([ 'title' => 'required|string', 'content' => 'required|string', ]); // Tạo post $post = new Post(); $post->title = $data['title']; $post->content = $data['content']; $post->save(); return redirect()->route('posts.index'); } // Publish post public function publish(Post $post) { $post->published_at = now(); $post->save(); return back(); } }
Ưu điểm
Cách viết này nhanh, gọn và nhẹ khi chúng ta cần xử lý những đoạn code đơn giản.
Nhược điểm
- Khi sử dụng cách này, gần như là Eloquent của bạn không phải xử lý tác vụ gì ngoài việc khai báo những trường được sử dụng và các mối quan hệ giữ các bảng. Ví dụ:
class Post extends Model { protected $guarded = ['id']; public function category() { return $this->belongsTo('AppCategory'); } }
- Khi xuất hiện thêm nhiều nghiệp vụ, nếu sử dụng cách này, Controller của bạn trở nên "phình to" => khó quản lý.
Về bản chất, Controller chỉ nên làm nhiệm vụ đúng như tên gọi của nó. Controller giống như một chiếc điều khiển TV. Khi người dùng bấm nút, Controller sẽ điều hướng tới kênh người dùng mong muốn và việc TV hiển thị ra gì phụ thuộc kết nối của TV với ăng ten hoặc đầu thu, không phải do chiếc điều khiển. Vậy làm thế nào để khắc phục những nhược điểm trên?
Để Controller đơn thuần là một chiếc điều khiển, chúng ta có thể chuyển toàn bộ code logic sang Eloquent:
class Post extends Models { // Declare attributes and relationships ... public static function savePost($title, $content) { $post = new Post(); $post->title = $title; $post->content = $content; $post ->save(); } public function publish() { $this->published_at = now(); $this->save(); } }
Controller của chúng ta sẽ trở nên đơn giản hơn:
class PostController extends Controller { public function store(Request $request) { // Validation $data = $request->validate([ 'title' => 'required|string', 'content' => 'required|string', ]); // Cầm chiếc điều khiển TV và điều hướng tới kênh "Tạo post" :)) Post::savePost($data['title'], $data['content']); return redirect()->route('posts.index'); } // Tương tự, chuyển sang kênh "Publish post" public function publish(Post $post) { $post->publish(); return back(); } }
Ngoài ra, chúng ta có thể sử dụng Scope. Trong Laravel, Scope cho phép bạn tạo ràng buộc cho các truy vấn tới Model. Chúng ta có thể viết một Scope để lấy ra những post được active trong Eloquent như thế này:
class Post extends Model { // Declare attributes and relationships ... // Scopes public function scopeActive($query) { return $query->where('status', 'active'); } }
Lưu ý: Scope trong Laravel luôn phải bắt đầu bằng "scopeNameOfTheScope". Laravel tự động hiểu "scope" như một alias vậy.
Ngoài ra, khi sử dụng Scope, code của chúng ta cũng trở nên gọn gàng hơn. Rõ ràng là, khi phải thực hiện nghiệp vụ trên nhiều modules, việc dùng Scope tiện hơn nhiều so với Eloquent ORM hay Query Builder để lấy ra được dữ liệu. Ví dụ, thay vì phải dùng Eloquent ORM để lấy những post được active:
$activePosts = Post::where('status', 'active')->get();
Chúng ta có thể thay thế bằng:
$activePosts = Post::active()->get();
Ưu điểm
- Giảm tải code logic ở Controller.
- Code gọn gàng hơn.
- Có thể tái sử dụng được. Về cơ bản, Scope trả về dữ liệu "gần cuối", tức là chưa đến bước ->get() hoặc là ->first(). Vì vậy, chúng ta có thể "nối đuôi" các scopes khi truy vấn dữ liệu. Ví dụ, sau khi khai báo 2 scopes là scopeGetName và scopeGetAddress, chúng ta có thể sử dụng chúng:
$customPosts = Post::getName('An')->getAddress('Vietnam')->get();
Nhược điểm
- Giống như Controller, việc sử dụng quá nhiều functions hay scopes khiến cho code khó kiểm soát và quản lý.
- Việc thay đổi code ở một Scope có thể gây ra phát sinh lỗi ở nhiều nơi. Rõ ràng là trong một scope scopeActive chúng ta thay đổi status từ 'active' thành 'deactive' thì bản chất của scope đó cũng thay đổi hoàn toàn rồi.
- Việc sử dụng quá nhiều functions hay scopes trong một Model khiến Model đó không thuần nhất là một Model nữa. Một Model chỉ nên có 3 phần:
- Khai báo các attributes.
- Khai báo các quan hệ.
- Khai báo các Scopes ràng buộc dữ liệu đơn giản, có thể tái sử dụng được.
Repository Pattern là một mẫu thiết kế trong Design Pattern (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).
Ý tưởng cơ bản thì Repository là cầu nối trung gian giữa Model và Controller.
Theo quan điểm cá nhân của tôi, chúng ta chỉ nên sử dụng Repository Pattern khi code logic trở nên phức tạp. Ví dụ, đối 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 Panel, ở Admin Panel chẳng hạn). Chúng sẽ như thế này:
class PostController extends Controller { public function index() { $posts = Post::all(); return view('home.posts', compact('posts')); } public function show($id) { $post = Post::findOrFail($id); return view('home.post', compact('post')); } public function store(Request $request) { $data = $request->all(); //... Validation here $post = Post::createOrFail($data); return view('home.post', compact('post')); } public function update(Request $request, $id) { $data = $request->all(); //... Validation here $post = Post::findOrFail($id); $post->update($data); return view('home.post', compact('post')); } 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 có status là 'active' thôi thì sao? Liệu chúng ta có phải sửa tất cả các action trên? May mắn là Repository Pattern được sinh ra nhằm khắc phục nhược điểm trên. Chúng ta sẽ đi sâu vào quy trình áp dụng Repository Pattern:
Step 1: Tạo thư mục quản lý các Repository
Ở bước này, ta sẽ tạo một thư mục Repositories trong thư mục app để quản lý các Repository.
Step 2: Tạo RepositoryInterface
Ta sẽ tạo một interface mang tên RepositoryInterface bắt buộc các repository theo một chuẩn chung:
namespace AppRepositories; interface RepositoryInterface { public function getAll(); public function find($id); public function create(array $attributes); public function update($id, array $attributes); public function delete($id); }
Step 3: Tạo RepositoryEloquent
Ta sẽ xây dựng một abstract class tên là RepositoryEloquent cho driver Eloquent implements từ RepositoryInterface. Abstratct class này đư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, v.v..).
namespace AppRepositories; abstract class RepositoryEloquent implements RepositoryInterface { protected $model; public function __construct() { $this->setModel(); } abstract public function getModel(); public function setModel() { $this->_model = app()->make($this->getModel()); } public function getAll() { return $this->_model->all(); } public function find($id) { $result = $this->_model->find($id); return $result; } public function create(array $attributes) { return $this->_model->create($attributes); } public function update($id, array $attributes) { $result = $this->find($id); if($result) { $result->update($attributes); return $result; } return false; } public function delete($id) { $result = $this->find($id); if($result) { $result->delete(); return true; } return false; } }
Step 4: Tạo PostRepositoryInterface
Bây giờ, tạo một mẫu PostPostRepositoryInterface để định nghĩa các phương thức chỉ có trong Post Repository. Ví dụ, 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:
namespace AppRepositoriesPost; interface PostRepositoryInterface { public function getAllPublished(); public function findOnlyPublished(); }
Step 5: Tạo PostRepositoryEloquent
Tiếp tục, tạo Repository Eloquent cho Post. Nó extends từ abstract class RepositoryEloquent (Step 3) và implements từ interface PostRepositoryInterface (Step 4):
namespace AppRepositoriesPost; use AppRepositoriesRepositoryEloquent; class PostRepositoryEloquent extends RepositoryEloquent implements PostRepositoryInterface { public function getModel() { return AppModelsPost::class; } public function getAllPublished() { $result = $this->_model->where('is_published', 1)->get(); return $result; } public function findOnlyPublished($id) { $result = $this ->_model ->where('id', $id) ->where('is_published', 1) ->first(); return $result; } }
Step 6: Inject
Chúng ta 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, AppRepositoriesPostPostRepositoryEloquent::class ); }
Step 7: Áp dụng Repository vào Controller
namespace AppHttpControllers; use IlluminateHttpRequest; use AppRepositoriesPostPostRepositoryInterface; class PostController extends Controller { protected $postRepository; public function __construct(PostReposiotoryInterface $postRepository) { $this->postRepository = $postRepository; } public function index() { $posts = $this->postRepository->getAll(); return view('home.posts', compact('posts')); } public function show($id) { $post = $this->postRepository->find($id); return view('home.post', compact('post')); } public function store(Request $request) { $data = $request->all(); //... Validation here $post = $this->postRepository->create($data); return view('home.post', compact('post')); } public function update(Request $request, $id) { $data = $request->all(); //... Validation here $post = $this->postRepository->update($id, $data); return view('home.post', compact('post')); } public function destroy($id) { $this->postRepository->delete($id); return view('home.post'); } }
Ưu điểm
GIảm việc lặp code.
Code dễ dàng maintain.
Nhược điểm
Mất thời gian để tìm hiểu và thích ứng.
Trong bài viết này tôi đã giới thiệu với các bạn các cách xử lý dữ liệu trong Laravel. Hi vọng bài viết giúp ích cho các bạn. Hẹn gặp lại các bạn ở các bài viết sau.
https://kipalog.com/posts/Repository-Pattern-trong-Laravel
https://medium.com/@mantasd/proper-way-to-use-laravel-eloquent-3ca194e2b766
https://viblo.asia/p/laravel-design-patterns-series-repository-pattern-part-3-ogBG2l1ZRxnL