Laravel Cashier
簡介
Laravel Cashier 提供口語化,流暢的介面與 Stripe 的訂購交易服務介接。它幾乎處理了所有讓人退步三舍的訂購管理相關邏輯。除了基本的訂購管理,Cashier 還可以處理折價券,訂購轉換,管理訂購「數量」、取消寬限期,甚至產生收據的 PDF。
設定
Composer
首先,將 Cashier 套件新增至 composer.json
檔案並執行 composer update
指令:
"laravel/cashier": "~5.0" (For Stripe SDK ~2.0, and Stripe APIs on 2015-02-18 version and later)
"laravel/cashier": "~4.0" (For Stripe APIs on 2015-02-18 version and later)
"laravel/cashier": "~3.0" (For Stripe APIs up to and including 2015-02-16 version)
服務提供者
接著,在 app
設定檔中註冊 Laravel\Cashier\CashierServiceProvider
服務提供者。
遷移
使用 Cashier 前,我們需要增加幾個欄位到資料庫。別擔心,你可以使用 cashier:table
Artisan 指令,建立遷移檔來新增必要欄位。舉個例子,若要增加欄位到 users 資料表,使用命令:php artisan cashier:table users
。
建立完遷移檔後,只要執行 migrate
指令即可。
模型設定
再來,將 Billable
trait 和適當的日期存取器新增至你的模型定義中:
use Laravel\Cashier\Billable;
use Laravel\Cashier\Contracts\Billable as BillableContract;
class User extends Model implements BillableContract
{
use Billable;
protected $dates = ['trial_ends_at', 'subscription_ends_at'];
}
你的模型中 $dates
屬性裡新增的欄位會指定 Eloquent 必須將該欄位回傳為 Carbon 或 DateTime 實例,而不是原始字串。
Stripe 金鑰
最後,在你的 services.php
設定檔中設定 Stripe 金鑰:
'stripe' => [
'model' => 'User',
'secret' => env('STRIPE_API_SECRET'),
],
訂購
建立訂購
要建立一個訂購,首先要取得可交易的模型實例,這通常會是 App\User
的實例。一旦你取得了模型實例,你可以使用 subscription
方法來管理模型的訂購:
$user = User::find(1);
$user->subscription('monthly')->create($creditCardToken);
create
方法會自動建立與 Stripe 的交易,以及將 Stripe 客戶 ID 和其他相關帳款資訊更新到資料庫。如果你的方案有在 Stripe 設定試用期,試用到期日也會自動儲存至使用者的記錄。
如果你想要實施試用期,但是你完全用應用程式來管理試用期,而不是在 Stripe 裡設定,那麼你必須手動設定試用到期日:
$user->trial_ends_at = Carbon::now()->addDays(14);
$user->save();
額外使用者詳細資料
如果你想自定額外的顧客詳細資料,你可以將資料陣列作為 create
方法的第二個參數傳入:
$user->subscription('monthly')->create($creditCardToken, [
'email' => $email, 'description' => '我們的第一個客戶'
]);
想知道更多 Stripe 支援的額外欄位,請查看 Stripe 的創建顧客的文件。
折價券
如果你想在建立訂購的時候使用折價券,可以使用 withCoupon
方法:
$user->subscription('monthly')
->withCoupon('code')
->create($creditCardToken);
確認訂購狀態
一旦使用者在你的應用程式訂購,你可以使用多種便捷的方法,很簡單的檢查他們的訂購狀態。首先,當使用者擁有有效訂購時,subscribed
方法會回傳 true
,即使該訂購目前在試用期間:
if ($user->subscribed()) {
//
}
subscribed
方法很適合用在路由中介層,讓你可以透過使用者的訂購狀態,過濾存取路由及控制器:
public function handle($request, Closure $next)
{
if ($request->user() && ! $request->user()->subscribed()) {
// 此使用者不是付費使用者...
return redirect('billing');
}
return $next($request);
}
如果你想確認使用者是否還在他們的試用期內,你可以使用 onTrial
方法。此方法在向使用者顯示他們還在試用期內的警告是很有用的:
if ($user->onTrial()) {
//
}
onPlan
方法可以用 Stripe ID 來確認使用者是否訂購某方案:
if ($user->onPlan('monthly')) {
//
}
取消訂購狀態
若要確認使用者是否曾經訂購,但是已經取消他們的訂購,你可以使用 cancelled
方法:
if ($user->cancelled()) {
//
}
你可能想確認使用者是否已經取消訂購,但是還在他們訂購完全到期前的「寬限期」。例如,如果使用者在三月五號取消了訂購,但是服務會到三月十號才過期。那麼使用者到三月十號前都是「寬限期」。注意,subscribed
方法在這個期間仍然會回傳 true
。
if ($user->onGracePeriod()) {
//
}
everSubscribed
方法可以用來確認使用者是否訂購過應用程式裡的方案:
if ($user->everSubscribed()) {
//
}
改變方案
當使用者在你的應用程式訂購之後,他們有時可能想更改自己的訂購方案。使用 swap
方法可以把使用者轉換到新的訂購。舉個例子,我們可以簡單的將使用者切換至 premium
訂購:
$user = App\User::find(1);
$user->subscription('premium')->swap();
如果使用者還在試用期間,試用服務會跟之前一樣可用。如果訂單有「數量」,也會和之前一樣。當改變方案時,你也可以使用 prorate
方法以表示該費用是按照比例計算。此外,你可以使用 swapAndInvoice
方法馬上開發票給改變方案的使用者:
$user->subscription('premium')
->prorate()
->swapAndInvoice();
訂購數量
有時候訂購行為會跟「數量」有關。例如,你的應用程式可能會依照帳號的使用者人數,每人每月收取 10 元。你可以使用 increment
和 decrement
方法簡單的調整訂購數量:
$user = User::find(1);
$user->subscription()->increment();
// 增加 5 個訂購數量...
$user->subscription()->increment(5);
$user->subscription()->decrement();
// 減少 5 個訂購數量...
$user->subscription()->decrement(5);
另外,你也可以使用 updateQuantity
方法來設置指定的數量:
$user->subscription()->updateQuantity(10);
有關訂購數量的更多資料,請參閱 Stripe 文件。
訂購稅金
在 Cashier 中,可以很容易的提供發送至 Stripe 的 tax_percent
值。要指定一個使用者付費訂購時的稅金比例,請在你的交易模型中實作 getTaxPercent
方法,並回傳一個介於 0 至 100 間,且不超過兩位小數的數值。
public function getTaxPercent() {
return 20;
}
這讓你基於個別模型去運用稅率,可能對橫跨多個國家的用戶群非常有幫助。
取消訂購
要取消一個訂購,只需要在使用者的訂購呼叫 cancel
方法:
$user->subscription()->cancel();
當訂購被取消時,Cashier 會自動更新資料庫的 subscription_ends_at
欄位。這個欄位會被用來判斷 subscribed
方法什麼時候該開始回傳 false
。例如,如果顧客在三月一號取消訂購,但是服務可以使用到三月五號為止,那麼 subscribed
方法在三月五號前都會傳回 true
。
你想確認使用者是否已經取消他們的訂購,但是還在他們的「寬限期」間,可以使用 onGracePeriod
方法:
if ($user->onGracePeriod()) {
//
}
恢復訂購
如果使用者已經取消了他們的訂購,但你想恢復訂購,可以使用 resume
方法:
$user->subscription('monthly')->resume($creditCardToken);
如果客戶取消訂購後,且接著在服務完全過期前恢復訂購,他們將不會在當下被扣款。他們的訂購會被重新啟動,而付款則會依照平常的週期。
處理 Stripe Webhooks
訂購失敗
如果顧客的信用卡過期了呢?無需擔心,Cashier 包含了 Webhook 控制器,可以幫你簡單的取消顧客的訂單。只要在路由註冊控制器:
Route::post('stripe/webhook', '\Laravel\Cashier\WebhookController@handleWebhook');
這樣就完成了!失敗的交易會經由控制器捕捉並進行處理。控制器在 Stripe 確認訂購已經失敗後 (通常在三次交易嘗試失敗後),才會取消顧客的訂單。別忘了:你必須設定 Stripe 控制面板設定裡的 webhook URI。
由於 Stripe webhooks 必須繞過 Laravel 的 CSRF 驗證,請確定在增加 URI 例外至你的 VerifyCsrfToken
中介層:
protected $except = [
'stripe/*',
];
其他 Webhooks
如果你想要處理額外的 Stripe webhook 事件,只需要繼承 Webhook 控制器。你的方法名稱要對應到 Cashier 預設的名稱,尤其是方法名稱應該使用 handle
前綴,並使用「駝峰式」命名法,後面接著你想要處理的 Stripe webhook。例如,如果你想要處理 invoice.payment_succeeded
webhook,你應該增加一個 handleInvoicePaymentSucceeded
方法到控制器。
<?php
namespace App\Http\Controllers;
use Laravel\Cashier\WebhookController as BaseController;
class WebhookController extends BaseController
{
/**
* 處理一個 stripe webhook。
*
* @param array $payload
* @return Response
*/
public function handleInvoicePaymentSucceeded($payload)
{
// 處理該事件
}
}
一次性收費
如果你想對一個已訂購客戶的信用卡進行「一次性」收費,你可以對一個交易模型實例使用 charge
方法。charge
方法接受你想收取應用程式使用貨幣的最低單位的金額。所以,舉例來說,下方的例子將會對使用者的信用卡收取 100 美分,或是 1 美元:
$user->charge(100);
charge
方法接受一個陣列作為第二個參數,你可以傳遞任何你希望的選項至底層的 Stripe 付費創建器:
$user->charge(100, [
'source' => $token,
'receipt_email' => $user->email,
]);
當收費失敗時 charge
方法會回傳 false
。這通常表示收費被拒絕:
if ( ! $user->charge(100)) {
// 該收費被拒絕...
}
如果收費成功,該方法會回傳完整的 Stripe 回應。
收據
你可以很簡單的透過 invoices
方法取得交易模型的收據資料陣列:
$invoices = $user->invoices();
當列出收據給客戶時,你可以使用收據的輔助方法來顯示收據的相關資訊。舉例來說,你可能希望列出每個收據至表格中,讓使用者可以簡單的下載其中一個:
<table>
@foreach ($invoices as $invoice)
<tr>
<td> {{ $invoice->dateString() }} </td>
<td> {{ $invoice->dollars() }} </td>
<td><a href="/user/invoice/ {{ $invoice->id }} ">下載</a></td>
</tr>
@endforeach
</table>
產生收據的 PDFs
在路由或是控制器中,使用 downloadInvoice
方法可以產生收據的 PDF 下載動作。此方法會自動產生正確的 HTTP 回應並發送下載動作至瀏覽器:
Route::get('user/invoice/{invoice}', function ($invoiceId) {
return Auth::user()->downloadInvoice($invoiceId, [
'vendor' => '你的公司',
'product' => '你的產品',
]);
});