Laravel Cashier
简介
Laravel 为 Stripe 和 Braintree 的订阅账单服务提供了直观流畅的接口。它几乎处理了所有害怕编写的固定格式的订阅账单代码。除了基本的订阅管理外,Cashier 还可以处理优惠券,更改订阅,订阅「数量」,取消宽限期,甚至生成发票 PDF。
如果只进行「一次性」收费并且不提供订阅,那么不应该使用 Cashier。而是,直接使用 Stripe 和 Braintree 的 SDK。
配置
Stripe
Composer
首先,将 Stripe 的 Cashier 扩展包添加到依赖中:
composer require "laravel/cashier":"~7.0"
数据库迁移
使用 Stripe 前,还需要 准备数据库。我们需要添加几个字段到 users
数据表,并创建一个新的存放所有客户订阅的 subscriptions
数据表:
Schema::table('users', function ($table) {
$table->string('stripe_id')->nullable()->collation('utf8mb4_bin');
$table->string('card_brand')->nullable();
$table->string('card_last_four', 4)->nullable();
$table->timestamp('trial_ends_at')->nullable();
});
Schema::create('subscriptions', function ($table) {
$table->increments('id');
$table->unsignedInteger('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();
});
创建迁移后,运行 Artisan 命令 migrate
。
Billable 模型
接下来,在模型定义中添加 Billable
Trait。此 Trait 提供各种方法来完成常见的账单任务,例如创建订阅,使用优惠券和更新信用卡信息:
use Laravel\Cashier\Billable;
class User extends Authenticatable
{
use Billable;
}
API Keys
最后,在 services.php
配置文件中配置 Stripe 密钥。可以从 Stripe 控制面板获取 Stripe API 密钥:
'stripe' => [
'model' => App\User::class,
'key' => env('STRIPE_KEY'),
'secret' => env('STRIPE_SECRET'),
],
Braintree
Braintree 附加说明
对于很多操作,Cashier 的 Stripe 和 Braintree 是一样的。两项服务都提供了使用信用卡订阅账单,但 Braintree 还支持通过 PayPal 支付。当然,Braintree 也缺少一些 Stripe 支持的功能。在决定使用 Stripe 或 Braintree 时,应该记住以下几点:
- Braintree 支持 PayPal 而 Stripe 不支持。
- Braintree 不支持订阅的
increment
和decrement
方法。这是 Braintree 的限制,而 Cashier 不限制。 - Braintree 不支持基于百分比的折扣。这是 Braintree 的限制,而 Cashier 不限制。
Composer
首先,将 Braintree 的 Cashier 扩展包添加到依赖:
composer require "laravel/cashier-braintree":"~2.0"
服务提供者
接下来,在 config/app.php
配置文件中注册 Laravel\Cashier\CashierServiceProvider
服务提供者:
Laravel\Cashier\CashierServiceProvider::class
信用卡优惠计划
在使用 Cashier 的 Braintree 前,需要在 Braintree 控制面板定义 plan-credit
折扣。此折扣将用于正确地分摊给从年更改到月,或从月更改到年的订阅。
在 Braintree 控制面板中配置的折扣金额可以是您希望的任何值,因为每次我们使用优惠券时,Cashier 都会使用自定义金额覆盖定义的金额。因为 Braintree 本身不支持按订阅频率分摊订阅费,所以需要优惠券。
数据库迁移
使用 Cashier 前,还需要 准备数据库。我们需要添加几个字段到 users
数据表,并创建一个新的存放所有客户订阅的 subscriptions
数据表:
Schema::table('users', function ($table) {
$table->string('braintree_id')->nullable();
$table->string('paypal_email')->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->unsignedInteger('user_id');
$table->string('name');
$table->string('braintree_id');
$table->string('braintree_plan');
$table->integer('quantity');
$table->timestamp('trial_ends_at')->nullable();
$table->timestamp('ends_at')->nullable();
$table->timestamps();
});
创建迁移后,运行 Artisan 命令 migrate
。
Billable 模型
接下来,在模型定义中添加 Billable
Trait:
use Laravel\Cashier\Billable;
class User extends Authenticatable
{
use Billable;
}
API Keys
接着,在 services.php
配置文件中配置下列选项:
'braintree' => [
'model' => App\User::class,
'environment' => env('BRAINTREE_ENV'),
'merchant_id' => env('BRAINTREE_MERCHANT_ID'),
'public_key' => env('BRAINTREE_PUBLIC_KEY'),
'private_key' => env('BRAINTREE_PRIVATE_KEY'),
],
然后,在 AppServiceProvider
服务提供者的 boot
方法中添加下列 Braintree SDK 调用:
\Braintree_Configuration::environment(config('services.braintree.environment'));
\Braintree_Configuration::merchantId(config('services.braintree.merchant_id'));
\Braintree_Configuration::publicKey(config('services.braintree.public_key'));
\Braintree_Configuration::privateKey(config('services.braintree.private_key'));
货币配置
默认的 Cashier 货币是美元(USD)。可以在一个服务提供者的 boot
方法中调用 Cashier::useCurrency
方法改变默认货币。useCurrency
方法接收两个字符串参数:货币和货币符号。
use Laravel\Cashier\Cashier;
Cashier::useCurrency('eur', '€');
订阅
创建订阅
要创建订阅,首先获取一个 Billable 模型实例,通常会是 App\User
的实例。获取模型实例后,可以使用 newSubscription
方法创建模型的订阅:
$user = User::find(1);
$user->newSubscription('main', 'premium')->create($stripeToken);
第一个传递给 newSubscription
方法的参数是订阅名称。如果应用只提供单个订阅,可以称其为 main
或 primary
。第二个参数是用户订阅的具体的 Stripe / Braintree 计划。此值对应于 Stripe 或 Braintree 中的计划标识符。
接收 Stripe 信用卡/源令牌的 create
方法会开始订阅,同时将客户 ID 和其它相关账单信息更新到数据库中。
用户其它详情
如果想要指定其它客户详情,可以通过将其作为第二个参数传递给 create
方法:
$user->newSubscription('main', 'monthly')->create($stripeToken, [
'email' => $email,
]);
要了解 Stripe 或 Braintree 支持的更多其它字段的更多信息,可以查看 Stripe 的 创建客户文档 或对应的 Braintree 文档。
优惠券
如果想要在创建订阅时使用优惠券,可以使用 withCoupon
方法:
$user->newSubscription('main', 'monthly')
->withCoupon('code')
->create($stripeToken);
检查订阅状态
用户订阅应用后,可以使用各种方便的方法轻松检查其订阅状态。首先,如果用户有一个有效的订阅,即使订阅当前在试用期,subscribed
方法会返回 true
:
if ($user->subscribed('main')) {
//
}
subscribed
方法也是 路由中间件 的理想选择,允许您根据用户的订阅状态过滤对路由器和控制器的访问:
public function handle($request, Closure $next)
{
if ($request->user() && ! $request->user()->subscribed('main')) {
// 此用户不是付款客户
return redirect('billing');
}
return $next($request);
}
如果想要判断用户是否仍在试用期,可以使用 onTrial
方法。此方法可用于向用户显示他们仍处于试用期的警告:
if ($user->subscription('main')->onTrial()) {
//
}
subscribedToPlan
方法可用于判断用户是否基于给定 Stripe / Braintree 计划 ID 订阅了给定计划。在此示例中,我们会判断用户的 main
订阅是否有效订阅了 monthly
计划:
if ($user->subscribedToPlan('monthly', 'main')) {
//
}
取消的订阅状态
要判断用户是否曾经是一个有效的订阅者,但是已经取消了订阅,可以使用 cancelled
方法:
if ($user->subscription('main')->cancelled()) {
//
}
还可以判断用户是否已取消了订阅,但仍处于「宽限期」直到订阅完全过期。例如,如果用户在 3 月 5 号取消了原定于 3 月 10 号到期的订阅,用户会在 3 月 10 号前一直处于「宽限期」。需要注意的是,subscribed
方法在此期间仍会返回 true
:
if ($user->subscription('main')->onGracePeriod()) {
//
}
更改计划
用户订阅应用后,可能有时要更改为新的订阅计划。要更改为新的订阅,可以将计划的标识符传递给 swap
方法:
$user = App\User::find(1);
$user->subscription('main')->swap('provider-plan-id');
如果用户在试用期,会保留试用期。同样,如果订阅存在「数量」,适量也会保留。
如果想要更改计划并取消用户当前所在的任何试用期,可以使用 skipTrial
方法:
$user->subscription('main')
->skipTrial()
->swap('provider-plan-id');
订阅数量
订阅数量只支持 Stripe 版本的 Cashier。Braintree 没有与 Stripe 的「数量」相对应的功能。
有时订阅还受「数量」的影响。例如,应用可能会在账户上收取每个用户 $10。要轻松增加或减少订阅数量,可以使用 incrementQuantity
和 decrementQuantity
方法:
$user = User::find(1);
$user->subscription('main')->incrementQuantity();
// 当前订阅数量加五
$user->subscription('main')->incrementQuantity(5);
$user->subscription('main')->decrementQuantity();
// 当前订阅数量减五
$user->subscription('main')->decrementQuantity(5);
或者,还可以使用 updateQuantity
方法设置指定数量:
$user->subscription('main')->updateQuantity(10);
noProrate
方法可用于更新订阅数量,而不对收费进行评级:
$user->subscription('main')->noProrate()->updateQuantity(10);
有关订阅数量的更多信息,可以查看 Stripe 文档。
订阅税费
要指定用户为订阅支付的税率,可以在 Billable 模型上实现 taxPercentage
方法,并返回不超过 2 个小数位的 0 到 100 的数值。
public function taxPercentage() {
return 20;
}
taxPercentage
方法允许您将税率逐个应用到模型上,这可能对跨越多个国家和税率的用户有帮助。
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($stripeToken);
有关管理订阅结算周期,可以查看 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();
如果用户取消订阅,然后在订阅完全过期前恢复该订阅,不会立即向他们收费。相反,他们的订阅将被重新激活,并且按原始结算周期计费。
更新信用卡
updateCard
方法可用于更新客户的信用卡信息。此方法接收一个 Stripe 令牌,并将新信用卡设置为默认结算来源:
$user->updateCard($stripeToken);
试用订阅
有信用卡
如果想要在预先收集付款方式信息的同时向用户提供试用期,可以在创建订阅时使用 trialDays
方法:
$user = User::find(1);
$user->newSubscription('main', 'monthly')
->trialDays(10)
->create($stripeToken);
此方法会在数据库中的订阅记录上设置试用期结束日期,并指示 Stripe / Braintree 在此日期后才开始向客户开账单。
如果客户的订阅在试用结束日期之前未被取消,则会在试用期结束后立即收取费用,因此要确保通知用户其试用结束日期。
trialUntil
方法允许您提供 DateTime
实例指定试用期应在何时结束:
use Carbon\Carbon;
$user->newSubscription('main', 'monthly')
->trialUntil(Carbon::now()->addDays(10))
->create($stripeToken);
您可以使用用户实例的 onTrial
方法,或订阅实例的 onTrial
方法判断用户是否在试用期内。以下两个示例是等效的:
if ($user->onTrial('main')) {
//
}
if ($user->subscription('main')->onTrial()) {
//
}
没有信用卡
如果想要在未预先手机用户付款方式信息的情况下提供试用期,可以将用户记录中的 trial_ends_at
字段设置为所需的试用结束日期。这通常在用户注册时完成:
$user = User::create([
// 设置其它用户属性
'trial_ends_at' => now()->addDays(10),
]);
确保在模型定义时为
trial_ends_at
添加了 日期修改器。
Cashier 将此类试用称为「通用试用」,因为它不添加到任何已有订阅。如果当前日期不是没有超过 trial_ends_at
的值,User
实例上的 onTrial
方法将返回 true
:
if ($user->onTrial()) {
// 用户在试用期中
}
如果希望明确知道用户处于「通用」试用期并且尚未创建实际订阅,也可以使用 onGenericTrial
方法:
if ($user->onGenericTrial()) {
// 用户在「通用」试用期中
}
准备好为用户创建实际订阅后,可以像往常一样使用 newSubscription
方法:
$user = User::find(1);
$user->newSubscription('main', 'monthly')->create($stripeToken);
客户
创建客户
有时可能希望在不开始订阅的情况下创建 Stripe 客户。可以试用 createAsStripeCustomer
方法完成此操作:
$user->createAsStripeCustomer($stripeToken);
当然,在 Stripe 中创建客户后,可以在稍后开始订阅。
Braintree 中和此方法等效的是
createAsBraintreeCustomer
方法。
处理 Stripe Webhooks
Stripe 和 Braintree 都可以通过 Webhook 向应用通知各种事件。要处理 Stripe Webhook,可以指定指向 Cashier 的 Webhook 控制器的路由。此控制器会处理所有传入的 Webhook 请求并将其分发到适当的控制器方法:
Route::post(
'stripe/webhook',
'\Laravel\Cashier\Http\Controllers\WebhookController@handleWebhook'
);
注册路由后,确保在 Stripe 控制面板设置中配置 Webhook URL。
默认情况下,此控制器会自动处理取消订阅,这些订阅包含太多的失败请求(通过 Stripe 设置定义);但是,我们会很快发现,您可以继承此控制器处理要处理的任何 Webhook。
Webhooks & CSRF 保护
由于 Stripe Webhooks 需要绕过 Laravel 的 CSRF 保护,确保将 URI 排除在 VerifyCsrfToken
中间件之外或者将路由列在 web
中间件组之外:
protected $except = [
'stripe/*',
];
定义 Webhook 事件处理程序
Cashier 会自动处理失败请求的取消订阅,但是如果有想要处理的其它 Stripe Webhook 事件,可以继承 Webhook 控制器。方法名应该与 Cashier 预期约定相对应,具体而言,方法应以 handle
为希望处理的 Stripe Webhook 名的「驼峰命名法」的前缀。例如,如果希望处理 invoice.payment_succeeded
Webhook,可以在控制器中添加 handleInvoicePaymentSucceeded
方法:
namespace App\Http\Controllers;
use Laravel\Cashier\Http\Controllers\WebhookController as CashierController;
class WebhookController extends CashierController
{
/**
* 处理 Stripe Webhook.
*
* @param array $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 确定订阅失败时(通常在三次付款尝试失败后),控制器将取消客户的订阅。
验证 Webhook 签名
要保护 Webhook,可以使用 Stripe 的 Webhook 签名。为方便起见,Cashier 包含一个中间件,验证传入的 Stripe Webhook 请求是有效的。
首先,确保在 services
配置文件中设置了 stripe.webhook.secret
配置值。配置 Webhook 秘钥后,可以将 VerifyWebhookSignature
中间件添加到路由:
use Laravel\Cashier\Http\Middleware\VerifyWebhookSignature;
Route::post(
'stripe/webhook',
'\App\Http\Controllers\WebhookController@handleWebhook'
)->middleware(VerifyWebhookSignature::class);
处理 Braintree Webhooks
Stripe 和 Braintree 都可以通过 Webhook 向应用通知各种事件。要处理 Braintree Webhook,可以指定指向 Cashier 的 Webhook 控制器的路由。此控制器会处理所有传入的 Webhook 请求并将其分发到适当的控制器方法:
Route::post(
'braintree/webhook',
'\Laravel\Cashier\Http\Controllers\WebhookController@handleWebhook'
);
注册路由后,确保在 Braintree 控制面板设置中配置 Webhook URL。
默认情况下,此控制器会自动处理取消订阅,这些订阅包含太多的失败请求(通过 Braintree 设置定义);但是,我们会很快发现,您可以继承此控制器处理要处理的任何 Webhook。
Webhooks & CSRF 保护
由于 Braintree Webhooks 需要绕过 Laravel 的 CSRF 保护,确保将 URI 排除在 VerifyCsrfToken 中间件之外或者将路由列在 web 中间件组之外:
protected $except = [
'braintree/*',
];
定义 Webhook 事件处理程序
Cashier 会自动处理失败请求的取消订阅,但是如果有想要处理的其它 Braintree Webhook 事件,可以继承 Webhook 控制器。方法名应该与 Cashier 预期约定相对应,具体而言,方法应以 handle 为希望处理的 Braintree Webhook 名的「驼峰命名法」的前缀。例如,如果希望处理 dispute_opened
Webhook,可以在控制器中添加 handleDisputeOpened
方法:
namespace App\Http\Controllers;
use Braintree\WebhookNotification;
use Laravel\Cashier\Http\Controllers\WebhookController as CashierController;
class WebhookController extends CashierController
{
/**
* 处理 Braintree Webhook
*
* @param WebhookNotification $webhook
* @return Response
*/
public function handleDisputeOpened(WebhookNotification $notification)
{
// Handle The Event
}
}
订阅失败
如果客户的信用卡到期怎么办?不用担心 —— Cashier 包含一个 Webhook 控制器可以轻松取消客户的订阅。如上所述,只需将路由指定控制器:
Route::post(
'braintree/webhook',
'\Laravel\Cashier\Http\Controllers\WebhookController@handleWebhook'
);
仅此而已!控制器将捕获并处理失败的付款。当 Stripe 确定订阅失败时(通常在三次付款尝试失败后),控制器将取消客户的订阅。不要忘记:您需要在 Braintree 控制面板设置中配置 Webhook URI。
单次收费
简单收费
当使用 Stripe 时,
charge
方法接收想要以应用使用的货币的最小单位支付的金额。然而,当使用 Braintree 时,应该传递完整的美元金额给charge
方法。
如果想要对订阅的客户的信用卡进行「一次性」收费,可以在 Billable 模型实例上使用 charge
方法:
// Stripe 按分接收费用
$stripeCharge = $user->charge(100);
// Braintree 按美元接收费用
$user->charge(1);
charge
方法接收一个数组作为其第二个参数,允许您创建付款时将任何希望的选项传递给底层的 Stripe / Braintree。有关创建收费时可用的选项,可以查看 Stripe 或 Braintree 文档:
$user->charge(100, [
'custom_option' => $value,
]);
如果收费失败,charge
方法会抛出一个异常。如果收费成功,会从此方法返回完整的 Stripe / Braintree 响应:
try {
$response = $user->charge(100);
} catch (Exception $e) {
//
}
收费并生成发票
有时可能需要一次性收费时还要生成收费发票,以便向客户提供 PDF 收据。invoiceFor
方法允许您完成此操作。例如,我们向客户开具 $5.00 的「一次性费用」发票:
// Stripe 按分接收费用
$user->invoiceFor('One Time Fee', 500);
// Braintree 按美元接收费用
$user->invoiceFor('One Time Fee', 5);
该发票会立即通过用户的信用卡收费。invoiceFor
方法也接收一个数组作为其第三个参数,允许您创建付款时将任何希望的选项传递给底层的 Stripe / Braintree :
$user->invoiceFor('One Time Fee', 500, [
'custom-option' => $value,
]);
如果使用 Braintree 作为结算提供者,在调用 invoiceFor
方法时必须包含 description
选项:
$user->invoiceFor('One Time Fee', 500, [
'description' => 'your invoice description here',
]);
invoiceFor
方法会创建 Stripe 发票,此发票将会在付款失败后重试。如果不想失败后重试,需要在第一次付款失败后使用 Stripe API 关闭它们。
退还收费
如果需要退还 Stripe 收费,可以使用 refund
方法。此方法接收 Stripe 收费 ID 作为其唯一参数:
$stripeCharge = $user->charge(100);
$user->refund($stripeCharge->id);
发票
可以使用 invoices
方法轻松获取 Billable 模型的发票数组:
$invoices = $user->invoices();
// 将处理中的发票包含到结果中
$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',
]);
});