# 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)。