Controller Model Repository 搭配Dto的快速開發架構
前言:
在開發系統的這段期間,我們寫了N支API,然而每個操作的動作都離不開Create, Update, Read, Delete 這些動作
為了加速以及更懶惰(誤),我們團隊討論出一個夠嚴謹也可以更快開發的架構
結構說明:
Controller : 作爲Api的入口也作為整個資料流程的控管,其他的事情都讓Service、Repository來處理
Model : 定義資料表內容、定義資料驗證規則、定義表與表之間的relation
Repository : 作為資料操作的動作定義,直接控制Model做資料存取
Dto們 : 作為資料傳遞的介面,嚴格定義傳入Controller或者傳入Service的資料結構,這樣做可以一定程度的避免錯誤
一切的開端:
當每個API都需要Create, Update, Read, Delete這些方法的時候,我們第一個想到的是
那就寫個抽象類別來繼承好了,這樣就可以把需要的方法都先定義好,大約可以解決6成以上重複性的原始碼
但是面臨的就是下一個問題,舉例來說每一次的Create如果沒有特別需要處理資料那麼都是從Controller中呼叫Reposiroty的Create function就結束了
這樣的話,是不是可以再寫的更簡單一些?
加速原則 :
相同的命名原則,讓開發可以更快
舉個簡單的例子先,如果我們要對Customer做Create,也就是新增一筆客戶資料
那麼資料流應該會長這樣子
1 |
CustomerController -> CustomerRepository(Create function) -> Customer Model (write data) |
因此我們寫了一個BaseController,在其中定義好repository_name, model_name 以及Dto
而且預設就是跟Controller相同的名字,整理一下上述案例,在CustomerController中,我們只要繼承了BaseController就會擁有以下
1 2 3 |
CustomerRepository CustomerModel CreateCustomerDto <--- 以Create作為範例的話 |
為了要在Controller一初始化就把以上物件準備好,我們會需要用上之前提過的callAction
我們在BaseController中先定義幾件事情
1 2 3 4 5 6 |
private string $__model_root__ = 'App\\Models\\'; //存放Model的路徑 private string $__dto_root__ = 'App\\Core\\Repository\\'; //存放Dto的路徑 private BaseModel|Authenticatable $__main_model__; //預設跟Controller同名的Model private ?string $__dto__; //需要用上的Dto 名字 private Repository $repo; //預設跟Contrller同名的Repository private string $name; //預設就是Controller的名字 |
接著定義幾個init_開頭的function,用來初始化以上的變數內容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
protected function init_start(): static { $this->name = str_replace('Controller', '', class_basename(static::class)); return $this; } protected function init_db_source(): static { if (class_exists($this->__model_root__.$this->name)) { $this->__main_model__ = new ($this->__model_root__.$this->name); $this->repo = $this->repository($this->name); } return $this; } protected function init_dto(): static { $this->__dto__ = match ($this->get_method()) { 'create' => "{$this->__dto_root__}{$this->name}\\Dtos\\Create{$this->name}Dto", 'update' => "{$this->__dto_root__}{$this->name}\\Dtos\\Update{$this->name}Dto", 'read_list' => ReadListParamsDto::class, default => Null }; return $this; } |
簡單說明一下這幾個function,分別是一句Controller的名稱初始化name , __main_model__ , repo , __dto__ 等等這幾個變數
接著在我們覆寫掉callAction這個function,讓以上幾個function可以正確的執行過一次
1 2 3 4 5 6 7 8 9 10 11 12 |
public function callAction($method, $parameters) { foreach (get_class_methods($this) as $method) { if (str_starts_with($method, 'init_')) { $this->{$method} } } return parent::callAction($method, $parameters); } |
以上這幾行,就是簡單的把init_開頭的function找到,然後透過動態呼叫全部執行一次
那Repository做什麼用?
Repository作為直接操作Model進行資料存取的元件,我們也是做了一層抽象處理,這樣這樣一來基本的Create, Update, Delete, Read都可以描述完成
每個各別的Repository繼承後只要描述關於relation的部分就可以了,大致像這樣子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public function create(Dto $params, ?Model $model=Null, ?array $relation=Null): Model { $payload = $params->to_array(); $relations = $relation !== Null ? $relation : $this->__create_relations__; $model = $model ? new ($model) : $this->__model__; try{ DB::beginTransaction(); $instance = $model->create($payload); $this->_relation_processor($instance, $payload, $relations); $instance->load($relations); DB::commit(); return $instance; }catch (Throwable $throwable) { DB::rollBack(); throw new Exception("{$model}新增錯誤: {$throwable->getMessage()}"); } } |
舉Create的例子來說,透過定義好的Dto傳入需要寫入的正確資料,然後讓relation_processor處理相關聯表單所需要寫入的資料
這麼一來,我們回到CustomerController的Create function中,就只剩下以下內容
1 2 3 4 |
public function create(Request $request): Model { return $this->repo->create(new $this->__dto__(...$request->json())); } |
邏輯上是把傳入值塞入CreateCustomerDto中,如果這時候傳入的資料不是預期的欄位就會直接報錯
接著將new出來的CreateCustomerDto交給CustomerRepository的Create function處理
是不是變得有夠簡單好寫呢?