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 看是要
- $middleware 全域
- $middlewareGroups 路由群組
- $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 聯絡我們的介面:
- 建立控制器:使用 artisan 來產生 app/Http/Controllers/ContactController.php
php artisan make:controller ContactController --resource
- 指定路由: routes/web.php,自動對應 CRUD
Route::resource('contact', 'ContactController');
測試 http://localhost:8000/contact 應該能正確讀取方法 ContactController::index()
- 添加視圖
我們先指定 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>
- 建立 service provider 服務提供者 ComposerServiceProvider:
手動添加 app/Providers/ComposerServiceProvider.php,參考
<?php namespace App\Providers; use Illuminate\Support\ServiceProvider; class ComposerServiceProvider extends ServiceProvider { public function boot() { // } }
- 加入設定: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) { // });
- 建立被調用的類別: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
- 建立用 View Compoer 抽離的視圖
<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 與帶入參數的介面了。<!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,預設會有以下事情,若要修改預設可以參考官方
- 對應複數資料表名稱,並使用下滑線連接單字
- 主鍵預設 id 且為整數,會自動遞增
- 時間戳記預設啟用 Y-m-d H:i:s,所以資料表須要 created_at 與 updated_at 欄位
- 資料庫連接使用設定檔,如果要連到額外的資料庫則須修改
<?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」即可。
前往查看簡單教學:透過 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