1020 views
# CRUD in Laravel -- Resource Controllers ###### tags: `MCL` `web` References ~ * https://en.wikipedia.org/wiki/Create,_read,_update_and_delete * https://laravel.com/docs/5.8/controllers#resource-controllers * https://scotch.io/tutorials/simple-laravel-crud-with-resource-controllers ## CRUD * 建立 Create * 讀取 Read * 更新 Update * 刪除 Delete 四個動作合併稱之為 CRUD,通常在 SQL 或是 API 做某一資源存取的時候會遇到。 ### Wiki 上的格式 在 Wiki 上有針對常見的平台,他們處理 CRUD 的時候會分別對應什麼動作: | Operation | SQL | HTTP | RESTful WS | DDS | MongoDB | | ---------------- | ------ | ------------------ | ---------- | ----------- | ------- | | Create | INSERT | PUT / POST | POST | write | Insert | | Read (Retrieve) | SELECT | GET | GET | read / take | Find | | Update (Modify) | UPDATE | PUT / POST / PATCH | PUT | write | Update | | Delete (Destroy) | DELETE | DELETE | DELETE | dispose | Remove | ### Laravel 上的格式及其常見用法 在 Laravel 上,也有一個統一的方式呈現一個資源在不同動作時,對應的 HTTP 方法以及動作函數: | Verb | URI | Action | Route Name | 功能 | | ------------- | ---------------------- | ------- | -------------- | ---------- | | GET | `/photos` | index | photos.index | 列表 | | GET | `/photos/create` | create | photos.create | 列新增表單 | | **POST** | `/photos` | store | photos.store | 列新增 | | GET | `/photos/{photo}` | show | photos.show | 列詳細視圖 | | GET | `/photos/{photo}/edit` | edit | photos.edit | 列編輯表單 | | **PUT/PATCH** | `/photos/{photo}` | update | photos.update | 列更新 | | **DELETE** | `/photos/{photo}` | destroy | photos.destroy | 列刪除 | 你可以選擇不使用這個標準規範,不過 Laravel 有提供簡單的步驟去實現這些行為。 > #### PUT 與 PATCH 的「定義」 > * PUT 的意思接近 Replace (Create or Update),意思是說須包含列的所有資訊 > * PATCH 可以被設計成只傳送部分資料,在這個特性下會比 PUT 省網路資源 > > 定義是絕對的,但應用是彈性的;或許在某些框架下沒有滿足絕對定義,但或許這樣反而比較好用。 > > 比如 Laravel 的 update 還是可以理解成 PUT 那樣,但是去掉 Create 這個選項。 ## Resource controller 在 Laravel 中會把 CURD 行為包裝成一個獨立的 Controller 使用 (Resource Controller),我們可以用以下指令新增一個 Resource Controller: ```bash php artisan make:controller CheckRecordController --resource --model=CheckRecord ``` 此時他就會建立一個空的 Controller,但是裡面函數已經幫我們開好了: ```php= <?php namespace App\Http\Controllers; use App\CheckRecord; use Illuminate\Http\Request; class CheckRecordController extends Controller { public function index() {} public function create() {} public function store(Request $request) {} public function show(CheckRecord $checkRecord) {} public function edit(CheckRecord $checkRecord) {} public function update(Request $request, CheckRecord $checkRecord) {} public function destroy(CheckRecord $checkRecord) {} } ``` 接著再把路由加進 `routes/web.php`: ```php= <?php // 因為我們想要抓使用者 ID,所以加上 auth 中介層保護 Route::group(['middleware' => 'auth'], function () { Route::resource('checkRecords', 'CheckRecordController'); // 就是這麼簡單 }); ``` 這樣就完成路由對應到 Controller 的接口了。 同時因為 Laravel Resource 總共有四種 GET 視圖,所以我們可以事先把這部分的視圖建立起來: * `pages.check-record.index` * `pages.check-record.create` * `pages.check-record.show` * `pages.check-record.edit` 回顧上週我們新增的資源為「簽到資料 `App\CheckRecord`」: | 欄位名稱 | 型態 | 備註 | | ------------ | --------------- | --------------------------------------------------- | | `id` | bigIncrements | 系統會自動記數 (視為這筆資料的唯一識別碼,很重要!!) | | `user_id` | unsignedInteger | 對應使用者 index | | `check_time` | dateTime | 這個有到秒哦 | | `comment` | string | 如果想儲存換行要使用 `text` | 接著我們就是依照上面的規格,以及我們的資源格式,一一把缺空的實作部分填起來。 ### [GET] checkRecords.index 這個路由功能在於顯示所有的列資料,因此在控制器的動作是取得要顯示的列資料,並伴隨視圖回傳 HTML 結果。 * 留意到因為要顯示所有列資料,所以在某些實作中不會一口氣顯示所有欄位。 * 舉例來說資源是公告時,你可能只會想列出標題,而不顯示公告內容。 ```php= // CheckRecordController public function index() { $checkRecords = CheckRecord::all(); return view('pages.check-record.index', compact('checkRecords')); } ``` ```htmlmixed= {{-- pages.check-record.index --}} <style> th, td { border: 1px solid #ccc; } button { background: none!important; border: none; padding: 0!important; color: rgb(0, 0, 238); text-decoration: underline; cursor: pointer; font-size: 16px; } </style> <a href="{{ route('checkRecords.create') }}">Create</a> <table> <thead> <th>Id</th> <th>UserId</th> <th>CheckTime</th> <th>Comment</th> <th>CreatedAt</th> <th>UpdatedAt</th> <th>Action</th> </thead> <tbody> @foreach ($checkRecords as $record) <tr> <td>{{ $record->id }}</td> <td>{{ $record->user_id }}</td> <td>{{ $record->check_time }}</td> <td>{{ $record->comment }}</td> <td>{{ $record->created_at }}</td> <td>{{ $record->updated_at }}</td> <td> <a href="{{ route('checkRecords.show', $record->id) }}">Show</a> <a href="{{ route('checkRecords.edit', $record->id) }}">Edit</a> <form action="{{ route('checkRecords.destroy', $record->id) }}" method="POST"> @csrf @method('DELETE') <button type="submit">Destroy</button> </form> </td> </tr> @endforeach </tbody> </table> ``` ### [GET] checkRecords.create 這個路由是顯示「新增列資料」的表單,所以在控制器的動作也是回傳視圖即可。 * 視圖中的表單 `form` 內必須嵌入 CSRF token `@csrf`,否則 Laravel 不會接受 POST/PUT/DELETE 等類型的請求。 * 當後端驗證失敗的時候,Laravel 會把失敗資訊放到 `$message`,並會觸發 `@error` 使得裡面的內容呈現出來。 * 在驗證失敗的時候,`old('comment')` 會等於使用者原先輸入的資訊,你可以放在 `input` 標籤中的 `value` 屬性,這樣使用者就不需要重新輸入該欄位內容。 * 驗證邏輯會在 [checkRecords.store](#POST-checkRecordsstore) 處理。 ```php= // CheckRecordController public function create() { return view('pages.check-record.create'); } ``` ```htmlmixed= {{-- pages.check-record.create --}} <form action="{{ route('checkRecords.store') }}" method="post"> @csrf <label for="comment">Comment</label> <input type="text" name="comment" value="{{ old('comment') }}"> @error('comment') <div style="color: red;">{{ $message }}</div> @enderror <br> <button type="submit">Submit</button> </form> ``` ### [POST] checkRecords.store 這個路由的功能是接收自使用者送來的資訊,這裡的動作是驗證資料格式無誤後,新增一個列資料後,最後把使用者重導向到 checkRecords.index 去。 * `$this->validate` 就是驗證函數,第一個參數放請求資料 `$request`,第二個參數放驗證邏輯。 * 驗證邏輯中,key 是欄位的名字;value 是邏輯寫法,如果是字串形式的話會以 `|` 當作分隔符,每個子字串代表一種邏輯。 * 在這裡的例子就會是 `required`、`string`、`min:5` 三個邏輯。 * 關於驗證邏輯的更多用法可以參考[官方文件的說明](https://laravel.com/docs/5.8/validation#available-validation-rules) * `$this->validate($request, [...])` 等價於 `$request->validate([...])` ```php= // CheckRecordController public function store(Request $request) { $data = $this->validate($request, [ 'comment' => 'required|string|min:5' ]); $checkRecord = new CheckRecord; // do not forget to put `use Auth;` in front of the file $checkRecord->user_id = Auth::user()->id; $checkRecord->check_time = now(); $checkRecord->comment = $data['comment']; $checkRecord->save(); return redirect()->to(route('checkRecords.index')); } ``` ### [GET] checkRecords.show 如 index 處所言,不會顯示所有列的資訊,而如果想要處理單一列詳細資料的顯示問題,會使用 `show`。 > 當作業囉 ### [GET] checkRecords.edit 這個路由是顯示「編輯列資料」的表單,所以在控制器的動作會是伴隨該列的資料回傳視圖。 * 根據規格所述,發送更新請求的 HTTP 方法是 PUT,但是前端表單只能送出 GET 跟 POST 兩種,所以在 Laravel 的折衷方法是在表單裡面嵌入 `method('PUT')`,這樣儘管送出去的時候是 POST 方法,但 Laravel 會把他視為 PUT 方法處理。 ```php= // CheckRecordController public function edit(CheckRecord $checkRecord) { return view('pages.check-record.edit', compact('checkRecord')); } ``` ```htmlmixed= {{-- pages.check-record.edit --}} <form action="{{ route('checkRecords.update', $checkRecord->id) }}" method="post"> @method('PUT') @csrf <input type="hidden" name="id" value="{{ $checkRecord->id }}"> <label for="comment">Check Time</label> <div>{{ $checkRecord->check_time }}</div> <label for="comment">Comment</label> <input type="text" name="comment" value="{{ $checkRecord->comment }}"> @error('comment') <div style="color: red;">{{ $message }}</div> @enderror <br> <button type="submit">Submit</button> </form> ``` ### [PUT] checkRecords.update 這個路由的功能是接收自使用者送來的資訊,這裡的動作是驗證資料格式無誤後,更新該列資料後,最後同樣把使用者重導向到 checkRecords.index 去。 ```php= // CheckRecordController public function update(Request $request, CheckRecord $checkRecord) { $data = $this->validate($request, [ 'comment' => 'required|string|min:5' ]); $checkRecord->comment = $data['comment']; $checkRecord->save(); return redirect()->to(route('checkRecords.index')); } ``` ### [DELETE] checkRecords.destroy 這個路由的功能是把使用者請求的該筆資料刪除,所以就把資料刪掉後並重導向到 checkRecords.index 去就可以了。 * 你可以注意在 `pages.check-record.index` 視圖中,因為此請求為 DELETE 方法,所以同樣的邏輯,開啟一個 `form`,裡面嵌入 `@method('DELETE')`。 ```php= // CheckRecordController public function destroy(CheckRecord $checkRecord) { $checkRecord->delete(); return redirect()->to(route('checkRecords.index')); } ``` 關於更多 Resource controller 的細節可以參考[官方文件的說明](https://laravel.com/docs/5.8/controllers#resource-controllers)。