Kiến trúc hệ thống trên Laravel – phần 9

Các bài viết trong series

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.

alt

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.

alt

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