Kiến trúc hệ thống trên Laravel – phần 9
Bài đăng này đã không được cập nhật trong 5 năm
Các bài viết trong series
- Kiến trúc hệ thống trên Laravel – phần 1 : Tại sao phải áp dụng architect vào trong Laravel
- Kiến trúc hệ thống trên Laravel – phần 2 : OOP, Interface, Dependency Injection, IoC
- Kiến trúc hệ thống trên Laravel – phần 3 : Phân tích sâu vào việc sử dụng interface
- Kiến trúc hệ thống trên Laravel – phần 4 : Design Pattern – Decorator
- Kiến trúc hệ thống trên Laravel – phần 5 : Design Pattern – Adapter
- Kiến trúc hệ thống trên Laravel – phần 6 : Design Pattern – Repository
- Kiến trúc hệ thống trên Laravel – phần 7 : Design Pattern – Factory
- Kiến trúc hệ thống trên Laravel – phần 8 : Advance component trong Laravel
- Kiến trúc hệ thống trên Laravel – phần 9 : Mô hình kiến trúc cụ thể Part 1
- Kiến trúc hệ thống trên Laravel – phần 10 : Mô hình kiến trúc cụ thể Part 2
Xin chào các bạn. Mình vào nghề lập trình cũng đã lâu, cũng có 1 số hiểu biết coi như là nâng cao về framework Laravel. Nên hôm nay mình xin chia sẻ 1 chút về kiến trúc hệ thống của mình được xây dựng trên Laravel như thế nào. Mong rằng có thể giúp ích cho các bạn .
Rồi, cuối cùng chúng ta cũng đến phần cuối cùng. Phần này chúng ta sẽ có 2 bài riêng biệt tương ứng với 2 kiến trúc gần giống nhau .
Nhưng trước hết, kiến trúc code (code structure) theo các bạn là gì?
Theo mình, nó đơn giản lắm, chỉ là cách sắp đặt code để đạt được 1 mục đích nhất định.
Nào, mục đích của các bạn là gì? Mục đích của mình thì đơn giản lắm, chỉ cố gắng biến các component thành dạng plug n play, thích thì nhúng vào, không thích thì bỏ ra -> cần sắp đặt code sao cho thực hiện được việc này dễ nhất.
Với mục đích đó, đương nhiên trước tiên mình cần phải xác định các component sẽ tương tác với nhau trong hệ thống
Trong phần 6, mình cũng có nói về việc xử lý 1 request thì thường đi qua những phần nào.
Request -> check authen -> check author -> validation -> process raw request data -> update database -> render view -> Response
Vậy cách mình xử lý nhưng phần trên như thế nào:
- Check Authen: sử dụng middleware mặc định của Laravel
- Check Authorization: đơn giản nhất sử dụng ACL mặc định của Laravel hoặc các package nổi tiếng về Authorization (các bạn xem lại phần 8 nhé)
- Validation: sử dụng Request của Laravel hoặc sử dụng Ardent
- Process Raw data: tách ra thành các class riêng gọi là services -> nhớ luôn phải tạo interface và inject interface đó vào trong controller nhé
- Tương tác với database: Sử dụng repository, inject vào service.
- Render view: Tất nhiên là sử dụng blade của Laravel rồi.
OK, chúng ta vào ví dụ cụ thể nhé. Mình sẽ hướng dẫn các bạn làm 1 cái cơ bản nhất để thể hiện ý tưởng của mình, đó là làm 1 cái CRUD cơ bản ^^. Mình luôn luôn lấy ví dụ này để training cho các bạn mới vào, chỉ cần tạo 1 CRUD liên quan tới Book (title, author) là okie ^^.
1. Khởi tạo project
Tạo project mới bằng command
laravel new demo
Sau đó vào thư mục demo mới tạo và edit file .env để link tới database mới tạo của bạn
Sau đó edit file config/database, comment 2 dòng dưới đây:
// 'charset' => 'utf8mb4',
// 'collation' => 'utf8mb4_unicode_ci',
Chạy command dưới đây để tạo hệ thống authentication default của Laravel
php artisan make:auth
Cuối cùng chạy các lệnh sau để migrate database
php artisan migrate
Vậy là các bạn có 1 hệ thống với các chức năng default của Laravel (Login / Register) -> giờ hoặc trỏ vhost hoặc chạy lệnh dưới đây để test laravel ở port 8000
php artisan serve
Bạn hãy chạy thử và đăng ký thử 1 user xem.
2. Cấu trúc project
Tạo mới thư mục “core” ở root của project
Edit file composer.json để autoload thực mực core
"psr-4": {
"App\\": "app/",
"Core\\": "core/"
}
Thực hiện lệnh dưới đây để autoload
composer dump-autoload
Rồi, giờ chúng ta sẽ tạo các sub-folder trong thư mục “core”. Bạn nhớ các sub-folder luôn phải viết hoa chữ cái đầu nhé.
Với phần tích ban đầu thì chỉ có Services và Repository là sử dụng ngoài core của Laravel -> chúng ta sẽ thêm 2 sub-folder là “Repositories” và “Services”.
Chúng ta sẽ cần 1 chỗ để bindding interface thành concrete class -> Tạo 1 file CoreServiceProvider.php trong thư mục con “core/Providers” để làm việc này
Sau khi hoàn thành xong thì bạn sẽ có cấu trúc folder như dưới đây.
Edit config/app.php để load CoreServiceProvider -> thêm dòng dưới đây vào providers array:
Core\Providers\CoreServiceProvider::class,
3. Tạo migration, controller, model và route
Chúng ta sẽ tạo migration cho Book (title, author) bằng command sau:
php artisan make:migration create_books_table --create=books
Mở file migration mới được tạo ra và thêm title, author như sau:
$table->increments('id');
$table->string('title');
$table->string('author');
$table->timestamps();
chạy lệnh migrate để update database
php artisan migrate
Tiếp theo là tạo controller
php artisan make:controller BooksController --resource
Tạo model
php artisan make:model Book
Mở file app/Book.php và thêm dòng dưới đây vào:
protected $fillable = ['title', 'author'];
Thêm dòng mới vào trong route (routes/web.php)
Route::resource('/books', 'BooksController');
4. Xử lý chính trong hệ thống
*) Áp middleware cho Authentication (Authorization mình sẽ ko nhắc tới ở đây ^^)
Áp dụng route group vào
Route::group(['middleware' => 'auth'], function () {
Route::resource('/books', 'BooksController');
});
Ở thời điểm này nếu bạn chưa login mà vào ../books thì sẽ bị redirect lại trang login là chắc chắn ^^.
*) Dùng request để validate: chúng ta sẽ validate trong 2 trường hợp, khi tạo mới và khi edit -> cần tạo 2 request khác nhau:
php artisan make:request CreateBookRequest
php artisan make:request EditBookRequest
Mở lần lượt từng file request mới được tạo ra (trong folder app/Http/Requests) và thay đổi
Hàm authorize() thì return true –> bạn có thể thực thi authorize cho request này ở đây cũng được, nhưng mình thường ko xử lý gì ở đây Hàm rules() thì return array dưới đây
return [
"title" => "required",
"author" => "required",
];
Sử dụng request trong controller -> Mở file BooksController.php và sửa lại signature của 2 hàm: store và update
public function store(CreateBookRequest $request)
public function update(EditBookRequest $request, $id)
Nhớ import namespace trên đầu file BooksController.php
use App\Http\Requests\EditBookRequest;
use App\Http\Requests\CreateBookRequest;
*) Tạo Repository
Tạo mới interface BookRepositoryContract (core/Repositories/BookRepositoryContract.php)
<?php
namespace Core\Repositories;
interface BookRepositoryContract
{
public function paginate();
public function find($id);
public function store($data);
public function update($id, $data);
public function destroy($id);
}
Tạo class xử lý Repository (core/Repositories/BookRepository.php)
<?php
namespace Core\Repositories;
use App\Book;
class BookRepository implements BookRepositoryContract
{
protected $model;
public function __construct(Book $model)
{
$this->model = $model;
}
public function paginate()
{
return $this->model->paginate(10);
}
public function find($id)
{
return $this->model->findOrFail($id);
}
public function store($data)
{
return $this->model->create($data);
}
public function update($id, $data)
{
$model = $this->find($id);
return $model->update($data);
}
public function destroy($id)
{
$model = $this->find($id);
return $model->destroy($id);
}
}
Binding trong CoreServiceProvider
$this->app->bind(BookRepositoryContract::class, BookRepository::class);
Nhớ import namespace ở đầu file
use Core\Repositories\BookRepository;
use Core\Repositories\BookRepositoryContract;
*) Tạo Services
Tạo mới interface core/Services/BookServiceContract.php
<?php
namespace Core\Services;
interface BookServiceContract
{
public function paginate();
public function find($id);
public function store($data);
public function update($id, $data);
public function destroy($id);
}
Tạo mới class core/Services/BookService.php
<?php
namespace Core\Services;
use Core\Repositories\BookRepositoryContract;
class BookService implements BookServiceContract
{
protected $repository;
public function __construct(BookRepositoryContract $repository)
{
return $this->repository = $repository;
}
public function paginate()
{
return $this->repository->paginate();
}
public function find($id)
{
return $this->repository->find($id);
}
public function store($data)
{
return $this->repository->store($data);
}
public function update($id, $data)
{
return $this->repository->update($id, $data);
}
public function destroy($id)
{
return $this->repository->destroy($id);
}
}
Bạn chú ý là mình inject interface BookRepositoryContract vào trong class này -> luôn luôn inject interface nhé.
Cuối cùng bindding trong CoreServiceProvider thôi
$this->app->bind(BookServiceContract::class, BookService::class);
*) Chỉnh sửa BooksController để nhúng service vào
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Requests\EditBookRequest;
use Core\Services\BookServiceContract;
use App\Http\Requests\CreateBookRequest;
class BooksController extends Controller
{
protected $service;
public function __construct(BookServiceContract $service)
{
$this->service = $service;
}
public function index()
{
$items = $this->service->paginate();
return view('books.index', compact("items"));
}
public function create()
{
return view('books.create');
}
public function store(CreateBookRequest $request)
{
$this->service->store($request->all());
return redirect()->route('books.index');
}
public function show($id)
{
$item = $this->service->find($id);
return view('books.show', compact('item'));
}
public function edit($id)
{
$item = $this->service->find($id);
return view('books.edit', compact('item'));
}
public function update(EditBookRequest $request, $id)
{
$this->service->update($id, $request->all());
return redirect()->route('books.index');
}
public function destroy($id)
{
$this->service->destroy($id);
return redirect()->route('books.index');
}
}
Bạn thấy không, mình inject interface BookServiceContract vào trong controllers đấy nhé ^^.
*) Cuối cùng, tạo view bằng blade, phần này cho phép mình ko post code ở đây nhé
Phù, cuối cùng cũng xong, hi vọng các bạn làm theo dễ dàng và có thể chạy được luôn
Bạn thấy đấy cách viết này sinh ra rất nhiều class nhưng mình hoàn toàn có thể thay thế dễ dàng vì đã inject interface và bindding trong CoreServiceProvider -> Ngoài ra nếu decorator để tạo ra 1 layer cache là hoàn toàn khả thi trong tầm tay rồi (bạn xem lại bài post trước để biết cách decorator cache nhé)
Nhưng, như mình đã nói ở đầu, mục tiêu của mình là plugin and play -> các component ở đây cũng tương đối linh hoạt rồi, nhưng nếu mình muốn đem toàn bộ BookCRUD này sang 1 hệ thống khác thì sẽ làm thế nào? Sẽ phải copy:
- Toàn bộ thư mục core
- BooksController
- Book model
- Requests
- Views
- Copy route
@@ Quá nhiều thứ, làm cách nào 1 phát ăn luôn nhỉ ^^ -> Câu trả lời của mình là biến BookCRUD này thành 1 package và install / remove thông qua composer -> Đó chính là nội dung của bài sau và cũng là bài cuối cùng của series này, mong các bạn sẽ tiếp tục đồng hành. Xin cảm ơn
À mà quên: code mình đã push lên git rồi nhé. Bạn clone về và chạy composer install, tạo file .env, tạo database mới và migrate là sẽ chạy được nhé 🙂 https://github.com/trthanhbk/BookCRUD
All rights reserved
Bình luận
thanks god
Thank bạn đã ủng hộ
Bài viết rất hay. thanks tác giả!
Thank bạn
Anh ơi, cho em hỏi bind vào Provider làm gì vậy anh, em thấy để vậy vẫn hoạt động được mà, nó có tốt hơn chỗ nào không anh?
Để tăng tính linh hoạt em nhé. Thoạt nhìn thì phiền phức hơn nhưng nó sẽ giúp ích cho việc maintain về sau, kiểu như: thêm tính năng hoặc thay đổi tính năng mà không phải động vào code cũ
cảm ơn bài viết hay và demo rất dễ làm!
Bạn ơi cho mình hỏi, mình đang dùng i5-repository tại sao không call thẳng repository trong controller mà phải inject qua service ạ?
Bài viết rất hay và chi tiết. Cảm ơn tác giả
Cho em hỏi là CURD trong core như vậy có tác dụng gì ạ? Và core này thường dùng cho chức năng gì?