--- 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吧!