php – Laravel 安裝與逐步教學

透過 Composer 安裝

先安裝 composer

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
composer create-project --prefer-dist laravel/laravel blog

可以在本機建立服務器,例如訪問 http://localhost:8000 會直接進入 blog 路徑

cd blog           <-若開新路徑記得進入底下
php artisan serve

API

如果要直接查閱 Laravel 的 Class 內部有哪些方法,可以參考官方 API Document 查詢你要的版本非常方便。官方 Laravel 手冊並沒有列出所有可用的方法,需要自行查找。

基礎教學

Facades 表面

若喜歡使用 Laravel 靜態的代理方法,例如使用 View::share() 替代 view()->share(),那麼必須要使用命名空間 Illuminate\Support\Facades。好處是看起來簡潔、難忘,Facades 提供許多的靜態方法可以到這裡查看

Illuminate\Support\Facades\View
Illuminate\Support\Facades\Cache

Routing 路由

預設 routes/web.php 是註冊給 web 訪問的路由,這些範例可以更快理解用法。

使用 GET 請求 domain/foo:

Route::get('foo', function () {
    return 'Hello World';
});

強制或選用參數傳遞:

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() 方法:

Route::get('/user', 'UserController@index');

下面這些都是回覆 HTTP 的動作:

Route::get($uri, $callback);
Route::post($uri, $callback);
Route::put($uri, $callback);
Route::patch($uri, $callback);
Route::delete($uri, $callback);
Route::options($uri, $callback);

同時指定多個方法到一個路由:

Route::match(['get', 'post'], '/', function () {
    //
});

任何HTTP動作都對應到一個路由:

Route::any('foo', function () {
    //
});

前綴用法,例如 admin/ 底下的路由

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 新增

php artisan make:middleware CheckAge
public function handle($request, Closure $next)
{
    if ($request->age <= 200) {
        return redirect('home');
    }

    return $next($request);
}

註冊中介層

預先註冊在 app/Http/Kernel.php 看是要

  1. $middleware 全域
  2. $middlewareGroups 路由群組
  3. $routeMiddleware 特定路由時觸發

一旦註冊,就可以在路由中使用。

使用全名

use App\Http\Middleware\CheckAge;

Route::get('admin/profile', function () {
    //
})->middleware(CheckAge::class);

使用別名

//app/Http/Kernel.php
protected $routeMiddleware = [
        // ......
        'checkage' => \App\Http\Middleware\CheckAge::class,
];
// 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 指示

<form method="POST" action="/profile">
    @csrf
    ...
</form>

透過 AJAX  的方法

通常我們在前端都已經使用 AJAX 發送 CRUD 請求了,所以我們需要夾帶在 headers 表頭傳送,這樣 Laravel 在中介層會透過 VerifyCsrfToken 檢查表頭中一個叫做 X-CSRF-TOKEN 的值。我們可以這樣製作,在視圖中添加

<meta name="csrf-token" content="{{ csrf_token() }}">

接著指示 jQuery 取得 csrf-token 並附加到 headers

$.ajaxSetup({
    headers: {
        'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
    }
});

如此一來就能夠簡單的保護 CSRF 攻擊。

Controllers 控制器

新增一般控制器

php artisan make:controller UserController

路由的參數也會對應到 show() 的 $id

// 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) 的動作。

將路由指定控制器

// routes/web.php
Route::get('/user/{id}', 'UserController@show');

控制器路徑底下的控制器,命名空間要注意預設的是 App\Http\Controllers。以下範例的路由 /foo 會讀取 App\Http\Controllers\Photos\AdminController.php。

// 路徑底下的寫法,foo 對應到實際的命名空間是 
Route::get('/foo', 'Photos\AdminController@method');
若要使用 middleware 中介層
Route::get('profile', 'UserController@show')->middleware('auth');

也可以在 Controller 中的 __construct() 使用分配哪些方法可以使用,哪些方法不可已使用,例如

class UserController extends Controller
{
    public function __construct()
    {
        // 所有 method 都使用
        $this->middleware('auth');

        // 只有 index() 使用
        $this->middleware('log')->only('index');

        // 除了 store() 以外都可以使用
        $this->middleware('subscribed')->except('store');
    }
}

新增 CRUD 控制器

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 帶入表單。

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

// routes/web.php
Route::put('user/{id}', 'UserController@update');
<?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 透過依賴入入的方式實現

use Illuminate\Http\Request;

Route::get('/', function (Request $request) {
    //
});

一些可以使用的方法

$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()

// 返回表單並夾帶快閃的使用者剛輸入的資料
return redirect('form')->withInput();

// 可以排除密碼
return redirect('form')->withInput(
    $request->except('password')
);

然後在重新填寫表單的控制器中,使用

$username = $request->old('username');

快閃取回剛剛填寫的欄位值。

JSON

如果來源請求 header 的 Content-Type 是 application/json,如 jQuery 的 $.post,那麼可以用 . 的方式來挖掘數據

$request->input('user.name');

過濾

$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. 分離使用,透過靜態方法 (個人較喜歡
public function index(Request $request)
{
    $minutes = 1;

    // 寫入
    \Cookie::queue('name', 'Jason', $minutes);

    // 取得
    echo \Cookie::get('name');
}
2. 附加在 reponse(),透過實體化 Request
public function index(Request $request)
{
    $minutes = 1;

    // 取得
    echo $request->cookie('name');

    // 寫入
    return response('Hello World')->cookie(
        'name', 'Jason', $minutes
    );
}

File 文件

主要是處理 $_FILE 的工具

$file = $request->file('photo');
$file = $request->photo;

上傳檔案

驗證檔案有效後,儲存到 storage/app/images/filename.png

if ($request->file('photo')->isValid()) {
    $request->photo->storeAs('images', 'filename.png');
}

如果要取得檔案相關資訊的化可以查看 file() 相關的方法,這是 Laravel 繼承使 Symfony的功能,前往看文件。例如

$request->file('photo')->hashName(); // 雜湊名稱,通常存入我會用這個作為檔名
$request->file('photo')->getClientOriginalName(); // 取得用戶端的檔名

通常我們上傳圖檔公開瀏覽,Laravel 有預設 storage/app/public 是用來公開訪問的儲存空間,那我們則改指定路徑:

$filename = $request->file('uploadPhoto')->hashName();
$path = $request->file('photo')->storeAs('public/images', $filename);

下 artisan 創建一個軟連結,讓 public/storage 連結到 storage/app/public,參考

php artisan storage:link

這樣我們可以用 asset() 做顯示圖片訪問了

echo asset("storage/images/{$filename}");

Reponse 回覆

簡單用法,直接在 controllers 或 routes 中使用 return 作為回覆數據。

Route::get('/', function () {
    
    // 回覆文字
    return 'Hello World';
    
    // 或陣列
    return [1, 2, 3];
});

Redirect 重新導向

導向路徑

// 內部
return redirect('home/dashboard');

// 外部
return redirect()->away('https://www.google.com');

導向上一頁並夾帶 input 參數

例如驗證錯誤表單的時候,會把值放到一次性的備存

// 若驗證失敗
return back()->withInput();

// 返回頁可以這樣取得剛剛得填寫資料
$username = $request->old('username');

導向被命名的路由

return redirect()->route('profile', ['id' => 1]);

導向控制器動作

return redirect()->action(
    'UserController@profile', ['id' => 1]
);

導向並夾帶快閃數據

這通常用在如 “新增成功” 的訊息

return redirect()
    ->action('ProductsController@create')
    ->with('message', '新增成功');

導向到的視圖可以這麼處理

@if (session('message'))
    <div class="alert alert-success">
        {{ session('message') }}
    </div>
@endif

JSON

return response()->json([
    'name' => 'Abigail',
    'state' => 'CA'
]);

JSONP

return response()
    ->json(['name' => 'Abigail', 'state' => 'CA'])
    ->withCallback($request->input('callback'));

File Downloads 文件下載

// 要下載的文件路徑
return response()->download($pathToFile);

// 可選用下載的檔案名稱,或是檔頭
return response()->download($pathToFile, $name, $headers);

// 下載後可以刪除
return response()->download($pathToFile)->deleteFileAfterSend(true);

Streamed Downloads 下載資料流

有時候我們會需要下載 echo 的檔案

return response()->streamDownload(function () {
    echo "Hello World";
}, 'laravel-readme.md');

File Responses 文件回覆

可以直接在瀏覽器顯示文件而不會啟動下載,例如 PDF

return response()->file($pathToFile);

Views 視圖

單張視圖的參數

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 的時候(下方會介紹)。

return view('greeting')->with('name', 'Jason');

共用視圖的參數

可以提供給所有視圖都使用這項參數。適合放置的地方在 app/Providers/AppServiceProvider.php

<?php
namespace App\Providers;
use Illuminate\Support\Facades\View;

class AppServiceProvider extends ServiceProvider
{
    public function boot()
    {
        View::share('key', 'value');
    }
}

Blade 刀片

傳遞到試圖的參數,基本上我們可以透過 {{ $data }} 來顯示並自動過濾XSS攻擊,如果不希望過濾XSS可以使用如

Hello, {!! $name !!}.

其實 blade 也只是把 {{ }} 符號轉換成

<?php echo e($test); ?>

{{ }} 內也可以使用純 PHP 語言如

The current UNIX timestamp is {{ time() }}.

另外邏輯與指令通常都會加入前綴 ‘@’ 來表示。基本上邏輯判斷的寫法跟 PHP 類似,所有用法就去官網看這裡不贅述。建立表單常用的:

<form action="/foo/bar" method="POST">
    @method('PUT')     <- 發送非 POST/GET 的 HTTP 請求動作
    @csrf <- CSRF保護
</form>

當然 Blade 可以組合不同分割的視圖,我們快速示範這個例子

// 透過指令建立控制器
php artisan make:controller ProductsController --resource

// routes/web.php 設定路由
Route::resource('products', 'ProductsController');

// ProductsController.php
public function create()
{
    // 注意我們顯視的是子視圖
    return view('products.create_child');
}

兩張視圖的部分設計如下

<!-- resources/views/products/create.blade.php -->
<h1>
    App Name - @yield('title')
</h1>

@section('sidebar')
    <p>這裡放置 sidebar</p>
@show

<div class="container">
    @yield('content')
</div>
<!-- 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()」,查看網址後會看到合併後的結果

<h1>App Name - 標題</h1>
<p>這裡放置 sidebar</p>
<p>因為 parent 的關係,這段文字合併追加在 create 的 sidebar</p>
<div class="container">
    <p>這是內容</p>
</div>

Service Inject 注入服務

如果要在 blade 內使用類別,例如我們常見要處理字串、if else 顯示不同區塊、依照各國顯示不同的日期格式等視覺。可以這麼使用

@inject('metrics', 'App\Services\MetricsService')

<div>
    Monthly Revenue: {{ $metrics->monthlyRevenue() }}.
</div>
  • @inject(‘接收的變數’, ‘類別名稱’)

一些工具

要截斷文字,通常用來顯示描述可以使用

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 聯絡我們的介面:


  1. 建立控制器:使用 artisan 來產生 app/Http/Controllers/ContactController.php
    php artisan make:controller ContactController --resource
  2. 指定路由: routes/web.php,自動對應 CRUD
    Route::resource('contact', 'ContactController');

    測試 http://localhost:8000/contact 應該能正確讀取方法 ContactController::index()

  3. 添加視圖
    我們先指定 ContactController.php 要載入的視圖並帶入參數

     

    public function index()
    {
        return view('contact.index', [
            'name' => 'Jason'
        ]);
    }

    新增 resources/views/contact/index.blade.php 並顯示參數值

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <title>Contact</title>
    </head>
    <body>
        <h1>聯絡我們</h1>
        <p>
            Hi, {{ $name }}
        </p>
    </body>
    </html>
  4.  建立 service provider 服務提供者 ComposerServiceProvider:
    手動添加 app/Providers/ComposerServiceProvider.php,參考

     

    <?php
    namespace App\Providers;
    use Illuminate\Support\ServiceProvider;
    
    class ComposerServiceProvider extends ServiceProvider
    {
        public function boot()
        {
            //
        }
    }
  5. 加入設定:config/app.php,讓系統運作的時候會啟用 ComposerServiceProvider
    'providers' => [
        //...
        App\Providers\ComposerServiceProvider::class,
    ],

    瀏覽器重新整理就會觸發上面 boot(),所以我們要添加邏輯:當讀取視圖 contact/sendtype.blade.php 會調用 App\Http\ViewComposers\SendtypeComposer 類別

    <?php
    namespace 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() ,這還有兩種寫法可以使用

    // 指定多個 view 調用
    View::composer(
        ['profile', 'dashboard'],
        'App\Http\ViewComposers\MyViewComposer'
    );
    // 所有 view 都調用匿名函式
    View::composer('*', function ($view) {
        //
    });
  6. 建立被調用的類別:app/Http/ViewComposers/SendtypeComposer.php,並提供視圖所需要的參數。我們透過 $view->with() 的方法來片段增加。
    <?php
    namespace App\Http\ViewComposers;
    use Illuminate\View\View;
    
    class SendtypeComposer
    {
        public function __construct()
        {
            //
        }
    
        public function compose(View $view)
        {
            $view->with('types', [
                '客服中心',
                '資訊部門',
                '設計部門',
            ]);
        }
    }

    這時候規則都建立好了,不過還無法看到實際效果,所以我們要建立 ComposerServiceProvider 所提到的視圖 sendtype.blade.php

  7. 建立用 View Compoer 抽離的視圖
    <select name="type">
        @foreach ($types as $key => $type)
            <option value="{{$key}}">{{ $type }}</option>
        @endforeach
    </select>
  8. 讓視圖 index.blade.php 透過模板語言載入視圖 sendtype.blade.php
    我們修改 resources/views/contact/index.blade.php 如下,利用 blade 指令的 @include(),接著重新整理畫面就能看到包含 contact/sendtype.blade.php 與帶入參數的介面了。

     

    <!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 網址生成

use Illuminate\Support\Facades\URL;
URL::current();
URL::full();
URL::previous();

也可以透過 url helper 直接使用

$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 命名,可以不必耦合到實際的路由。當我們實際的路由發生改變,就不需要更動調用的路由函式。

Route::get('/article/{id}', function ($id) {
    
})->name('article.show');
echo route('article.show', ['post' => 1]);
// http://localhost:8000/article/1

Signed URLs 簽署網址

產生一個有時效性的網址,這會通過簽署來認證合法性。通常用在發送 E-mail 給客戶用來申請忘記密碼、或是退訂訂單確認的時候。以下範例

路由先定義好否則會報錯

use Illuminate\Http\Request;

Route::get('/unsubscribe/{user}', function (Request $request)
{
    if (!$request->hasValidSignature())
    {
        abort(401, '簽章錯誤');
    }

    return response('簽章通過');
})->name('unsubscribe');

接著不經過控制器直接路由示範

Route::get('/test', function ()
{
    return URL::temporarySignedRoute(
        'unsubscribe', now()->addMinutes(30), ['user' => 1]
    );
});

打開 http://localhost:8000/test 會看到一串網址,我們貼到網址去,成功的話就會出現簽章通過。

如果要在 form 表單中夾帶簽章參數,拋送到 Laravel 的時候可以透過中間層去驗證,可以簡化一些程式碼。

// app/Http/Kernel.php
// 5.6 版已經預先載入到中間層了
protected $routeMiddleware = [
    'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
];

路由的部分,會需要透過中間層去認證簽署,我們在命名路由之後,緊接著使用 middleware()

Route::post('/unsubscribe/{user}', function (Request $request) {
    // ...
})->name('unsubscribe')->middleware('signed');

URLs For Controller Actions 控制器操作網址

對應到控制器的路由,如果控制器使用 resource 的方式對應CRUD,那麼網址也會自動轉換,必填的路由參數字段,也會強制要帶入。

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]),
    ]);
});
// 輸出
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"
]

在視圖表單的時候也可以這麼使用

<form method="post" action="{{ action('ProductsController@store') }}">

Session 會話

這個就不陌生了,當然建議直接使用 Laravel 提供的,因為包含了自動加密。使用如

use Illuminate\Http\Request;
public function index(Request $request)
{
    $request->session;
}

不過我推薦使用全域函式。

新增/修改

session(['name' => 'Kelly']);

提取

// 不存在有預設值
session('name', 'default');
// 取得所有
session()->all();

拉出並刪除

session()->pull('name', 'default');

刪除

session()->forget('name');

清空所有

session()->flush();

Validation 驗證

暫時空著,太多囉

Error Handling 錯誤處理

報告紀錄而不會渲染錯誤頁面

try {
    // Validate the value...
} catch (Exception $e) {
    report($e);

    return false;
}

HTTP Exceptions – HTTP 例外處理

abort(403, 'Unauthorized action.');

Logging 紀錄

紀錄預設會在 storage/logs/laravel.log ,相關設定檔可以查閱 config/logging.php。有這八個級別可以用

Log::emergency($message);
Log::alert($message);
Log::critical($message);
Log::error($message);
Log::warning($message);
Log::notice($message);
Log::info($message);
Log::debug($message);

可以記錄陣列

Log::info($message, [
    'id' => 2,
    'name' => 'Jason'
]);

Encryption 加密

生成隨機的鑰匙,添加到 config/app.php 的選用參數 key

php artisan key:generate
use Illuminate\Support\Facades\Crypt;

$encrypted = Crypt::encryptString('Hello world.'); // 加密

$decrypted = Crypt::decryptString($encrypted); // 解密

Hash 哈希

單向雜湊,適合用在儲存密碼

use Illuminate\Support\Facades\Hash;
Hash::make($password);

Database 資料庫連接

config/database.php 設定如 mysql 的帳號密碼,預設使用 Laravel Homestead,但是我們不用所已把參數替換成如

'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語言直接做溝通。

$users = DB::table('users')->select('name', 'email as user_email')->get();
DB::table('users')->insert([
    ['email' => 'taylor@example.com', 'votes' => 0],
    ['email' => 'dayle@example.com', 'votes' => 0]
]);
DB::table('users')
    ->where('id', 1)
    ->update(['options->enabled' => true]);
DB::table('users')->where('votes', '>', 100)->delete();

如果需要關聯查詢

$users = DB::table('users')
            ->leftJoin('posts', 'users.id', '=', 'posts.user_id')
            ->get();

其他詳細的看 官方介紹

Eloquent ORM 付於表現的物件關聯對映

內部的基礎指令是透過 Query Build,我們這裡介紹 ORM 操作。Laravel 模型類別名稱使用單數。以下示範,我們先透過 artisan 建立

php artisan make:model Product

開啟 app/Product.php,預設會有以下事情,若要修改預設可以參考官方

  1. 對應複數資料表名稱,並使用下滑線連接單字
  2. 主鍵預設 id 且為整數,會自動遞增
  3. 時間戳記預設啟用 Y-m-d H:i:s,所以資料表須要 created_at 與 updated_at 欄位
  4. 資料庫連接使用設定檔,如果要連到額外的資料庫則須修改
<?php
namespace App;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    //
}

上方式基本上欲設規則了,但很多時候我們的資料庫可能是沿用過去,欄位設計不一定符合 Laravel 預設,因此我們可以手動修改例如

<?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 的相關操作囉

use App\Product;
$products = Product::all();

ORM/Query Build 在預設情況下,幫我們處理了 SQL Injection 攻擊。常用的 CRUD 指令都非常多,建議到官網去看。以下介紹基本

新增

$product = new Product;
$product->title = $request->input('title');
$product->price = $request->input('price');
$product->save();
echo $product->id; // 取得新增的編號

查詢

Product::where('id', '>', '2')->get();
Product::select(['id', 'title']) // 可以多個欄位
    ->where('id', '>', '2')
    ->orderBy('id')
    ->skip(10) // 也可用 offset()
    ->take(5) // 也可用 limit()
    ->get();

我們常常需要找不到資料來可以獲取 Exception 列外來顯示找不到頁面,可以這樣用

Product::where('id', '=', 100)->firstOrFail();

修改

$product = Product::find(53);
$product->title = $request->input('title') . " - sale";
$product->price = DB::raw('price * 0.7'); // 也就是 price = price * 0.7
$product->save();

實際刪除

這個刪除會真的從資料表中刪除。

Product::where('id', 52)->delete();

如果僅希望虛擬的刪除 (Soft Delete 軟刪除) 那要使用下方介紹

軟刪除

並不是真的刪除數據,而是透過改變欄位 deleted_at 來判斷刪除。所以資料表一定要有這個欄位,Model 也須要添加 trait,例如

<?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 出現了時間戳記。

Product::where('id', 52)->delete();

當我們從 Laravel 的模型中取出資料表資料,這筆資料就不會存在了,當然也包含任何的計算數量與查詢。

使用原始文字

有時候我們會用到原生的資料庫方法,這時候就搭配使用 DB::raw(),可以免去自動加引號。例如

$product = new Product;
$product->title = $request->input('title');
$product->timestamp = DB::raw('NOW()');
$product->save();

自訂類別

因為使用 psr-4 標準,我們在 composer.json 可以看到已經定義在 app/ 底下

"autoload": {
    "psr-4": {
        "App\\": "app/"
    }
},

所以我們自訂的類別可以放在 app/ 底下的任何地方,只要符合命名空間與路境的對應就好。有了 psr-4 會很方便,因為我們從命名空間就能知道類別視放置在哪裡。例如我有個購物車類別

// app/Jason/Cart.php

namespace APP\Jason;

class Cart 
{
    public function get()
    {
        return 'Apple';
    }
}

控制器或路由就直接使用即可

use App\Jason\Cart;
echo (new Cart)->get();

Pagination 分頁

主要有兩個方法

  • paginate() 包含計算所有數量與各個分頁
  • simplePaginate() 只有上下頁,所以無法取得總數量與每個分頁

分頁的 HTML 結構與 CSS 樣式,會與 Bootstrap 前端元件函視庫 (front-end component library) 相符合,例如控制器或路由中指定需要每頁 5 筆

// 自動分配數據與分頁連結
$products = DB::table('products')->paginate(5);

// 如果資料庫使用 OPM 的話
Product::paginate(5);

// 若要自訂連結
$products->withPath('custom/url');

return view('products.index', 
[
    'products' => $products
]);

視圖

<div class="container">
    <ul>
        @foreach ($products as $product)
            <li>
                <a href="">{{ $product->id }} . {{ $product->title }}</a>
            </li>
        @endforeach
    </ul>
</div>

{{ $products->links() }}

追加 Query String

{{ $products->appends(['sort' => 'votes'])->links() }} // custom/url?sort=votes&page=3

添加錨點

{{ $products->fragment('foo')->links() }}  // custom/url?page=3#foo

手動製作

通常我們會配合 Model 自動分頁,但有時我們希望自己切割陣列或物件來製作。參考使用 Illuminate\Pagination\LengthAwarePaginator,實例化(參考)要夾帶參數如

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 套件,如果我們不使用它而打算自定義視圖的話,可以寫

{{ $paginator->links('view.name') }}

// 還可以帶入參數
{{ $paginator->links('view.name', ['foo' => 'bar']) }}

當然我們可以透過 artisan 產生並從中修改已經定義好的視圖,會更方便

php artisan vendor:publish --tag=laravel-pagination

會在 resources/views/vendor/pagination/ 看到預設的視圖模板。

指定預設分頁視圖

如果不使用 Bootstrap 但要套用到整個系統的預設值,可以在 blog/app/Providers/AppServiceProvider.php 設定

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

其他操作方法

$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。
// 建立在 Feature 路徑
php artisan make:test UserTest

// 建立在 Unit 路徑
php artisan make:test UserTest --unit

看 tests/Unit/ExampleTest.php 這個方法 assertTrue() 是用來斷言為真

public function testBasicTest()
{
    $this->assertTrue(true);
}

接著我們下指令測試,官方是說用 phpunit ,不過在 windows 要這麼使用

.\vendor\bin\phpunit

Linux 下使用

vendor/bin/phpunit

(如果要看到概要可以這麼用)

.\vendor\bin\phpunit --testdox

接著我們可以看到測試結果

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); 因為會是錯誤的結果,那麼會得到這樣

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 資料庫喬遷

php artisan make:migration create_users_table

Authentication 認證

使用 artisan 在 app/Http/Controllers/Auth 自動建立註冊、登入、重設密碼、忘記密碼四個控制器。

php artisan make:auth

接著要建立資料表 users,但因為路徑 database/migrations 已經預設了喬遷資料庫的紀錄,我們只需要啟用來建立預設的使用者資料表。

php artisan migrate

如果下了指令後看到報錯

Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes

那麼只要在 app/Providers/AppServiceProvider.php 加入

use Illuminate\Support\Facades\Schema;

public function boot()
{
    Schema::defaultStringLength(191);
}

然後去資料庫把 migrations, users 資料表刪除,重新下 artisan 指令即可。成功建立後,前台頁面就可以嘗試註冊使用者並登入。

重新導向

認證成功會自動導向 /home 若要修改,可以修改控制器的屬性如

protected $redirectTo = '/';

這個屬性可以在 LoginController.php, RegisterController.php, ResetPasswordController.php 中找到。另外還要到 app/Http/Middleware/RedirectIfAuthenticated.php 修改如

public function handle($request, Closure $next, $guard = null)
{
    // ...
    return redirect('/');
    // ...
}

自訂使用者儲存資料

修改 form 表單後,在 app/Http/Controllers/Auth/RegisterController.php 有兩個方法

  • validator()
    • 可以自行添加要驗證的
  • create()
    • 這是使用 Eloquent ORM 新增,可自行添加要寫入的

取得認證成功的使用者資料

通過認證,不管在哪裡我們都可以用簡單的方法來取得

use Illuminate\Support\Facades\Auth;

// 取得當前認證的使用者
$user = Auth::user();
$email = Auth::user()->email;

// 取得當前認證的使用者編號
$id = Auth::id();

當然也可以在控制器中使用 Request

<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;

class ProfileController extends Controller
{
    public function update(Request $request)
    {
        // 認證過的使用者實例 
        $request->user();
    }
}

確定當前用戶是否認證

通常這會在 Middleware 中介層做好認證後才訪問某些控制器。

use Illuminate\Support\Facades\Auth;

if (Auth::check()) {
   //...
}

訪問需要使用者認證

可以在路由使用

Route::get('profile', function () {
    // 只有認證過的使用者才能進入,否則導向到登入頁
})->middleware('auth');

或是在控制器添加

public function __construct()
{
    $this->middleware('auth');
}

重新導向未經授權的用戶

當 auth 中介層偵測到未經授權的用戶,將返回 JSON 401,或者如果不是 AJAX 請求,將重新導向用戶到登陸名為 route。app/Exceptions/Handler.php 添加

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 的鍵

public function __construct()
{
    $this->middleware('auth:api');
}

登入限制

LoginController 預設多次登入失敗,將會暫停登入1分鐘。定義在 Illuminate\Foundation\Auth\ThrottlesLogins.php。

手動登入

密碼我們不用 Hash 後到資料表中比對,因為 Laravel 會幫我們處理好。

<?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');
        }
    }
}

如果要附加一些條件,可以這麼寫

if (Auth::attempt(['email' => $email, 'password' => $password, 'active' => 1])) {
    // 使用者處於活動狀態且存在,不是暫停使用
}

訪問特定的 Guard 警衛實例

下面例子,對應到 config/auth.php 中的陣列 guards。

if (Auth::guard('admin')->attempt($credentials)) {
    //
}

登出

Auth::logout();

記住使用者

登入後,通常我們會記住使用者,那麼就給予第二個參數布林值 true

if (Auth::attempt(['email' => $email, 'password' => $password], true)) {
    //
}

我們可以透過這樣來確認已經被記住了

if (Auth::viaRemember()) {
    //
}

將取得的使用者登入

若我們透過 Model 取得使用者,打算將他登入

$user = User::find(1);
Auth::login($user);
Auth::login($user, true); // 記住

也可以使用 guard 警衛

Auth::guard('admin')->login($user);

也可以直接使用主鍵

Auth::loginUsingId(1);

// 登入並記住
Auth::loginUsingId(1, true);

若要單次請求被記住,不會使用 session, cookie

if (Auth::once($credentials)) {
    //
}

Authorization 授權

通常混和 Gates 與 Policies 兩個方法,可以想像是 Routes 與 Controllers 的角色。通常決定要使用 Gates 或是 Policies 可以這樣想:

  • Gates 適用在無任何模型或資源,例如查看儀錶版。
  • Policies 適用在想要為特定模型或資源做授權。

Gates 大門

Gates 寫在 App\Providers\AuthServiceProvider:

public function boot()
{
    $this->registerPolicies();

    Gate::define('update-post', function ($user, $post) {
        return $user->id == $post->user_id;
    });
}

或是使用 Class@method 樣式,那麼會自動對應到 Policies (下個段落介紹)

Gate::define('update-post', 'App\Policies\PostPolicy@update');

當然也可以使用 resource 方法,如同在 Routes 路由自動定義 Controllers 一樣

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');

如果要複寫功能的話,可以透過第三個參數,鍵代表功能,值代表方法

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 的閉包。

if (Gate::allows('update-post', $post)) {
    // 使用者可以更新發佈
}

if (Gate::denies('update-post', $post)) {
    // 使用者不可以更新發佈
}

不過如果想要明確指定哪個使用者的話,可以這麼寫

if (Gate::forUser($user)->allows('update-post', $post)) {
    // 可更新
}

if (Gate::forUser($user)->denies('update-post', $post)) {
    // 不可更新
}

範例,當身分不是 super 的時候不能訪問儀錶版:

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 政策的方法

php artisan make:policy PostPolicy
// 或
php artisan make:policy PostPolicy --model=Post

Registering Policies 註冊政策

一旦政策存在,我們就要註冊它。只要到 app/Providers/AuthServiceProvider.php 找到屬性 policies 陣列填入即可

<?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 會跳出未授權的結果。

<?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 所以這兩個方法可以使用。

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 的寫法

if ($user->can('create', Post::class)) {
    // 
}

2. 透過 Moddleware 中介層

use App\Post;

Route::put('/post/{post}', function (Post $post) {
    // 當前使用者有授權更新
})->middleware('can:update,post');

不需使用 Post Model 的寫法

Route::post('/post', function () {
    // 當前使用者可以新增
})->middleware('can:create,App\Post');

3. 透過 Controller 控制器

<?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 的寫法

$this->authorize('create', Post::class);

4.  透過 Blade Template 刀片模板

@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

@if (Auth::user()->can('update', $post))
    當前使用者可以更新
@endif

@unless (Auth::user()->can('update', $post))
    當前使用者不可更新
@endunless

不需要使用 Post Model 也是把 $post 替換成 「App\Post::class」即可。

Mail

前往查看簡單教學:透過 Gmail 發送 E-mail 信件

Cache

相關設定可以再 config/cache.php 找到,這裡列出簡單的方法

// 寫入,不指定秒數將會永遠保存。重複使用會複寫
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

php artisan queue:table
php artisan migrate

建立一個工作類別,會自動建立在 app\jobs 底下

php artisan make:job ProcessLog

在 handle() 製作我們要處理的程序

use Illuminate\Support\Facades\Log;

public function handle()
{
    Log::info('Hello World');
}

接著須要調度工作,例如我們透過 LogController

<?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));
    }
}

另外也有一些寫法

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 添加

Route::get('log', 'LogController@index');

目前為止,我們打算從網址觸發 LogController::index() ,並調度工作 ProccessLog 至隊列。在進入網址之前,務必將 Queues 啟動監聽

修改 .env,將對列配置指定使用 database

QUEUE_CONNECTION=database

接著下達指令,就會讓 Queue 監聽調度到 Database 的行為,注意這會持續運作。

php artisan queue:work // 若改程式碼,需要重新執行
php artisan queue:listen // 若改程式碼,不必重新執行,效率不比 :work 好
php artisan queue:work --once

當然也有其他的參數可使用

php artisan queue:work database // 若與 .env QUEUE_CONNECTION 不同時,可強制指定配置
php artisan queue:work --queue=work // 只執行隊列被命名為 work 的工作

持續監聽以後,我們從網址觸發,在資料表 jobs 會看到有一筆數據;若有設定延遲 5 秒鐘,那麼過了 5 秒會在 command 看到如

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

發表迴響