php – Laravel 安裝與逐步教學
透過 Composer 安裝
先安裝 composer
1 2 3 4 5 6 |
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" php composer-setup.php php -r "unlink('composer-setup.php');" sudo mv composer.phar /usr/local/bin/composer |
1 2 3 |
composer create-project --prefer-dist laravel/laravel blog |
可以在本機建立服務器,例如訪問 http://localhost:8000 會直接進入 blog 路徑
1 2 3 4 |
cd blog <-若開新路徑記得進入底下 php artisan serve |
API
如果要直接查閱 Laravel 的 Class 內部有哪些方法,可以參考官方 API Document 查詢你要的版本非常方便。官方 Laravel 手冊並沒有列出所有可用的方法,需要自行查找。
基礎教學
Facades 表面
若喜歡使用 Laravel 靜態的代理方法,例如使用 View::share() 替代 view()->share(),那麼必須要使用命名空間 Illuminate\Support\Facades。好處是看起來簡潔、難忘,Facades 提供許多的靜態方法可以到這裡查看
1 2 3 4 |
Illuminate\Support\Facades\View Illuminate\Support\Facades\Cache |
Routing 路由
預設 routes/web.php 是註冊給 web 訪問的路由,這些範例可以更快理解用法。
使用 GET 請求 domain/foo:
1 2 3 4 5 |
Route::get('foo', function () { return 'Hello World'; }); |
強制或選用參數傳遞:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Route::get('user/{id}', function ($id) { return 'User '.$id; }); Route::get('posts/{post}/comments/{comment}', function ($postId, $commentId) { // }); Route::get('user/{name?}', function ($name = null) { return $name; }); |
使用 GET 請求 domain/user 讀取 UserController 控制器的 index() 方法:
1 2 3 |
Route::get('/user', 'UserController@index'); |
下面這些都是回覆 HTTP 的動作:
1 2 3 4 5 6 7 8 |
Route::get($uri, $callback); Route::post($uri, $callback); Route::put($uri, $callback); Route::patch($uri, $callback); Route::delete($uri, $callback); Route::options($uri, $callback); |
同時指定多個方法到一個路由:
1 2 3 4 5 |
Route::match(['get', 'post'], '/', function () { // }); |
任何HTTP動作都對應到一個路由:
1 2 3 4 5 |
Route::any('foo', function () { // }); |
前綴用法,例如 admin/ 底下的路由
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
Route::prefix('admin')->group(function (){ // url: admin/users Route::get('users', function (){ return 'users'; }); // url: admin/products Route::get('products', function (){ return 'products'; }); }); |
Middleware 中介層
這是在程序進入 routing 之間的邏輯層,例如驗證 CSRF 跨站請求偽造就可以在這裡處理。
新增中介層
例如我要新增一個新的 app/Http/Middleware/CheckAge.php,然後在 handle() 寫入我要的年齡判斷年齡小於 200 回到首頁,否則通過。那麼可以使用 artisan 新增
1 2 3 |
php artisan make:middleware CheckAge |
1 2 3 4 5 6 7 8 9 10 |
public function handle($request, Closure $next) { if ($request->age <= 200) { return redirect('home'); } return $next($request); } |
註冊中介層
預先註冊在 app/Http/Kernel.php 看是要
- $middleware 全域
- $middlewareGroups 路由群組
- $routeMiddleware 特定路由時觸發
一旦註冊,就可以在路由中使用。
使用全名
1 2 3 4 5 6 7 |
use App\Http\Middleware\CheckAge; Route::get('admin/profile', function () { // })->middleware(CheckAge::class); |
使用別名
1 2 3 4 5 6 7 |
//app/Http/Kernel.php protected $routeMiddleware = [ // ...... 'checkage' => \App\Http\Middleware\CheckAge::class, ]; |
1 2 3 4 5 6 |
// routes/web.php Route::get('admin/profile', function () { // })->middleware('checkage'); |
我們可以在 app/Http/Kernel.php 看到有哪些是預設註冊的中介層。
CSRF Protection 跨站請求偽造
預設已經在 app/Http/Kernel.php 的屬性 $middlewareGroups[‘web’] 中註冊了,所以會自動從 session 中驗證。若要排除的網址可以添加在 app/Http/Middleware/VerifyCsrfToken.php
在 form 添加可以使用 Blade 提供的 @csrf 指示
1 2 3 4 5 6 |
<form method="POST" action="/profile"> @csrf ... </form> |
透過 AJAX 的方法
通常我們在前端都已經使用 AJAX 發送 CRUD 請求了,所以我們需要夾帶在 headers 表頭傳送,這樣 Laravel 在中介層會透過 VerifyCsrfToken 檢查表頭中一個叫做 X-CSRF-TOKEN 的值。我們可以這樣製作,在視圖中添加
1 2 3 |
<meta name="csrf-token" content="{{ csrf_token() }}"> |
接著指示 jQuery 取得 csrf-token 並附加到 headers
1 2 3 4 5 6 7 |
$.ajaxSetup({ headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') } }); |
如此一來就能夠簡單的保護 CSRF 攻擊。
Controllers 控制器
新增一般控制器
1 2 3 |
php artisan make:controller UserController |
路由的參數也會對應到 show() 的 $id
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// app/Http/Controllers/UserController.php <?php namespace APP\Http\Controllers; use App\User; use App\Http\Controllers\Controller; class UserController extends Controller { // 我們可以這麼寫 public function show($id) { return reponse('Hello World'); // 直接輸出 // return view('user.profile', ['user' => User::findOrFail($id)]); // 使用 view } } |
這邊有趣的是,如果把 show($id) 改成 show(User $user),讓路由傳遞過來的 $id 改成由 App\User 接收,Laravel 會自動幫你把使用者搜尋出來喔!可以省掉自己寫 User::find($id) 的動作。
將路由指定控制器
1 2 3 4 |
// routes/web.php Route::get('/user/{id}', 'UserController@show'); |
控制器路徑底下的控制器,命名空間要注意預設的是 App\Http\Controllers。以下範例的路由 /foo 會讀取 App\Http\Controllers\Photos\AdminController.php。
1 2 3 4 |
// 路徑底下的寫法,foo 對應到實際的命名空間是 Route::get('/foo', 'Photos\AdminController@method'); |
若要使用 middleware 中介層
1 2 3 |
Route::get('profile', 'UserController@show')->middleware('auth'); |
也可以在 Controller 中的 __construct() 使用分配哪些方法可以使用,哪些方法不可已使用,例如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class UserController extends Controller { public function __construct() { // 所有 method 都使用 $this->middleware('auth'); // 只有 index() 使用 $this->middleware('log')->only('index'); // 除了 store() 以外都可以使用 $this->middleware('subscribed')->except('store'); } } |
新增 CRUD 控制器
1 2 3 |
php artisan make:controller PhotoController --resource |
- index() 顯示列表的資源
- create() 填寫的表單頁
- store(Request $request) 將數據儲存,也就是新增
- show($id) 顯示特定的資源
- edit($id) 編輯特定的資源表單
- update(Request $request, $id) 更新已經存在的特定資源
- destroy($id) 刪除特定資源
接著附加聰明的路由給 routes/web.php,這樣使用 API 呼叫 POST/GET/PUT/PATCH/DELETE 的時候將能自動對應。不過要注意,需要寫入的動作如 POST/PUT/DELETE 對應接收的方法,都會進行驗證 CSRF,所以測試的時候一定也要把 @CSRF 帶入表單。
1 2 3 |
Route::resource('photos', 'PhotoController'); |
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 |
Request 請求
若想從路由指定並帶入到控制器,如下例:路由 user 後方第一個參數是 id,但因為在控制器中第一個參數是依賴注入的 $request 所以會跳過,直接對應到第二個參數 $id
1 2 3 4 |
// routes/web.php Route::put('user/{id}', 'UserController@update'); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<?php // app/Http/Controllers/UserController.php namespace App\Http\Controllers; use Illuminate\Http\Request; <- 務必添加 class UserController extends Controller { // 依賴注入 Request 類別 public function update(Request $request, $id) { // } } |
input 會自動修剪 / (TrimStrings) 與 轉換無文字為 null (ConvertEmptyStringsToNull),若要禁用可以到 App\Http\Kernel 移除預設,參考。如果路由也打算直接取得 request 可以這樣寫,一樣 $request 透過依賴入入的方式實現
1 2 3 4 5 6 7 |
use Illuminate\Http\Request; Route::get('/', function (Request $request) { // }); |
一些可以使用的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
$request->input('name', 'Sally') // 對應 HTTP 動作而取得的參數,參數二是預設值 $request->input('products.0.name'); // 陣列 0 取出 name $request->input('products.*.name'); // 所有陣列的 name $request->name // 同 $request->input('name') // 只取得 Query String, 用法同 input。會先從 Query String 取得,若不存在會從路由參數取得 $request->query('name'); $request->all() // 所有 input 資料 $request->path() // 如 http://domain.com/foo/bar 會得到 foo/bar $request->is('admin/*') // 判斷是否在該 path 底下 $request->url() // 沒有 Query String $request->fullUrl() // 包含 Query String $request->method() // 取得 HTTP 請求的方法如 get/post/put... $request->isMethod('post') // 判斷 HTTP 請求方法 $request->has('name') // 判斷質是否存在 $request->has(['name', 'email']) $request->filled('name') // 不讓當下存在的請求值為 empty 的時候可以使用填充 $request->flash() // 一次使用的 input $request->old('title') // 取得前一筆表單的快閃數據 |
若要操作 Request 的 header 與 body 可以參考我。
表單重新填寫用法
當我們驗證失敗即將返回前一頁要求使用者重新填寫的時候,可以配合 withInput()
1 2 3 4 5 6 7 8 9 |
// 返回表單並夾帶快閃的使用者剛輸入的資料 return redirect('form')->withInput(); // 可以排除密碼 return redirect('form')->withInput( $request->except('password') ); |
然後在重新填寫表單的控制器中,使用
1 2 3 |
$username = $request->old('username'); |
快閃取回剛剛填寫的欄位值。
JSON
如果來源請求 header 的 Content-Type 是 application/json,如 jQuery 的 $.post,那麼可以用 . 的方式來挖掘數據
1 2 3 |
$request->input('user.name'); |
過濾
1 2 3 4 5 6 7 8 |
$input = $request->only(['username', 'password']); $input = $request->only('username', 'password'); $input = $request->except(['credit_card']); $input = $request->except('credit_card'); $request->flashOnly(['username', 'email']); $request->flashExcept('password'); |
若要使用 PSR-7 Requests 的請求則需要額外安裝,參考官網。
Cookies 餅乾
1. 分離使用,透過靜態方法 (個人較喜歡
1 2 3 4 5 6 7 8 9 10 11 12 |
public function index(Request $request) { $minutes = 1; // 寫入 \Cookie::queue('name', 'Jason', $minutes); // 取得 echo \Cookie::get('name'); } |
2. 附加在 reponse(),透過實體化 Request
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public function index(Request $request) { $minutes = 1; // 取得 echo $request->cookie('name'); // 寫入 return response('Hello World')->cookie( 'name', 'Jason', $minutes ); } |
File 文件
主要是處理 $_FILE 的工具
1 2 3 4 |
$file = $request->file('photo'); $file = $request->photo; |
上傳檔案
驗證檔案有效後,儲存到 storage/app/images/filename.png
1 2 3 4 5 |
if ($request->file('photo')->isValid()) { $request->photo->storeAs('images', 'filename.png'); } |
如果要取得檔案相關資訊的化可以查看 file() 相關的方法,這是 Laravel 繼承使 Symfony的功能,前往看文件。例如
1 2 3 4 |
$request->file('photo')->hashName(); // 雜湊名稱,通常存入我會用這個作為檔名 $request->file('photo')->getClientOriginalName(); // 取得用戶端的檔名 |
通常我們上傳圖檔公開瀏覽,Laravel 有預設 storage/app/public 是用來公開訪問的儲存空間,那我們則改指定路徑:
1 2 3 4 |
$filename = $request->file('uploadPhoto')->hashName(); $path = $request->file('photo')->storeAs('public/images', $filename); |
下 artisan 創建一個軟連結,讓 public/storage 連結到 storage/app/public,參考
1 2 3 |
php artisan storage:link |
這樣我們可以用 asset() 做顯示圖片訪問了
1 2 3 |
echo asset("storage/images/{$filename}"); |
Reponse 回覆
簡單用法,直接在 controllers 或 routes 中使用 return 作為回覆數據。
1 2 3 4 5 6 7 8 9 10 |
Route::get('/', function () { // 回覆文字 return 'Hello World'; // 或陣列 return [1, 2, 3]; }); |
Redirect 重新導向
導向路徑
1 2 3 4 5 6 7 |
// 內部 return redirect('home/dashboard'); // 外部 return redirect()->away('https://www.google.com'); |
導向上一頁並夾帶 input 參數
例如驗證錯誤表單的時候,會把值放到一次性的備存
1 2 3 4 5 6 7 |
// 若驗證失敗 return back()->withInput(); // 返回頁可以這樣取得剛剛得填寫資料 $username = $request->old('username'); |
導向被命名的路由
1 2 3 |
return redirect()->route('profile', ['id' => 1]); |
導向控制器動作
1 2 3 4 5 |
return redirect()->action( 'UserController@profile', ['id' => 1] ); |
導向並夾帶快閃數據
這通常用在如 “新增成功” 的訊息
1 2 3 4 5 |
return redirect() ->action('ProductsController@create') ->with('message', '新增成功'); |
導向到的視圖可以這麼處理
1 2 3 4 5 6 7 |
@if (session('message')) <div class="alert alert-success"> {{ session('message') }} </div> @endif |
JSON
1 2 3 4 5 6 |
return response()->json([ 'name' => 'Abigail', 'state' => 'CA' ]); |
JSONP
1 2 3 4 5 |
return response() ->json(['name' => 'Abigail', 'state' => 'CA']) ->withCallback($request->input('callback')); |
File Downloads 文件下載
1 2 3 4 5 6 7 8 9 10 |
// 要下載的文件路徑 return response()->download($pathToFile); // 可選用下載的檔案名稱,或是檔頭 return response()->download($pathToFile, $name, $headers); // 下載後可以刪除 return response()->download($pathToFile)->deleteFileAfterSend(true); |
Streamed Downloads 下載資料流
有時候我們會需要下載 echo 的檔案
1 2 3 4 5 |
return response()->streamDownload(function () { echo "Hello World"; }, 'laravel-readme.md'); |
File Responses 文件回覆
可以直接在瀏覽器顯示文件而不會啟動下載,例如 PDF
1 2 3 |
return response()->file($pathToFile); |
Views 視圖
單張視圖的參數
1 2 3 4 5 6 7 8 9 10 11 12 |
public function create(Request $request) { $params = [ 'base' => $request->root(), 'name' => 'Jason' ]; // 讀取 resources/views/photos/create.blade.php return view('photos.create', $params); } |
也可以使用這種方法在其他區域為 view 檔定數據,例如使用 view composer 的時候(下方會介紹)。
1 2 3 |
return view('greeting')->with('name', 'Jason'); |
共用視圖的參數
可以提供給所有視圖都使用這項參數。適合放置的地方在 app/Providers/AppServiceProvider.php
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<?php namespace App\Providers; use Illuminate\Support\Facades\View; class AppServiceProvider extends ServiceProvider { public function boot() { View::share('key', 'value'); } } |
Blade 刀片
傳遞到試圖的參數,基本上我們可以透過 {{ $data }} 來顯示並自動過濾XSS攻擊,如果不希望過濾XSS可以使用如
1 2 3 |
Hello, {!! $name !!}. |
其實 blade 也只是把 {{ }} 符號轉換成
1 2 3 |
<?php echo e($test); ?> |
{{ }} 內也可以使用純 PHP 語言如
1 2 3 |
The current UNIX timestamp is {{ time() }}. |
另外邏輯與指令通常都會加入前綴 ‘@’ 來表示。基本上邏輯判斷的寫法跟 PHP 類似,所有用法就去官網看這裡不贅述。建立表單常用的:
1 2 3 4 5 6 |
<form action="/foo/bar" method="POST"> @method('PUT') <- 發送非 POST/GET 的 HTTP 請求動作 @csrf <- CSRF保護 </form> |
當然 Blade 可以組合不同分割的視圖,我們快速示範這個例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 透過指令建立控制器 php artisan make:controller ProductsController --resource // routes/web.php 設定路由 Route::resource('products', 'ProductsController'); // ProductsController.php public function create() { // 注意我們顯視的是子視圖 return view('products.create_child'); } |
兩張視圖的部分設計如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<!-- resources/views/products/create.blade.php --> <h1> App Name - @yield('title') </h1> @section('sidebar') <p>這裡放置 sidebar</p> @show <div class="container"> @yield('content') </div> |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<!-- resources/views/products/create_child.blade.php --> @extends('products.create') @section('title', '標題') @section('sidebar') @parent <p>因為 parent 的關係,這段文字合併追加在 create 的 sidebar</p> @endsection @section('content') <p>這是內容</p> @endsection |
我們發現這兩個指令
- @yield() – 產生:提供給 @section 產生的位置
- @section() – 部分:實作內容模塊,這些內容會提供給 @yield() 產生
也就是說 「section() ——— 顯示到 ———> yield()」,查看網址後會看到合併後的結果
1 2 3 4 5 6 7 8 |
<h1>App Name - 標題</h1> <p>這裡放置 sidebar</p> <p>因為 parent 的關係,這段文字合併追加在 create 的 sidebar</p> <div class="container"> <p>這是內容</p> </div> |
Service Inject 注入服務
如果要在 blade 內使用類別,例如我們常見要處理字串、if else 顯示不同區塊、依照各國顯示不同的日期格式等視覺。可以這麼使用
1 2 3 4 5 6 7 |
@inject('metrics', 'App\Services\MetricsService') <div> Monthly Revenue: {{ $metrics->monthlyRevenue() }}. </div> |
- @inject(‘接收的變數’, ‘類別名稱’)
一些工具
要截斷文字,通常用來顯示描述可以使用
1 2 3 |
Illuminate\Support\Str::words(string $value, int $words = 100, string $end = '...') |
View Composer 視圖作曲家
View composers are callbacks or class methods that are called when a view is rendered. If you have data that you want to be bound to a view each time that view is rendered, a view composer can help you organize that logic into a single location.
-Laravel Document
一開始有點難以理解,其實就是說:若每次渲染該視圖時都要綁定參數,那麼我們可以把這個綁定的邏輯獨立出來。應用情況例如後台管理介面,<header> 都會顯示登入後的會員名字。這與 view::share() 的差別在於
- view::share() 只是分享這個參數到所有 view 都可以使用
- View Composer 指定讀取哪個視圖的時候運作自訂的邏輯
兩者可以處理共用視圖的方式類似,看你偏好如何處理囉。提供簡單快速的範例來實作 Contact 聯絡我們的介面:
- 建立控制器:使用 artisan 來產生 app/Http/Controllers/ContactController.php
123php artisan make:controller ContactController --resource
- 指定路由: routes/web.php,自動對應 CRUD
123Route::resource('contact', 'ContactController');
測試 http://localhost:8000/contact 應該能正確讀取方法 ContactController::index()
- 添加視圖
我們先指定 ContactController.php 要載入的視圖並帶入參數12345678public function index(){return view('contact.index', ['name' => 'Jason']);}新增 resources/views/contact/index.blade.php 並顯示參數值
123456789101112131415<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Contact</title></head><body><h1>聯絡我們</h1><p>Hi, {{ $name }}</p></body></html> - 建立 service provider 服務提供者 ComposerServiceProvider:
手動添加 app/Providers/ComposerServiceProvider.php,參考
12345678910111213<?phpnamespace App\Providers;use Illuminate\Support\ServiceProvider;class ComposerServiceProvider extends ServiceProvider{public function boot(){//}} - 加入設定:config/app.php,讓系統運作的時候會啟用 ComposerServiceProvider
123456'providers' => [//...App\Providers\ComposerServiceProvider::class,],
瀏覽器重新整理就會觸發上面 boot(),所以我們要添加邏輯:當讀取視圖 contact/sendtype.blade.php 會調用 App\Http\ViewComposers\SendtypeComposer 類別
12345678910111213141516<?phpnamespace App\Providers;use Illuminate\Support\ServiceProvider;use Illuminate\Support\Facades\View;class ComposerServiceProvider extends ServiceProvider{public function boot(){View::composer('contact.sendtype', 'App\Http\ViewComposers\SendtypeComposer');}}我們會看到 View::composer() ,這還有兩種寫法可以使用
1234567891011// 指定多個 view 調用View::composer(['profile', 'dashboard'],'App\Http\ViewComposers\MyViewComposer');// 所有 view 都調用匿名函式View::composer('*', function ($view) {//}); - 建立被調用的類別:app/Http/ViewComposers/SendtypeComposer.php,並提供視圖所需要的參數。我們透過 $view->with() 的方法來片段增加。
12345678910111213141516171819202122<?phpnamespace App\Http\ViewComposers;use Illuminate\View\View;class SendtypeComposer{public function __construct(){//}public function compose(View $view){$view->with('types', ['客服中心','資訊部門','設計部門',]);}}
這時候規則都建立好了,不過還無法看到實際效果,所以我們要建立 ComposerServiceProvider 所提到的視圖 sendtype.blade.php
- 建立用 View Compoer 抽離的視圖
1234567<select name="type">@foreach ($types as $key => $type)<option value="{{$key}}">{{ $type }}</option>@endforeach</select>
- 讓視圖 index.blade.php 透過模板語言載入視圖 sendtype.blade.php
我們修改 resources/views/contact/index.blade.php 如下,利用 blade 指令的 @include(),接著重新整理畫面就能看到包含 contact/sendtype.blade.php 與帶入參數的介面了。123456789101112131415161718<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Contact</title></head><body><h1>聯絡我們</h1><p>Hi, {{ $name }}</p><form action="">@include('contact.sendtype')</form></body></html>
也就是說透過 View Composer 可以把共用的視圖獨立出具有邏輯行為的試圖,這意味著若打算在其他視圖中使用 contact/sendtype.blade.php 並渲染數據,我們只要透過 @include(‘contact.sendtype’) 插入即可。
URL Generation 網址生成
1 2 3 4 5 6 |
use Illuminate\Support\Facades\URL; URL::current(); URL::full(); URL::previous(); |
也可以透過 url helper 直接使用
1 2 3 4 5 6 7 8 |
$base = url('/'); // base url $url = url('user/profile'); $url = url('user/profile', [1]); $current = url()->current(); $full = url()->full(); $previous = url()->previous(); |
URLs For Named Routes 替網址命名
幫 routes 命名,可以不必耦合到實際的路由。當我們實際的路由發生改變,就不需要更動調用的路由函式。
1 2 3 4 5 |
Route::get('/article/{id}', function ($id) { })->name('article.show'); |
1 2 3 4 |
echo route('article.show', ['post' => 1]); // http://localhost:8000/article/1 |
Signed URLs 簽署網址
產生一個有時效性的網址,這會通過簽署來認證合法性。通常用在發送 E-mail 給客戶用來申請忘記密碼、或是退訂訂單確認的時候。以下範例
路由先定義好否則會報錯
1 2 3 4 5 6 7 8 9 10 11 12 13 |
use Illuminate\Http\Request; Route::get('/unsubscribe/{user}', function (Request $request) { if (!$request->hasValidSignature()) { abort(401, '簽章錯誤'); } return response('簽章通過'); })->name('unsubscribe'); |
接著不經過控制器直接路由示範
1 2 3 4 5 6 7 8 |
Route::get('/test', function () { return URL::temporarySignedRoute( 'unsubscribe', now()->addMinutes(30), ['user' => 1] ); }); |
打開 http://localhost:8000/test 會看到一串網址,我們貼到網址去,成功的話就會出現簽章通過。
如果要在 form 表單中夾帶簽章參數,拋送到 Laravel 的時候可以透過中間層去驗證,可以簡化一些程式碼。
1 2 3 4 5 6 7 |
// app/Http/Kernel.php // 5.6 版已經預先載入到中間層了 protected $routeMiddleware = [ 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, ]; |
路由的部分,會需要透過中間層去認證簽署,我們在命名路由之後,緊接著使用 middleware()
1 2 3 4 5 |
Route::post('/unsubscribe/{user}', function (Request $request) { // ... })->name('unsubscribe')->middleware('signed'); |
URLs For Controller Actions 控制器操作網址
對應到控制器的路由,如果控制器使用 resource 的方式對應CRUD,那麼網址也會自動轉換,必填的路由參數字段,也會強制要帶入。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Route::get('/test', function () { dd([ 'index()' => action('ProductsController@index'), 'create()' => action('ProductsController@create'), 'show()' => action('ProductsController@show', ['id' => 123]), 'edit()' => action('ProductsController@edit', ['id' => 123]), 'update()' => action('ProductsController@update', ['id' => 123]), 'destroy()' => action('ProductsController@destroy', ['id' => 123]), ]); }); |
1 2 3 4 5 6 7 8 9 10 11 |
// 輸出 array:6 [▼ "index()" => "http://localhost:8000/products" "create()" => "http://localhost:8000/products/create" "show()" => "http://localhost:8000/products/123" "edit()" => "http://localhost:8000/products/123/edit" "update()" => "http://localhost:8000/products/123" "destroy()" => "http://localhost:8000/products/123" ] |
在視圖表單的時候也可以這麼使用
1 2 3 |
<form method="post" action="{{ action('ProductsController@store') }}"> |
Session 會話
這個就不陌生了,當然建議直接使用 Laravel 提供的,因為包含了自動加密。使用如
1 2 3 4 5 6 7 |
use Illuminate\Http\Request; public function index(Request $request) { $request->session; } |
不過我推薦使用全域函式。
新增/修改
1 2 3 |
session(['name' => 'Kelly']); |
提取
1 2 3 4 5 6 |
// 不存在有預設值 session('name', 'default'); // 取得所有 session()->all(); |
拉出並刪除
1 2 3 |
session()->pull('name', 'default'); |
刪除
1 2 3 |
session()->forget('name'); |
清空所有
1 2 3 |
session()->flush(); |
Validation 驗證
暫時空著,太多囉
Error Handling 錯誤處理
報告紀錄而不會渲染錯誤頁面
1 2 3 4 5 6 7 8 9 |
try { // Validate the value... } catch (Exception $e) { report($e); return false; } |
HTTP Exceptions – HTTP 例外處理
1 2 3 |
abort(403, 'Unauthorized action.'); |
Logging 紀錄
紀錄預設會在 storage/logs/laravel.log ,相關設定檔可以查閱 config/logging.php。有這八個級別可以用
1 2 3 4 5 6 7 8 9 10 |
Log::emergency($message); Log::alert($message); Log::critical($message); Log::error($message); Log::warning($message); Log::notice($message); Log::info($message); Log::debug($message); |
可以記錄陣列
1 2 3 4 5 6 |
Log::info($message, [ 'id' => 2, 'name' => 'Jason' ]); |
Encryption 加密
生成隨機的鑰匙,添加到 config/app.php 的選用參數 key
1 2 3 |
php artisan key:generate |
1 2 3 4 5 6 7 |
use Illuminate\Support\Facades\Crypt; $encrypted = Crypt::encryptString('Hello world.'); // 加密 $decrypted = Crypt::decryptString($encrypted); // 解密 |
Hash 哈希
單向雜湊,適合用在儲存密碼
1 2 3 4 |
use Illuminate\Support\Facades\Hash; Hash::make($password); |
Database 資料庫連接
config/database.php 設定如 mysql 的帳號密碼,預設使用 Laravel Homestead,但是我們不用所已把參數替換成如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
'mysql' => [ 'driver' => 'mysql', 'host' => 'localhost', 'port' => '3306', 'database' => 'laravel', 'username' => 'root', 'password' => '', 'unix_socket' => '', 'charset' => 'utf8mb4', 'collation' => 'utf8mb4_unicode_ci', 'prefix' => '', 'strict' => true, 'engine' => null, ] |
Query Builder 查詢產生器
幾乎能處理絕大多數的 SQL 語句。ORM 的內部也是使用 Query Builder,我們可以快速透過熟悉的SQL語言直接做溝通。
1 2 3 |
$users = DB::table('users')->select('name', 'email as user_email')->get(); |
1 2 3 4 5 6 |
DB::table('users')->insert([ ['email' => 'taylor@example.com', 'votes' => 0], ['email' => 'dayle@example.com', 'votes' => 0] ]); |
1 2 3 4 5 |
DB::table('users') ->where('id', 1) ->update(['options->enabled' => true]); |
1 2 3 |
DB::table('users')->where('votes', '>', 100)->delete(); |
如果需要關聯查詢
1 2 3 4 5 |
$users = DB::table('users') ->leftJoin('posts', 'users.id', '=', 'posts.user_id') ->get(); |
其他詳細的看 官方介紹。
Eloquent ORM 付於表現的物件關聯對映
內部的基礎指令是透過 Query Build,我們這裡介紹 ORM 操作。Laravel 模型類別名稱使用單數。以下示範,我們先透過 artisan 建立
1 2 3 |
php artisan make:model Product |
開啟 app/Product.php,預設會有以下事情,若要修改預設可以參考官方
- 對應複數資料表名稱,並使用下滑線連接單字
- 主鍵預設 id 且為整數,會自動遞增
- 時間戳記預設啟用 Y-m-d H:i:s,所以資料表須要 created_at 與 updated_at 欄位
- 資料庫連接使用設定檔,如果要連到額外的資料庫則須修改
1 2 3 4 5 6 7 8 9 10 11 |
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Product extends Model { // } |
上方式基本上欲設規則了,但很多時候我們的資料庫可能是沿用過去,欄位設計不一定符合 Laravel 預設,因此我們可以手動修改例如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Product extends Model { const CREATED_AT = 'pro_created_at'; const UPDATED_AT = 'pro_updated_at'; protected $connection = 'mysql_custom'; protected $table = 'project'; protected $primaryKey = "pro_id"; } |
當然還有其他參數可以設定,可參考。接著我們就能在任何地方調用 CRUD 的相關操作囉
1 2 3 |
use App\Product; |
1 2 3 |
$products = Product::all(); |
ORM/Query Build 在預設情況下,幫我們處理了 SQL Injection 攻擊。常用的 CRUD 指令都非常多,建議到官網去看。以下介紹基本
新增
1 2 3 4 5 6 7 |
$product = new Product; $product->title = $request->input('title'); $product->price = $request->input('price'); $product->save(); echo $product->id; // 取得新增的編號 |
查詢
1 2 3 |
Product::where('id', '>', '2')->get(); |
1 2 3 4 5 6 7 8 |
Product::select(['id', 'title']) // 可以多個欄位 ->where('id', '>', '2') ->orderBy('id') ->skip(10) // 也可用 offset() ->take(5) // 也可用 limit() ->get(); |
我們常常需要找不到資料來可以獲取 Exception 列外來顯示找不到頁面,可以這樣用
1 2 3 |
Product::where('id', '=', 100)->firstOrFail(); |
修改
1 2 3 4 5 6 |
$product = Product::find(53); $product->title = $request->input('title') . " - sale"; $product->price = DB::raw('price * 0.7'); // 也就是 price = price * 0.7 $product->save(); |
實際刪除
這個刪除會真的從資料表中刪除。
1 2 3 |
Product::where('id', 52)->delete(); |
如果僅希望虛擬的刪除 (Soft Delete 軟刪除) 那要使用下方介紹
軟刪除
並不是真的刪除數據,而是透過改變欄位 deleted_at 來判斷刪除。所以資料表一定要有這個欄位,Model 也須要添加 trait,例如
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<?php namespace App; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; // 使用命名空間 class Product extends Model { use SoftDeletes; // 使用 trait protected $dates = ['deleted_at']; // 須要添加 } |
同時實際刪除的方法,但從資料表中會看到欄位 deleted_at 出現了時間戳記。
1 2 3 |
Product::where('id', 52)->delete(); |
當我們從 Laravel 的模型中取出資料表資料,這筆資料就不會存在了,當然也包含任何的計算數量與查詢。
使用原始文字
有時候我們會用到原生的資料庫方法,這時候就搭配使用 DB::raw(),可以免去自動加引號。例如
1 2 3 4 5 6 |
$product = new Product; $product->title = $request->input('title'); $product->timestamp = DB::raw('NOW()'); $product->save(); |
自訂類別
因為使用 psr-4 標準,我們在 composer.json 可以看到已經定義在 app/ 底下
1 2 3 4 5 6 7 |
"autoload": { "psr-4": { "App\\": "app/" } }, |
所以我們自訂的類別可以放在 app/ 底下的任何地方,只要符合命名空間與路境的對應就好。有了 psr-4 會很方便,因為我們從命名空間就能知道類別視放置在哪裡。例如我有個購物車類別
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// app/Jason/Cart.php namespace APP\Jason; class Cart { public function get() { return 'Apple'; } } |
控制器或路由就直接使用即可
1 2 3 4 |
use App\Jason\Cart; echo (new Cart)->get(); |
Pagination 分頁
主要有兩個方法
- paginate() 包含計算所有數量與各個分頁
- simplePaginate() 只有上下頁,所以無法取得總數量與每個分頁
分頁的 HTML 結構與 CSS 樣式,會與 Bootstrap 前端元件函視庫 (front-end component library) 相符合,例如控制器或路由中指定需要每頁 5 筆
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// 自動分配數據與分頁連結 $products = DB::table('products')->paginate(5); // 如果資料庫使用 OPM 的話 Product::paginate(5); // 若要自訂連結 $products->withPath('custom/url'); return view('products.index', [ 'products' => $products ]); |
視圖
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<div class="container"> <ul> @foreach ($products as $product) <li> <a href="">{{ $product->id }} . {{ $product->title }}</a> </li> @endforeach </ul> </div> {{ $products->links() }} |
追加 Query String
1 2 3 |
{{ $products->appends(['sort' => 'votes'])->links() }} // custom/url?sort=votes&page=3 |
添加錨點
1 2 3 |
{{ $products->fragment('foo')->links() }} // custom/url?page=3#foo |
手動製作
通常我們會配合 Model 自動分頁,但有時我們希望自己切割陣列或物件來製作。參考使用 Illuminate\Pagination\LengthAwarePaginator,實例化(參考)要夾帶參數如
1 2 3 4 |
use Illuminate\Pagination\LengthAwarePaginator as Paginator; $paginator = new Paginator(mixed $items, int $total, int $perPage, int|null $currentPage = null, array $options = []); |
- items (mixed) 頁分頁的項目如陣列 (或物件)
- total (int) 總數量
- perPage (int) 每頁多少筆
- currentPage (int|null) 當前頁數
- options (array)
- path,可以用 $request->url()
- query,可以用 $request->query()
- fragment
- pageName
返回 JSON
當透過使用者端請求 JSON 格式,那會自動返回 total, current_page, last_page 的分頁參數,以及 data 的實際結果。
自訂分頁視圖
因為預設使用 Bootstrap 套件,如果我們不使用它而打算自定義視圖的話,可以寫
1 2 3 4 5 6 |
{{ $paginator->links('view.name') }} // 還可以帶入參數 {{ $paginator->links('view.name', ['foo' => 'bar']) }} |
當然我們可以透過 artisan 產生並從中修改已經定義好的視圖,會更方便
1 2 3 |
php artisan vendor:publish --tag=laravel-pagination |
會在 resources/views/vendor/pagination/ 看到預設的視圖模板。
指定預設分頁視圖
如果不使用 Bootstrap 但要套用到整個系統的預設值,可以在 blog/app/Providers/AppServiceProvider.php 設定
1 2 3 4 5 6 7 8 9 |
use Illuminate\Pagination\Paginator; public function boot() { Paginator::defaultView('pagination::semantic-ui'); Paginator::defaultSimpleView('pagination::default'); } |
例如 pagination::semantic-ui 代表位於 resources/views/vendor/pagination/semantic-ui.blade.php
其他操作方法
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$results->count() $results->currentPage() $results->firstItem() $results->hasMorePages() $results->lastItem() $results->lastPage() (不可用在 simplePaginate) $results->nextPageUrl() $results->perPage() $results->previousPageUrl() $results->total() (不可用在 simplePaginate) $results->url($page) |
TESTING 測試
- 定義在 phpunit.xml。
- 可以增加 .env.testing 環境設定,當使用 artisan 添加選用參數 —env=testing 可以覆蓋掉 .env 的設定。
- 路徑 tests 看到兩個路徑
- Feature:測試較大型的程式碼,通常是不同對象的交互運用,甚至是 HTTP 請求。
- Unit:專注於測試較小的程式碼,通常是單一 method。
1 2 3 4 5 6 7 |
// 建立在 Feature 路徑 php artisan make:test UserTest // 建立在 Unit 路徑 php artisan make:test UserTest --unit |
看 tests/Unit/ExampleTest.php 這個方法 assertTrue() 是用來斷言為真
1 2 3 4 5 6 |
public function testBasicTest() { $this->assertTrue(true); } |
接著我們下指令測試,官方是說用 phpunit ,不過在 windows 要這麼使用
1 2 3 |
.\vendor\bin\phpunit |
Linux 下使用
1 2 3 |
vendor/bin/phpunit |
(如果要看到概要可以這麼用)
1 2 3 |
.\vendor\bin\phpunit --testdox |
接著我們可以看到測試結果
1 2 3 4 5 6 7 8 9 |
PHPUnit 7.3.1 by Sebastian Bergmann and contributors. .. 2 / 2 (100%) Time: 240 ms, Memory: 10.00MB OK (2 tests, 2 assertions) |
如果我們修改成 $this->assertTrue(false); 因為會是錯誤的結果,那麼會得到這樣
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
PHPUnit 7.3.1 by Sebastian Bergmann and contributors. F. 2 / 2 (100%) Time: 233 ms, Memory: 10.00MB There was 1 failure: 1) Tests\Unit\ExampleTest::testBasicTest Failed asserting that false is true. C:\www\laravel\tests\Unit\ExampleTest.php:17 FAILURES! Tests: 2, Assertions: 2, Failures: 1. |
各種斷言的方法,可以在 PHPUnit 文件中找到。
完整的測試範例教學可以參考這篇。
Database Migrations 資料庫喬遷
1 2 3 |
php artisan make:migration create_users_table |
Authentication 認證
使用 artisan 在 app/Http/Controllers/Auth 自動建立註冊、登入、重設密碼、忘記密碼四個控制器。
1 2 3 |
php artisan make:auth |
接著要建立資料表 users,但因為路徑 database/migrations 已經預設了喬遷資料庫的紀錄,我們只需要啟用來建立預設的使用者資料表。
1 2 3 |
php artisan migrate |
如果下了指令後看到報錯
1 2 3 |
Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes |
那麼只要在 app/Providers/AppServiceProvider.php 加入
1 2 3 4 5 6 7 8 |
use Illuminate\Support\Facades\Schema; public function boot() { Schema::defaultStringLength(191); } |
然後去資料庫把 migrations, users 資料表刪除,重新下 artisan 指令即可。成功建立後,前台頁面就可以嘗試註冊使用者並登入。
重新導向
認證成功會自動導向 /home 若要修改,可以修改控制器的屬性如
1 2 3 |
protected $redirectTo = '/'; |
這個屬性可以在 LoginController.php, RegisterController.php, ResetPasswordController.php 中找到。另外還要到 app/Http/Middleware/RedirectIfAuthenticated.php 修改如
1 2 3 4 5 6 7 8 |
public function handle($request, Closure $next, $guard = null) { // ... return redirect('/'); // ... } |
自訂使用者儲存資料
修改 form 表單後,在 app/Http/Controllers/Auth/RegisterController.php 有兩個方法
- validator()
- 可以自行添加要驗證的
- create()
- 這是使用 Eloquent ORM 新增,可自行添加要寫入的
取得認證成功的使用者資料
通過認證,不管在哪裡我們都可以用簡單的方法來取得
1 2 3 4 5 6 7 8 9 10 |
use Illuminate\Support\Facades\Auth; // 取得當前認證的使用者 $user = Auth::user(); $email = Auth::user()->email; // 取得當前認證的使用者編號 $id = Auth::id(); |
當然也可以在控制器中使用 Request
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; class ProfileController extends Controller { public function update(Request $request) { // 認證過的使用者實例 $request->user(); } } |
確定當前用戶是否認證
通常這會在 Middleware 中介層做好認證後才訪問某些控制器。
1 2 3 4 5 6 7 |
use Illuminate\Support\Facades\Auth; if (Auth::check()) { //... } |
訪問需要使用者認證
可以在路由使用
1 2 3 4 5 |
Route::get('profile', function () { // 只有認證過的使用者才能進入,否則導向到登入頁 })->middleware('auth'); |
或是在控制器添加
1 2 3 4 5 6 |
public function __construct() { $this->middleware('auth'); } |
重新導向未經授權的用戶
當 auth 中介層偵測到未經授權的用戶,將返回 JSON 401,或者如果不是 AJAX 請求,將重新導向用戶到登陸名為 route。app/Exceptions/Handler.php 添加
1 2 3 4 5 6 7 8 9 10 |
use Illuminate\Auth\AuthenticationException; protected function unauthenticated($request, AuthenticationException $exception) { return $request->expectsJson() ? response()->json(['message' => $exception->getMessage()], 401) : redirect()->guest(route('login')); } |
指定一名警衛
將中間層 auth 附加到路由時,還可以指定要使用哪個警衛來驗證用戶。
指定的 guard 應該對應到 auth.php 配置文件中 guard 的鍵
1 2 3 4 5 6 |
public function __construct() { $this->middleware('auth:api'); } |
登入限制
LoginController 預設多次登入失敗,將會暫停登入1分鐘。定義在 Illuminate\Foundation\Auth\ThrottlesLogins.php。
手動登入
密碼我們不用 Hash 後到資料表中比對,因為 Laravel 會幫我們處理好。
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 |
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; class LoginController extends Controller { public function create() { return view('login.create'); } // 這是比對使用 public function authenticate(Request $request) { // 從請求參數中取得 email 與 password,email 可以換成其他如 username $credentials = $request->only('email', 'password'); // 在資料表中嘗試比對 if (Auth::attempt($credentials)) { // 前往儀表板... return redirect()->intended('dashboard'); } } } |
如果要附加一些條件,可以這麼寫
1 2 3 4 5 |
if (Auth::attempt(['email' => $email, 'password' => $password, 'active' => 1])) { // 使用者處於活動狀態且存在,不是暫停使用 } |
訪問特定的 Guard 警衛實例
下面例子,對應到 config/auth.php 中的陣列 guards。
1 2 3 4 5 |
if (Auth::guard('admin')->attempt($credentials)) { // } |
登出
1 2 3 |
Auth::logout(); |
記住使用者
登入後,通常我們會記住使用者,那麼就給予第二個參數布林值 true
1 2 3 4 5 |
if (Auth::attempt(['email' => $email, 'password' => $password], true)) { // } |
我們可以透過這樣來確認已經被記住了
1 2 3 4 5 |
if (Auth::viaRemember()) { // } |
將取得的使用者登入
若我們透過 Model 取得使用者,打算將他登入
1 2 3 4 5 |
$user = User::find(1); Auth::login($user); Auth::login($user, true); // 記住 |
也可以使用 guard 警衛
1 2 3 |
Auth::guard('admin')->login($user); |
也可以直接使用主鍵
1 2 3 4 5 6 |
Auth::loginUsingId(1); // 登入並記住 Auth::loginUsingId(1, true); |
若要單次請求被記住,不會使用 session, cookie
1 2 3 4 5 |
if (Auth::once($credentials)) { // } |
Authorization 授權
通常混和 Gates 與 Policies 兩個方法,可以想像是 Routes 與 Controllers 的角色。通常決定要使用 Gates 或是 Policies 可以這樣想:
- Gates 適用在無任何模型或資源,例如查看儀錶版。
- Policies 適用在想要為特定模型或資源做授權。
Gates 大門
Gates 寫在 App\Providers\AuthServiceProvider:
1 2 3 4 5 6 7 8 9 10 |
public function boot() { $this->registerPolicies(); Gate::define('update-post', function ($user, $post) { return $user->id == $post->user_id; }); } |
或是使用 Class@method 樣式,那麼會自動對應到 Policies (下個段落介紹)
1 2 3 |
Gate::define('update-post', 'App\Policies\PostPolicy@update'); |
當然也可以使用 resource 方法,如同在 Routes 路由自動定義 Controllers 一樣
1 2 3 4 5 6 7 8 9 10 |
Gate::resource('post', 'App\Policies\PostPolicy'); // 上方的寫法也等同於手動定義下方這些 Gate::define('post.view', 'App\Policies\PostPolicy@view'); Gate::define('post.create', 'App\Policies\PostPolicy@create'); Gate::define('post.update', 'App\Policies\PostPolicy@update'); Gate::define('post.delete', 'App\Policies\PostPolicy@delete'); |
如果要複寫功能的話,可以透過第三個參數,鍵代表功能,值代表方法
1 2 3 4 5 6 |
Gate::resource('post', 'App\Policies\PostPolicy', [ 'image' => 'updateImage', 'photo' => 'updatePhoto', ]); |
上面代表的意思就是
- post.image 對應 App\Policies\PostPolicy@updateImage
- post.photo 對應 App\Policies\PostPolicy@updatePhoto
Authorizing Actions 授權行為
授權行為必須透過 Gate,要注意這個範例因為在 defined() 有使用 $user,但在此處我們不需要傳入使用者到這個方法,Laravel 會自動帶入到 Gate 的閉包。
1 2 3 4 5 6 7 8 9 |
if (Gate::allows('update-post', $post)) { // 使用者可以更新發佈 } if (Gate::denies('update-post', $post)) { // 使用者不可以更新發佈 } |
不過如果想要明確指定哪個使用者的話,可以這麼寫
1 2 3 4 5 6 7 8 9 |
if (Gate::forUser($user)->allows('update-post', $post)) { // 可更新 } if (Gate::forUser($user)->denies('update-post', $post)) { // 不可更新 } |
範例,當身分不是 super 的時候不能訪問儀錶版:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
use Illuminate\Support\Facades\Gate; use App\User; class DashboardController extends Controller { public function index() { if (!Gate::allows('super', User::class)) { abort(403, '您沒有這個權限'); } return 'Hello Dashboard'; } } |
Intercepting Gate Checks 攔截門檢查
(待補上)
Creating Policies 建立政策
有了 Gate 以後我們要產生政策,Policies 政策是一個包圍在特定的 Model 或是資源之外的類別,例如我們有 Blog 應用程式,會有 PostModel 發佈模型,那麼就有一個相對應的 PostPolicy 發佈政策,用來授權使用者新增或修改發佈。
透過 artisan 建立,可以選擇是否使用 model 來自動產生 CRUD 政策的方法
1 2 3 4 5 |
php artisan make:policy PostPolicy // 或 php artisan make:policy PostPolicy --model=Post |
Registering Policies 註冊政策
一旦政策存在,我們就要註冊它。只要到 app/Providers/AuthServiceProvider.php 找到屬性 policies 陣列填入即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<?php namespace App\Providers; use App\Post; use App\Policies\PostPolicy; use Illuminate\Support\Facades\Gate; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; class AuthServiceProvider extends ServiceProvider { protected $policies = [ Post::class => PostPolicy::class, ]; // } |
Writing Policies 編撰政策
app/Policies/PostPolicy.php 在 create() 通常如新增文章,不太需要 Post Model 的寫法,update() 注入兩個模型 User 與 Post。方法會返回布林值,true 代表授權成功,false 會跳出未授權的結果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<?php namespace App\Policies; use App\User; use App\Post; class PostPolicy { public function create(User $user) { // } public function update(User $user, Post $post) { return $user->id === $post->user_id; } } |
Authorizing Actions Using Policies 使用政策的授權行為
下面會配合以 Post Model 為說明範例,有 4 種方式:
1. 透過 User Model 使用者模型
假設寫在控制器中,找會員編號 2 是否有授權,可使用 can() 或 cant()。因為上面已經把 Policy 政策註冊在 app/Providers/AuthServiceProvider.php 所以這兩個方法可以使用。
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 |
namespace App\Http\Controllers; use App\User; use App\Post; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; class PostController extends Controller { public function update(Request $request, Post $post) { $user = User::find(2); if ($user->can('update', $post)) { echo "有授權"; } if ($user->cant('update', $post)) { echo "未授權"; } } } |
不須使用 Post Model 的寫法
1 2 3 4 5 |
if ($user->can('create', Post::class)) { // } |
2. 透過 Moddleware 中介層
1 2 3 4 5 6 7 |
use App\Post; Route::put('/post/{post}', function (Post $post) { // 當前使用者有授權更新 })->middleware('can:update,post'); |
不需使用 Post Model 的寫法
1 2 3 4 5 |
Route::post('/post', function () { // 當前使用者可以新增 })->middleware('can:create,App\Post'); |
3. 透過 Controller 控制器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<?php namespace App\Http\Controllers; use App\Post; use Illuminate\Http\Request; use App\Http\Controllers\Controller; class PostController extends Controller { public function update(Request $request, Post $post) { $this->authorize('update', $post); // 當前使用者有授權更新 } } |
不需要 Post Model 的寫法
1 2 3 |
$this->authorize('create', Post::class); |
4. 透過 Blade Template 刀片模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@can('update', $post) <div>可以更新</div> @elsecan('create', App\Post::class) <div>可以新增</div> @endcan @cannot('update', $post) <div>當前使用者不可以更新</div> @elsecannot('create', App\Post::class) <div>當前使用者不可以新增</div> @endcannot |
也可以透過 @if 或 @unless
1 2 3 4 5 6 7 8 9 |
@if (Auth::user()->can('update', $post)) 當前使用者可以更新 @endif @unless (Auth::user()->can('update', $post)) 當前使用者不可更新 @endunless |
不需要使用 Post Model 也是把 $post 替換成 「App\Post::class」即可。
前往查看簡單教學:透過 Gmail 發送 E-mail 信件
Cache
相關設定可以再 config/cache.php 找到,這裡列出簡單的方法
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 |
// 寫入,不指定秒數將會永遠保存。重複使用會複寫 Cache::put('key', 'value', $seconds); // 不存在才寫入,不指定秒數將會永遠保存 Cache::add('key', 'value', $seconds); // 永遠儲存,必須使用 Cache::forget() 移除 Cache::forever('key', 'value'); // 希望取回快取項目,但如果不存在能寫入預設值,這種綜合體可以這麼寫 $value = Cache::remember('users', $seconds, function () { return DB::table('users')->get(); }); // 取得 $value = Cache::get('key'); // 檢索後並刪除。如果存在並刪除成功會返回該值;不存在則返回 null $value = Cache::pull('key'); // 刪除 Cache::forget('key'); // 清除整個緩存 Cache::flush(); |
Queues 隊列
可以延遲處理耗時的任務,例如發送消耗一段時間的 Email、圖片處理。設定檔 config/queue.php,在 connections 隊列配置底下都有一個隊列屬性 queue ,預設都是 default。隊列配置方式支援多種,有 Database、Redis、Amazon SQS、Beanstalkd 等等,以下使用 Database 範例。
我們先透過 migrate 建立一張資料表 jobs
1 2 3 4 |
php artisan queue:table php artisan migrate |
建立一個工作類別,會自動建立在 app\jobs 底下
1 2 3 |
php artisan make:job ProcessLog |
在 handle() 製作我們要處理的程序
1 2 3 4 5 6 7 8 |
use Illuminate\Support\Facades\Log; public function handle() { Log::info('Hello World'); } |
接著須要調度工作,例如我們透過 LogController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<?php namespace App\Http\Controllers; use App\Jobs\ProcessLog; use Illuminate\Http\Request; class LogController extends Controller { public function index() { ProcessLog::dispatch() ->delay(now()->addSeconds(5)); } } |
另外也有一些寫法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
ProccessLog::dispatch() ->delay(now()->addSeconds(5)) // 要延遲時間可指定 delay() ->onQueue('processing') // 指定隊列的命名,預設是 default ->onConnection('sqs'); // 指定要使用的隊列配置 ProcessPodcast::dispatchNow($podcast); // 同步調度,工作將不會排隊,而會直接執行 // 作業鏈,可按順序執行,並確保每個通道都執行完成 ProcessPodcast::withChain([ new OptimizePodcast, new ReleasePodcast ])->dispatch()->allOnConnection('redis')->allOnQueue('podcasts'); |
接著在路由 web.php 添加
1 2 3 |
Route::get('log', 'LogController@index'); |
目前為止,我們打算從網址觸發 LogController::index() ,並調度工作 ProccessLog 至隊列。在進入網址之前,務必將 Queues 啟動監聽。
修改 .env,將對列配置指定使用 database
1 2 3 |
QUEUE_CONNECTION=database |
接著下達指令,就會讓 Queue 監聽調度到 Database 的行為,注意這會持續運作。
1 2 3 4 5 |
php artisan queue:work // 若改程式碼,需要重新執行 php artisan queue:listen // 若改程式碼,不必重新執行,效率不比 :work 好 php artisan queue:work --once |
當然也有其他的參數可使用
1 2 3 4 |
php artisan queue:work database // 若與 .env QUEUE_CONNECTION 不同時,可強制指定配置 php artisan queue:work --queue=work // 只執行隊列被命名為 work 的工作 |
持續監聽以後,我們從網址觸發,在資料表 jobs 會看到有一筆數據;若有設定延遲 5 秒鐘,那麼過了 5 秒會在 command 看到如
1 2 3 4 5 |
php artisan queue:work [2019-08-07 03:51:11][25] Processing: App\Jobs\ProccessPodcast [2019-08-07 03:51:12][25] Processed: App\Jobs\ProccessPodcast |