---
tags: PHP / Laravel / Lumen
---
# Laravel CRUD實作(下)
### Model
在上一篇的內容,有提到migration的建立與資料遷移。現在我們打開app\Entities\Post.php,在model的class內新增欄位,並給予欄位批量賦值的修飾,同時建立 user 方法,使 Post 能夠與 User 進行關聯:
```php
class Post extends Model
{
protected $fillable = [
'user_id', 'title', 'content',
];
public function user()
{
return $this->belongsTo('App\Entities\User');
}
}
```
app\Entities\User 新增關聯:
```php
public function posts()
{
return $this->hasMany('App\Entities\Post');
}
```
* 批量賦值:model可利用「create」方法將array型態的資料進行模型(實體)的新增,也就是說,$fillable 內都是允許「被填入值」的欄位
※當然,model內還有許多對欄位的屬性設定,比如指定model對應的資料表 (若未指定,會預設指向對應名稱的資料表,如:Post→posts),或者修改主鍵
```php
protected $table = 'table_name';
protected $primaryKey = 'key-name';
```
### View、Controller、Route的緊密相連
為何會一起講解畫面、路由與 controller 呢?因為當你指定某個 URI 來呼叫頁面或 API 時,路由檔會先檢查該 URI 是否被定義,有被定義的話則會執行該路由所指定 controller 的 function;若沒定義就會報錯。例如
* Route
```php
// 透過GET方法,導向「/test」,執行TestController的test function,並回傳某個畫面
Route::get('test', 'TestController@test');
```
* Controller
```php
// 進入test function後,回傳「resources\views\test.blade.php」的頁面
class TestController extends Controller
{
public function test()
{
return view('test');
}
}
```
### 建立控制器 (Controller)
```
$ php artisan make:controller PostController --resource
```
※「--resource」是透過 Artisan 為 controller 建立所有符合 RESTful API 風格的 function
### 建立視圖
為了避免views資料夾因為頁面太多而雜亂,因此先在resources\views底下建立「post」資料夾,並在post資料夾內建立「index.blade.php」、「show.blade.php」、「create.blade.php」、「edit.blade.php」一共4個檔案,分別用來顯示文章列表、顯示單一文章、建立文章以及編輯文章
### 建立會員系統與畫面
```
$ php artisan make:auth
```
1. 這個指令會為我們建立 Laravel 內建的會員驗證功能(Auth),並且讓它先幫我們建立一個共同的layout。此時你會看到resources\views多了一個layouts資料夾,裡面有個「app.blade.php」,這檔案能夠當作許多頁面的「主視圖 (master page)」,也就是當作其他子視圖的共用模板,讓子視圖繼承它,這樣可以省下些許前端設計的麻煩。
2. 副檔名「.blade.php」是讓這個檔案能使用Laravel的Blade模板,它能使畫面的開發上更加便利,且不需使用「<?php」的標籤
### 註冊使用者
來到 [http://localhost:8000/register](http://localhost:8000/register) 註冊一個會員,便於後續操作
### 定義路由並套用 auth 中介層
routes\web.php 新增以下程式碼
```php
Route::group(['middleware' => ['auth:web']], function () {
Route::resource('post', 'PostController');
});
```
1. 利用Route的resource方法,依據PostController內的每個function,各自建立一支符合RESTful API的路由 (GET、POST、PUT/PATCH、DELETE),並以post作為路由前綴修飾,如:/post/create
2. 可在command輸入「php artisan route:list」查看路由表,可以看見resource幫我們建立了所有function對應的路由
3. 路由表可查看所有路由的 URI 與 name
4. 我們讓post的所有路由,都經過auth的中介層來驗證身分是否合法 (是否登入),若未登入則被導向到登入畫面
### 建立 Repository
接著在**app**底下建立一個「**Repositories**」資料夾,並在**Repositories**資料夾中新增「**PostRepository.php**」,而這個檔案的用意為何呢?因為以往都會在 model 中撰寫 query 的邏輯,但現在我們將處理資料庫的邏輯,都交給了 Repository,這樣能夠使 controller 的程式碼更簡潔,而且避免因為過多 scope 而造成 model 的肥大。而上篇也有提到 entity 的部分,就是將 model 拆分為 entity 與 repository,分別進行資料表實體定義、資料存取邏輯的處理。
app\Repositories\PostRepository.php:
```php
<?php
namespace App\Repositories;
use App\Entities\Post;
class PostRepository
{
}
```
* namespace為宣告該檔案的命名空間,切記請勿打錯,否則會發生找不到檔案的錯誤
### CRUD step1 - 文章列表&單筆文章顯示
編輯 app\Http\Controllers\PostController.php
```php
<?php
namespace App\Http\Controllers;
use App\Repositories\PostRepository;
class PostController extends Controller
{
protected $postRepo;
public function __construct(PostRepository $postRepo)
{
$this->postRepo = $postRepo;
}
public function index()
{
$posts = $this->postRepo->index();
return view('post.index', ['posts' => $posts]);
}
// ...略
public function show($id)
{
$post = $this->postRepo->find($id);
if (!$post) {
return redirect()->route('post.index');
}
return view('post.show', ['post' => $post]);
}
}
```
* controller中,將PostRepository引入
* 利用protected定義$postRepo的屬性,讓這支 controller 能夠透過該屬性來調用PostRepository
* 「__construct」為 controller 類別的「建構函數」,意即這支 controller 的初始狀態,同時將PostRepository實例化為 postRepo 的物件,並指定給controller的postRepo屬性
* 「$this->postRepo->index()」為調用repository的index方法
* index function列出所有文章,並夾帶資料渲染回前端,讓前端得以使用blade進行處理
* show function是藉由路由傳入的id,找出該id的文章,並回傳給show頁面,若查無此文章則重新導向至文章列表
修改 PostRepository.php
```php
<?php
namespace App\Repositories;
use App\Entities\Post;
class PostRepository
{
public function index()
{
return Post::with('user')->get();
}
public function find($id)
{
return Post::with('user')->find($id);
}
}
```
* 利用 Post 取出文章,並將結果 return 給 controller
* find 是透過「主鍵」尋找資料
編輯index.blade.php:
```htmlmixed=
@extends('layouts.app')
@section('content')
<div class="container">
<div align="center">
<a href="{{ route('post.create') }}" class="btn btn-primary">我要貼文</a>
</div>
@foreach ($posts as $key => $post)
<div class="card text-center">
<div class="card-header">
標題:{{ $post->title }}
</div>
<div class="card-body">
<h5 class="card-title">
作者:{{ $post->user->name }}
</h5>
<a href="{{ route('post.show', $post->id) }}" class="btn btn-secondary">查看文章</a>
</div>
<div class="card-footer text-muted">
發文日期:{{ $post->created_at }}
</div>
</div><br>
@endforeach
</div>
@endsection
```
* route('post.create') 與 view 的post.create不同!前者是導向由resource建立的「/post/create」,後者是回傳「post」資料夾的create頁面,請勿搞混
* extends是讓index.blade.php繼承layouts\app.blade.php的模板
* section是自定義的一個區塊,這邊將內容都放在content這區塊中,而在app.blade.php中,可藉由「@yield('content')」呼叫所有繼承它的子視圖中,名叫「content」的區塊
* 利用blade的@foreach,顯示controller回傳的datas變數中,各筆資料的所有欄位
編輯 show.blade.php
```htmlmixed=
@extends('layouts.app')
@section('content')
<div class="container">
<div class="card text-center">
<div class="card-header">
標題:{{ $post->title }}
</div>
<div class="card-body">
<h5 class="card-title">
作者:{{ $post->user->name }}
</h5>
<p class="card-text">
{{ $post->content }}
</p>
</div>
<div class="card-footer text-muted">
發文日期:{{ $post->created_at }}
</div>
<div align="center">
<a href="{{ route('post.edit', $post->id) }}" class="btn btn-primary">編輯</a>
<form action="{{ route('post.destroy', $post->id) }}" method="POST">
@csrf
@method('DELETE')
<button class="btn btn-danger">刪除</button>
</form>
</div>
</div>
</div>
@endsection
```
* $data->user->name 為透過 Post model 關聯到 User model,取得發文者姓名
* 使用html的form元素,導向至刪除文章的路由
* @csrf 為夾帶CSRF Token,讓後端得知該請求並非偽造請求
* @method('DELETE') 是夾帶DELETE方法,發送一個 DELETE 方法的 HTTP Request
* form元素的method屬性,僅支援GET、POST方法
### CRUD step2 - 文章刪除
修改PostController.php的destroy function
```php
public function destroy($id)
{
$result = $this->postRepo->delete($id);
if ($result) {
return redirect()->route('post.index');
}
return back();
}
```
* 藉由路由傳入文章id,並調用repository的delete方法進行刪除,最後重新導向至文章列表
於 PostRepository.php 新增 delete 方法
```php
public function delete($id)
{
return Post::destroy($id);
}
```
* destroy為透過主鍵刪除該文章
### CRUD step3 - 新增文章
編輯 create.blade.php
```htmlmixed=
@extends('layouts.app')
@section('content')
<div class="container">
<form action="{{ route('post.store') }}" method="POST">
@csrf
<div class="form-group row">
<label for="title" class="col-sm-1 col-form-label">標題</label>
<div class="col-sm-4">
<input type="text" class="form-control" id="title" name="title" placeholder="標題">
</div>
</div>
<div class="form-group row">
<label for="content" class="col-sm-1 col-form-label">內容</label>
<div class="col-sm-4">
<textarea class="form-control" id="content" name="content">
</textarea>
</div>
</div>
<div class="form-group row">
<div class="col-sm-10">
<input type="submit" class="btn btn-primary" value="送出">
</div>
</div>
</form>
</div>
@endsection
```
* 藉由POST發送請求給伺服器
* 元素中的name屬性,用於讓後端辨識參數名稱
修改PostController的 create 及 store function
```php
public function create()
{
return view('post.create');
}
public function store()
{
$post = $this->postRepo->create(request()->only('title', 'content'));
if ($post) {
return redirect()->route('post.show', $post->id);
}
return back();
}
```
* 回傳view\post的create頁面
* store function中,將前端傳送的值由Request類別接收,並調用repository的create方法進行文章新增,最後重新導向至該文章的畫面
* $request 物件的 only 方法為限定只接收部分參數,此作法係避免接收到不必要或具有攻擊性的參數
於PostRepository建立create方法
```php
public function create(array $data)
{
return auth()->user()->posts()->create($data);
}
```
* auth()->user() 為取得目前登入者的資訊 (User model),並使用建立好的「posts」關聯到 Post model,建立一筆文章,這樣就可以透過關聯的方式直接在 posts 資料表存入 user_id (發文者id)
* Model 的 create 方法所回傳的是一個模型,而update與destroy回傳的是true與false
### CRUD step4 - 編輯文章
修改PostController的edit 與 update function
```php
public function edit($id)
{
$post = $this->postRepo->find($id);
if (!$post) {
return redirect()->route('post.index');
}
return view('post.edit', ['post' => $post]);
}
public function update($id)
{
$result = $this->postRepo->update($id, request()->only('title', 'content'));
if (!$result) {
return redirect()->route('post.index');
}
return redirect()->route('post.show', $id);
}
```
* edit function藉由路由傳入的id尋找文章,並回傳給edit頁面
* update function接收前端的值,並調用repository的update方法進行文章的更新,若更新失敗則跳轉至文章列表,若更新成功則跳轉至該文章頁面
於PostRepository建立update方法
```php
public function update($id, array $data)
{
$post = Post::find($id);
return $post ? $post->update($data) : false;
}
```
* 若查無此文章,則回傳false,否則進行資料更新
編輯 edit.blade.php
```htmlmixed=
@extends('layouts.app')
@section('content')
<div class="container">
<form action="{{ route('post.update', $post->id) }}" method="POST">
@csrf
@method('PUT')
<div class="form-group row">
<label for="title" class="col-sm-1 col-form-label">標題</label>
<div class="col-sm-4">
<input type="text" class="form-control" id="title" name="title" value="{{ $post->title }}">
</div>
</div>
<div class="form-group row">
<label for="content" class="col-sm-1 col-form-label">內容</label>
<div class="col-sm-4">
<textarea class="form-control" id="content" name="content">{{ $post->content }}</textarea>
</div>
</div>
<div class="form-group row">
<div class="col-sm-10">
<input type="submit" class="btn btn-primary" value="送出">
</div>
</div>
</form>
</div>
@endsection
```
* 透過form導向至文章更新的路由
* @method('PUT') 讓事件夾帶PUT方法傳送表單請求
* PUT/PATCH都是更新,但PUT是完整更新,PATCH是做部分更新
在URL輸入[http://localhost:8000/post/](http://localhost:8000/post/)至文章列表,測試剛剛建立的CRUD吧!