[ Laravel 5.8 文件 ] 官方擴充套件包 —— 訂閱支付解決方案:Laravel Cashier(Stripe)
簡介
Laravel Cashier 為通過Stripe 實現訂閱支付服務提供了一個優雅的流式介面。它封裝了幾乎所有你恐懼編寫的樣板化的訂閱支付程式碼。除了基本的訂閱管理外,Cashier 還支援處理優惠券、訂閱升級/替換、訂閱「數量」、取消寬限期,甚至生成 PDF 發票。
注:該文件適用於 Cashier 與 Stripe 的整合,如果你在使用 Braintree,請參考Braintree 整合文件。
注:如果你只需要一次性支付,並不提供訂閱,就不應該使用 Cashier,而是直接使用 Stripe SDK。
升級
要升級到最新版本的 Cashier,需要仔細閱讀升級指南 。
安裝
首先,通過 Composer 安裝用於 Stripe 的 Cashier 擴充套件包:
composer require laravel/cashier
配置
資料庫遷移
使用 Cashier 之前,我們需要準備好資料庫。我們需要新增一個欄位到users
表,還要建立新的subscriptions
表來處理所有使用者訂閱:
Schema::table('users', function ($table) { $table->string('stripe_id')->nullable(); $table->string('card_brand')->nullable(); $table->string('card_last_four')->nullable(); $table->timestamp('trial_ends_at')->nullable(); }); Schema::create('subscriptions', function ($table) { $table->increments('id'); $table->integer('user_id'); $table->string('name'); $table->string('stripe_id')->collation('utf8mb4_bin'); $table->string('stripe_plan'); $table->integer('quantity'); $table->timestamp('trial_ends_at')->nullable(); $table->timestamp('ends_at')->nullable(); $table->timestamps(); });
建立好遷移後,只需簡單執行migrate
命令,相應修改就會更新到資料庫。
Billable 模型
接下來,新增Billable
trait 到模型定義,這個 trait 提供了多個方法以便執行常用支付任務,例如建立訂閱、使用優惠券以及更新信用卡資訊:
use Laravel\Cashier\Billable; class User extends Authenticatable { use Billable; }
API 金鑰
最後,在配置檔案services.php
中配置 Stripe 的金鑰,你可以在Stripe 官網個人中心的控制面板中獲取這些 Stripe API 金鑰資訊:
'stripe' => [ 'model'=> App\User::class, 'key' => env('STRIPE_KEY'), 'secret' => env('STRIPE_SECRET'), 'webhook' => [ 'secret' => env('STRIPE_WEBHOOK_SECRET'), 'tolerance' => env('STRIPE_WEBHOOK_TOLERANCE', 300), ], ],
貨幣配置
Cashier 預設貨幣是美元(USD),你可以在某個服務提供者的boot
方法中通過呼叫Cashier::useCurrency
方法來更改預設的貨幣,useCurrency
方法接收兩個字串引數 —— 貨幣及貨幣符號:
use Laravel\Cashier\Cashier; Cashier::useCurrency('rmb', '¥');
訂閱
建立訂閱
要建立一個訂閱,首先要獲取一個賬單模型的例項,通常是App\User
的例項。獲取到該模型例項之後,你可以使用newSubscription
方法來建立該模型的訂閱:
$user = User::find(1); $user->newSubscription('main', 'premium')->create($token);
第一個傳遞給newSubscription
方法的引數是該訂閱的名字,如果應用只有一個訂閱,可以將其稱作main
或primary
,第二個引數用於指定使用者訂閱的計劃,該值對應 Stripe 中相應計劃的 id。
create
方法會自動建立這個 Stripe 訂閱,同時更新資料庫中 Stripe 的客戶 ID(即users
表中的stripe_id
)和其它相關的賬單資訊。
額外的使用者資訊
如果你想要指定額外的客戶資訊,你可以將其作為第二個引數傳遞給create
方法:
$user->newSubscription('main', 'monthly')->create($stripeToken, [ 'email' => $email, ]);
要了解更多 Stripe 支援的欄位,可以檢視 Stripe 關於建立消費者的文件 。
優惠券
如果你想要在建立訂閱的時候使用優惠券,可以使用withCoupon
方法:
$user->newSubscription('main', 'monthly') ->withCoupon('code') ->create($token);
檢查訂閱狀態
使用者訂閱你的應用後,你可以使用各種便利的方法來簡單檢查訂閱狀態。首先,如果使用者有一個有效的訂閱,則subscribed
方法返回true
,即使訂閱現在處於試用期:
if ($user->subscribed('main')) { // }
subscribed
方法還可以用於路由中介軟體,基於使用者訂閱狀態允許你對路由和控制器的訪問進行過濾:
public function handle($request, Closure $next){ if ($request->user() && ! $request->user()->subscribed('main')) { // This user is not a paying customer... return redirect('billing'); } return $next($request); }
如果你想要判斷一個使用者是否還在試用期,可以使用onTrial
方法,該方法對於還處於試用期的使用者顯示警告資訊很有用:
if ($user->->subscription('main')->onTrial()) { // }
subscribedToPlan
方法可用於判斷使用者是否基於 Stripe/Braintree ID 訂閱了給定的計劃,在本例中,我們會判斷使用者的main
訂閱是否訂閱了monthly
計劃:
if ($user->subscribedToPlan('monthly', 'main')) { // }
recurring
方法可用於判定使用者當前是否已經訂閱並且不在試用期:
if ($user->subscription('main')->recurring()) { // }
已取消的訂閱狀態
要判斷使用者是否曾經是有效的訂閱者,但現在取消了訂閱,可以使用cancelled
方法:
if ($user->subscription('main')->cancelled()) { // }
你還可以判斷使用者是否曾經取消過訂閱,但現在仍然在「寬限期」直到完全失效。例如,如果一個使用者在3月5號取消了一個實際有效期到3月10號的訂閱,該使用者處於「寬限期」直到3月10號。注意subscribed
方法在此期間仍然返回true
。
if ($user->subscription('main')->onGracePeriod()) { // }
要判斷使用者已經取消訂閱並且不在「寬限期」內,可以使用ended
方法:
if ($user->subscription('main')->ended()) { // }
修改計劃
使用者訂閱應用後,偶爾想要改變到新的訂閱計劃,要將使用者切換到新的訂閱, 傳遞計劃標識到swap
方法:
$user = App\User::find(1); $user->subscription('main')->swap('provider-plan-id');
如果使用者在試用,試用期將會被維護。還有,如果訂閱存在多個,數量也可以被維護。
如果你想要切換計劃並取消使用者所在的所有試用期,你可以使用skipTrial
方法:
$user->subscription('main') ->skipTrial() ->swap('provider-plan-id');
訂閱數量
有時候訂閱也會被數量影響,例如,應用中每個賬戶每月需要付費$10,要簡單增加或減少訂閱數量,使用incrementQuantity
和decrementQuantity
方法:
$user = User::find(1); $user->subscription('main')->incrementQuantity(); // Add five to the subscription's current quantity... $user->subscription('main')->incrementQuantity(5); $user->subscription('main')->decrementQuantity(); // Subtract five to the subscription's current quantity... $user->subscription('main')->decrementQuantity(5);
你也可以使用updateQuantity
方法指定數量:
$user->subscription('main')->updateQuantity(10);
noProrate
方法可用於更新訂閱數量而無需對收費進行評級:
$user->subscription('main')->noProrate()->updateQuantity(10);
想要了解更多訂閱數量資訊,查閱相關Stripe文件 。
訂閱稅金
要指定使用者支付訂閱的稅率,實現賬單模型的taxPercentage
方法,並返回一個在0到100之間的數值,不要超過兩位小數:
public function taxPercentage() { return 20; }
這將使你可以在模型基礎上使用稅率,對跨越不同國家不同稅率的使用者很有用。
注:taxPercentage
方法只能用於訂閱支付,如果你使用 Cashier 生成一次性賬單,需要手動指定稅率。
同步稅率
修改taxPercentage
方法返回的硬編碼值時,該使用者所有現有訂閱上的稅金設定都將保持不變。如果你想要更新taxPercentage
方法返回的現有訂閱上的稅金,需要呼叫使用者訂閱例項上的syncTaxPercentage
方法:
$user->subscription('main')->syncTaxPercentage();
訂閱錨定日期
注:只有 Stripe 版本的 Cashier 支援修改訂閱錨定日期。
預設情況下,支付週期錨點就是訂閱建立的日期,或者如果使用了試用期的話,該日期就是訂閱期結束的日子。如果你想要編輯支付錨定日期,可以使用anchorBillingCycleOn
方法:
use App\User; use Carbon\Carbon; $user = User::find(1); $anchor = Carbon::parse('first day of next month'); $user->newSubscription('main', 'premium') ->anchorBillingCycleOn($anchor->startOfDay()) ->create($token);
想要了解更多管理訂閱支付週期的資訊,可以參考Stripe 支付週期文件 。
取消訂閱
要取消訂閱,可以呼叫使用者訂閱上的cancel
方法:
$user->subscription('main')->cancel();
當訂閱被取消時,Cashier 將會自動設定資料庫中的ends_at
欄位。該欄位用於瞭解subscribed
方法什麼時候開始返回false
。例如,如果客戶3月1號份取消訂閱,但訂閱直到3月5號才會結束,那麼subscribed
方法繼續返回true
直到3月5號。
你可以使用onGracePeriod
方法判斷使用者是否已經取消訂閱但仍然在“寬限期”:
if ($user->subscription('main')->onGracePeriod()) { // }
如果你想要立即取消訂閱,呼叫使用者訂閱上的方法cancelNow
即可:
$user->subscription('main')->cancelNow();
恢復訂閱
如果使用者已經取消訂閱但想要恢復該訂閱,可以使用resume
方法,前提是該使用者必須在寬限期內:
$user->subscription('main')->resume();
如果該使用者取消了一個訂閱然後在訂閱失效之前恢復了這個訂閱,則不會立即支付該賬單,取而代之的,他們的訂閱只是被重新啟用,並回到正常的支付週期。
訂閱試用期
帶信用卡資訊
如果你想要在提供給使用者試用期的同時預先收集支付方式資訊,可以在建立訂閱的時候使用trialDays
方法:
$user = User::find(1); $user->newSubscription('main', 'monthly') ->trialDays(10) ->create($token);
該方法會在資料庫訂閱記錄上設定試用期結束日期,以便告知 Stripe/Braintree 在此之前不要計算使用者的賬單資訊。
注:如果使用者的訂閱沒有在試用期結束之前取消,則會在試用期結束時立即支付,所以要確保通知使用者試用期結束時間。
trialUntil
方法允許你提供一個DateTime
例項來指定試用期什麼時候結束:
use Carbon\Carbon; $user->newSubscription('main', 'monthly') ->trialUntil(Carbon::now()->addDays(10)) ->create($token);
可以使用使用者例項或訂閱例項上的onTrial
方法判斷使用者是否處於試用期,下面兩個例子作用是等價的:
if ($user->onTrial('main')) { // } if ($user->subscription('main')->onTrial()) { // }
不帶信用卡資訊
如果你不想在提供試用期的時候收集使用者支付方式資訊,只需設定使用者記錄的trial_ends_at
欄位為期望的試用期結束日期即可,這通常在使用者註冊期間完成:
$user = User::create([ // Populate other user properties... 'trial_ends_at' => Carbon::now()->addDays(10), ]);
注:確保已新增trial_ends_at
日期修改器到模型定義。
Cashier 將這種型別的試用期看作“一般體驗”,因為這種使用並沒有附加到任何已經在的訂閱,如果當前日期沒有超過trial_ends_at
的值,User
例項上的onTrial
方法將返回true
:
if ($user->onTrial()) { // User is within their trial period... }
如果你想要知道使用者是否在“一般”試用期並且還沒有建立實際的訂閱還可以使用onGenericTrial
方法:
if ($user->onGenericTrial()) { // User is within their "generic" trial period... }
一旦你準備好為使用者建立實際的訂閱,可以使用newSubscription
方法:
$user = User::find(1); $user->newSubscription('main', 'monthly')->create($token);
顧客
建立顧客
少數情況下,你可能希望建立一個沒有開始訂閱的 Stripe 顧客,這可以通過createAsStripeCustomer
方法來實現:
$user->createAsStripeCustomer();
一旦在 Stripe 中建立這樣的顧客後,需要在稍晚時候讓他開始訂閱。
信用卡
獲取信用卡資訊
支付模型例項的cards
方法會返回Laravel\Cashier\Card
例項集合:
$cards = $user->cards();
要獲取預設信用卡,可以使用defaultCard
方法:
$card = $user->defaultCard();
判斷使用者是否綁定了信用卡
你可以使用hasCardOnFile
方法檢查顧客是否綁定了信用卡:
if ($user->hasCardOnFile()) { // }
更新信用卡
updateCard
方法可用於更新使用者的信用卡資訊,該方法接收一個 Stripe 令牌並設定新的信用卡作為支付源:
$user->updateCard($token);
要通過在 Stripe 中預設的卡資訊同步你的卡資訊,可以使用updateCardFromStripe
方法:
$user->updateCardFromStripe();
刪除信用卡
要刪除一張信用卡,首先需要通過cards
方法獲取顧客的信用卡,然後,可以呼叫要刪除的卡對應例項的delete
方法進行刪除操作:
foreach ($user->cards() as $card) { $card->delete(); }
注:如果你刪除了預設的信用卡,請確保使用updateCardFromStripe
方法同步新的預設信用卡到資料庫。
deleteCards
方法將會刪除應用儲存的所有卡資訊:
$user->deleteCards();
注:如果使用者有一個有效的訂閱,需要考慮如何避免刪除最後剩餘的付款來源。
處理 Stripe Webhooks
Stripe 可以通過 webhooks 通知應用各種事件,要處理 Stripe webhooks,需要定義一個指向 Cashier webhook 控制器的路由,這個控制器將會處理所有輸入 webhook 請求並將它們分發到合適的控制器方法:
Route::post( 'stripe/webhook', '\Laravel\Cashier\Http\Controllers\WebhookController@handleWebhook' );
注:註冊好控制器後,還要在 Stripe 控制面板中配置 webhook URL。
預設情況下,這個控制器將會自動對支付失敗次數(這個次數可以在 Stripe 設定中定義)過多的訂閱進行取消,自動處理顧客更新、刪除,以及訂閱更新和信用卡更新等操作;此外,我們很快會發現,你可以擴充套件這個控制器來處理任何你想要處理的 webhook 事件。
注:確保通過 Cashier 引入的webhook 簽名驗證中介軟體對輸入請求進行保護。
Webhooks & CSRF 防護
由於 Stripe webhook 需要繞開 Laravel 的CSRF 保護,所以需要將其羅列到VerifyCsrfToken
中介軟體的排除列表或者將其置於web
中介軟體組之外:
protected $except = [ 'stripe/*', ];
定義 Webhook 事件處理器
Cashier 會基於支付失敗次數自動取消訂閱,但是如果你想要處理額外的 Stripe webhook 事件,擴充套件 Webhook 控制器即可。定義的方法名需要與 Cashier 約定的格式保持一致,特別是方法名需要以handle
開頭並且是想要處理的 Stripe webhook 的駝峰格式。例如,如果你想要處理invoice.payment_succeeded
webhook,則需要新增一個handleInvoicePaymentSucceeded
方法到控制器:
<?php namespace App\Http\Controllers; use Laravel\Cashier\Http\Controllers\WebhookController as CashierController; class WebhookController extends CashierController { /** * Handle a Stripe webhook. * * @paramarray$payload * @return Response */ public function handleInvoicePaymentSucceeded($payload) { // Handle The Event } }
接下來,在routes/web.php
中定義一個指向 Cashier 控制器的路由:
Route::post( 'stripe/webhook', '\App\Http\Controllers\WebhookController@handleWebhook' );
失敗的訂閱
如果使用者的信用卡過期怎麼辦?不用擔心 —— Cashier webhook 控制器可以輕鬆為你取消該使用者的訂閱,正如上面所提到的,你所需要做的只是將路由指向該控制器:
Route::post( 'stripe/webhook', '\Laravel\Cashier\Http\Controllers\WebhookController@handleWebhook' );
就是這樣,失敗的支付將會被控制器捕獲和處理,該控制器將會在 Stripe 判斷訂閱支付失敗次數(通常是3次)達到上限時取消該使用者的訂閱。
驗證 Webhook 簽名
如果要對 webhook 進行安全加固,可以使用Stripe 的 webhook 簽名 。為了方便起見,Cashier 會自動引入驗證輸入的 Stripe webhook 請求是否有效的中介軟體。
要啟用 webhook 驗證,確保配置檔案services.php
中的stripe.webhook.secret
配置被設定,改配置值可以從 Stripe 賬戶後臺面板中獲取。
一次性支付
簡單支付
注:使用 Stripe 時,charge
方法可以接收應用所使用貨幣對應的最小單位金額。
如果你想要使用訂閱客戶的信用卡一次性結清賬單,可以使用賬單模型例項上的charge
方法:
// Stripe Accepts Charges In Cents... $user->charge(100); // Braintree Accepts Charges In Dollars... $user->charge(1);
charge
方法接收一個數組作為第二個引數,允許你傳遞任何你想要傳遞的底層 Stripe 賬單建立引數,建立賬單時我們可以參考 Stripe 文件提供的可用選項:
$user->charge(100, [ 'custom_option' => $value, ]);
如果支付失敗charge
方法將丟擲異常,如果支付成功,該方法會返回完整的 Stripe 響應:
try { $response = $user->charge(100); } catch (Exception $e) { // }
帶發票的支付
有時候你需要建立一個一次性支付並且同時生成對應發票以便為使用者提供一個PDF單據,invoiceFor
方法可以幫助我們實現這個需求。例如,讓我們為使用者的“一次性費用”生成一張$5.00的發票:
// Stripe Accepts Charges In Cents... $user->invoiceFor('One Time Fee', 500);
該單據會通過使用者信用卡立即支付,invoiceFor
方法還可以接收一個數組作為第三個引數,該陣列包含發票專案的計費選項。該方法的第四個引數也是一個數組,包含的是發票本身的計費選項:
$user->invoiceFor('Stickers', 500, [ 'quantity' => 50, ], [ 'tax_percent' => 21, ]);
注:invoiceFor
方法會建立一個對失敗支付進行重試的 Stripe 單據,如果你不想要單據重試失敗的支付,需要在首次支付失敗後使用 Stripe API 關閉它們。
退款
如果你需要對 Stripe 支付進行退款,可以使用refund
方法,該方法接收 Stripe 支付 ID 作為唯一引數:
$stripeCharge = $user->charge(100); $user->refund($stripeCharge->id);
發票
你可以使用invoices
方法輕鬆獲取賬單模型的發票陣列:
$invoices = $user->invoices(); // Include pending invoices in the results... $invoices = $user->invoicesIncludingPending();
當列出客戶發票時,你可以使用發票的輔助函式來顯示相關的發票資訊。例如,你可能想要在表格中列出每張發票,從而方便使用者下載它們:
<table> @foreach ($invoices as $invoice) <tr> <td>{{ $invoice->date()->toFormattedDateString() }}</td> <td>{{ $invoice->total() }}</td> <td><a href="/user/invoice/{{ $invoice->id }}">Download</a></td> </tr> @endforeach </table>
生成 PDF 發票
在路由或控制器中,使用downloadInvoice
方法生成發票的 PDF 下載,該方法將會自動生成相應的 HTTP 響應傳送下載到瀏覽器:
use Illuminate\Http\Request; Route::get('user/invoice/{invoice}', function (Request $request, $invoiceId) { return $request->user()->downloadInvoice($invoiceId, [ 'vendor'=> 'Your Company', 'product' => 'Your Product', ]); });