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處理
是不是變得有夠簡單好寫呢?

