Skip to content

Laravel Cashier

介绍

Laravel Cashier 提供了一个对 StripeBraintree 订阅计费服务的表达性、流畅的接口。它处理了几乎所有你不想写的订阅计费样板代码。除了基本的订阅管理,Cashier 还可以处理优惠券、交换订阅、订阅“数量”、取消宽限期,甚至生成发票 PDF。

Stripe 配置

Composer

首先,将 Stripe 的 Cashier 包添加到你的 composer.json 文件中,并运行 composer update 命令:

php
"laravel/cashier": "~6.0"

服务提供者

接下来,在你的 app 配置文件中注册 Laravel\Cashier\CashierServiceProvider 服务提供者

数据库迁移

在使用 Cashier 之前,我们还需要准备数据库。我们需要在 users 表中添加几个列,并创建一个新的 subscriptions 表来保存所有客户的订阅:

php
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');
    $table->string('stripe_plan');
    $table->integer('quantity');
    $table->timestamp('trial_ends_at')->nullable();
    $table->timestamp('ends_at')->nullable();
    $table->timestamps();
});

创建迁移后,只需运行 migrate Artisan 命令。

模型设置

接下来,将 Billable trait 添加到你的模型定义中:

php
use Laravel\Cashier\Billable;

class User extends Authenticatable
{
    use Billable;
}

提供者密钥

接下来,你应该在 services.php 配置文件中配置你的 Stripe 密钥:

php
'stripe' => [
    'model'  => App\User::class,
    'secret' => env('STRIPE_SECRET'),
],

Braintree 配置

Braintree 注意事项

对于许多操作,Cashier 的 Stripe 和 Braintree 实现功能相同。两种服务都提供信用卡订阅计费,但 Braintree 还支持通过 PayPal 付款。然而,Braintree 也缺乏一些 Stripe 支持的功能。在决定使用 Stripe 或 Braintree 时,你应该记住以下几点:

  • Braintree 支持 PayPal,而 Stripe 不支持。
  • Braintree 不支持订阅的 incrementdecrement 方法。这是 Braintree 的限制,而不是 Cashier 的限制。
  • Braintree 不支持基于百分比的折扣。这是 Braintree 的限制,而不是 Cashier 的限制。

Composer

首先,将 Braintree 的 Cashier 包添加到你的 composer.json 文件中,并运行 composer update 命令:

php
"laravel/cashier-braintree": "~1.0"

服务提供者

接下来,在你的 app 配置文件中注册 Laravel\Cashier\CashierServiceProvider 服务提供者

计划信用优惠券

在使用 Braintree 的 Cashier 之前,你需要在 Braintree 控制面板中定义一个 plan-credit 折扣。此折扣将用于正确按比例调整从年度到月度计费或从月度到年度计费的订阅。Braintree 控制面板中配置的折扣金额可以是你希望的任何值,因为 Cashier 每次应用优惠券时都会简单地覆盖定义的金额。

数据库迁移

在使用 Cashier 之前,我们还需要准备数据库。我们需要在 users 表中添加几个列,并创建一个新的 subscriptions 表来保存所有客户的订阅:

php
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->integer('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();
});

创建迁移后,只需运行 migrate Artisan 命令。

模型设置

接下来,将 Billable trait 添加到你的模型定义中:

php
use Laravel\Cashier\Billable;

class User extends Authenticatable
{
    use Billable;
}

提供者密钥

接下来,你应该在 services.php 文件中配置以下选项:

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 调用:

php
\Braintree_Configuration::environment(env('BRAINTREE_ENV'));
\Braintree_Configuration::merchantId(env('BRAINTREE_MERCHANT_ID'));
\Braintree_Configuration::publicKey(env('BRAINTREE_PUBLIC_KEY'));
\Braintree_Configuration::privateKey(env('BRAINTREE_PRIVATE_KEY'));

订阅

创建订阅

要创建订阅,首先检索你的可计费模型的实例,通常是 App\User 的实例。一旦你检索到模型实例,你可以使用 newSubscription 方法来创建模型的订阅:

php
$user = User::find(1);

$user->newSubscription('main', 'monthly')->create($creditCardToken);

传递给 newSubscription 方法的第一个参数应该是订阅的名称。如果你的应用程序只提供一个订阅,你可以称之为 mainprimary。第二个参数是用户订阅的特定 Stripe / Braintree 计划。此值应对应于 Stripe 或 Braintree 中计划的标识符。

create 方法将自动创建订阅,并更新你的数据库中的客户 ID 和其他相关的计费信息。

额外的用户详细信息

如果你想指定额外的客户详细信息,可以通过将它们作为第二个参数传递给 create 方法:

php
$user->newSubscription('main', 'monthly')->create($creditCardToken, [
    'email' => $email,
]);

要了解 Stripe 或 Braintree 支持的其他字段,请查看 Stripe 的客户创建文档或相应的Braintree 文档

优惠券

如果你想在创建订阅时应用优惠券,可以使用 withCoupon 方法:

php
$user->newSubscription('main', 'monthly')
     ->withCoupon('code')
     ->create($creditCardToken);

检查订阅状态

一旦用户订阅了你的应用程序,你可以使用各种方便的方法轻松检查他们的订阅状态。首先,如果用户有一个活跃的订阅,即使订阅当前处于试用期,subscribed 方法也会返回 true

php
if ($user->subscribed('main')) {
    //
}

subscribed 方法也可以作为路由中间件的一个很好的候选者,允许你根据用户的订阅状态过滤对路由和控制器的访问:

php
public function handle($request, Closure $next)
{
    if ($request->user() && ! $request->user()->subscribed('main')) {
        // 该用户不是付费客户...
        return redirect('billing');
    }

    return $next($request);
}

如果你想确定用户是否仍在试用期内,可以使用 onTrial 方法。此方法可用于向用户显示他们仍在试用期内的警告:

php
if ($user->subscription('main')->onTrial()) {
    //
}

subscribedToPlan 方法可用于根据给定的 Stripe / Braintree 计划 ID 确定用户是否订阅了给定的计划。在此示例中,我们将确定用户的 main 订阅是否积极订阅了 monthly 计划:

php
if ($user->subscribedToPlan('monthly', 'main')) {
    //
}

取消的订阅状态

要确定用户是否曾经是活跃的订阅者,但已取消他们的订阅,可以使用 cancelled 方法:

php
if ($user->subscription('main')->cancelled()) {
    //
}

你还可以确定用户是否已取消他们的订阅,但仍在他们的“宽限期”内,直到订阅完全过期。例如,如果用户在 3 月 5 日取消订阅,而订阅原定于 3 月 10 日到期,则用户在 3 月 10 日之前处于“宽限期”。请注意,在此期间,subscribed 方法仍然返回 true

php
if ($user->subscription('main')->onGracePeriod()) {
    //
}

更改计划

用户订阅你的应用程序后,他们可能偶尔想要更改为新的订阅计划。要将用户切换到新的订阅,请使用 swap 方法。例如,我们可以轻松地将用户切换到 premium 订阅:

php
$user = App\User::find(1);

$user->subscription('main')->swap('provider-plan-id');

如果用户处于试用期,试用期将保持不变。此外,如果订阅存在“数量”,该数量也将保持不变:

php
$user->subscription('main')->swap('provider-plan-id');

如果你想更换计划但跳过你要更换到的计划的试用期,可以使用 skipTrial 方法:

php
$user->subscription('main')
        ->skipTrial()
        ->swap('provider-plan-id');

订阅数量

lightbulb

订阅数量仅由 Cashier 的 Stripe 版本支持。Braintree 没有等同于 Stripe 的“数量”的功能。

有时订阅会受到“数量”的影响。例如,你的应用程序可能会按每个帐户每月 每个用户 收取 $10。要轻松增加或减少订阅数量,请使用 incrementQuantitydecrementQuantity 方法:

php
$user = User::find(1);

$user->subscription('main')->incrementQuantity();

// 在订阅的当前数量上增加五个...
$user->subscription('main')->incrementQuantity(5);

$user->subscription('main')->decrementQuantity();

// 在订阅的当前数量上减少五个...
$user->subscription('main')->decrementQuantity(5);

或者,你可以使用 updateQuantity 方法设置特定数量:

php
$user->subscription('main')->updateQuantity(10);

有关订阅数量的更多信息,请查阅 Stripe 文档

订阅税

使用 Cashier,可以轻松提供发送到 Stripe / Braintree 的 tax_percent 值。要指定用户在订阅上支付的税率,请在你的可计费模型上实现 taxPercentage 方法,并返回一个介于 0 和 100 之间的数字值,最多保留两位小数。

php
public function taxPercentage() {
    return 20;
}

这使你能够在模型的基础上应用税率,这对于跨多个国家的用户群可能很有帮助。

取消订阅

要取消订阅,只需在用户的订阅上调用 cancel 方法:

php
$user->subscription('main')->cancel();

当订阅被取消时,Cashier 会自动在你的数据库中设置 ends_at 列。此列用于知道何时 subscribed 方法应开始返回 false。例如,如果客户在 3 月 1 日取消订阅,但订阅原定于 3 月 5 日到期,则 subscribed 方法将继续返回 true 直到 3 月 5 日。

你可以使用 onGracePeriod 方法确定用户是否已取消他们的订阅,但仍在他们的“宽限期”内:

php
if ($user->subscription('main')->onGracePeriod()) {
    //
}

恢复订阅

如果用户已取消他们的订阅并且你希望恢复它,请使用 resume 方法。用户必须仍在他们的宽限期内才能恢复订阅:

php
$user->subscription('main')->resume();

如果用户取消订阅,然后在订阅完全过期之前恢复该订阅,他们将不会立即被计费。相反,他们的订阅将被重新激活,并且他们将在原始计费周期中被计费。

订阅试用

需要信用卡

如果你想在仍然收集支付方式信息的情况下向客户提供试用期,你应该在创建订阅时使用 trialDays 方法:

php
$user = User::find(1);

$user->newSubscription('main', 'monthly')
            ->trialDays(10)
            ->create($creditCardToken);

此方法将在数据库中的订阅记录上设置试用期结束日期,并指示 Stripe / Braintree 在此日期之后才开始向客户计费。

lightbulb

如果客户的订阅在试用期结束日期之前未被取消,他们将在试用期到期后立即被收费,因此你应该通知用户他们的试用期结束日期。

你可以使用用户实例的 onTrial 方法或订阅实例的 onTrial 方法来确定用户是否在他们的试用期内。以下两个示例在目的上基本相同:

php
if ($user->onTrial('main')) {
    //
}

if ($user->subscription('main')->onTrial()) {
    //
}

不需要信用卡

如果你想在不收集用户支付方式信息的情况下提供试用期,你可以简单地将用户记录上的 trial_ends_at 列设置为你想要的试用期结束日期。例如,这通常在用户注册期间完成:

php
$user = User::create([
    // 填充其他用户属性...
    'trial_ends_at' => Carbon::now()->addDays(10),
]);

Cashier 将此类型的试用称为“通用试用”,因为它不附加到任何现有订阅。User 实例上的 onTrial 方法将在当前日期未超过 trial_ends_at 的值时返回 true

php
if ($user->onTrial()) {
    // 用户在他们的试用期内...
}

如果你希望具体知道用户在他们的“通用”试用期内并且尚未创建实际订阅,你也可以使用 onGenericTrial 方法:

php
if ($user->onGenericTrial()) {
    // 用户在他们的“通用”试用期内...
}

一旦你准备好为用户创建实际订阅,你可以像往常一样使用 newSubscription 方法:

php
$user = User::find(1);

$user->newSubscription('main', 'monthly')->create($creditCardToken);

处理 Stripe Webhooks

失败的订阅

如果客户的信用卡过期怎么办?不用担心 - Cashier 包含一个 Webhook 控制器,可以轻松地为你取消客户的订阅。只需将路由指向控制器:

php
Route::post(
    'stripe/webhook',
    '\Laravel\Cashier\Http\Controllers\WebhookController@handleWebhook'
);

就是这样!失败的付款将由控制器捕获和处理。当 Stripe 确定订阅失败时(通常在三次付款失败后),控制器将取消客户的订阅。不要忘记:你需要在 Stripe 控制面板设置中配置 webhook URI。

由于 Stripe webhooks 需要绕过 Laravel 的CSRF 验证,请确保在 VerifyCsrfToken 中将 URI 列为例外,或将路由列在 web 中间件组之外:

php
protected $except = [
    'stripe/*',
];

其他 Webhooks

如果你有其他想要处理的 Stripe webhook 事件,只需扩展 Webhook 控制器。你的方法名称应符合 Cashier 的预期约定,具体来说,方法应以 handle 和你想要处理的 Stripe webhook 的“驼峰式”名称为前缀。例如,如果你想处理 invoice.payment_succeeded webhook,你应该在控制器中添加一个 handleInvoicePaymentSucceeded 方法。

php
<?php

namespace App\Http\Controllers;

use Laravel\Cashier\Http\Controllers\WebhookController as BaseController;

class WebhookController extends BaseController
{
    /**
     * 处理 Stripe webhook。
     *
     * @param  array  $payload
     * @return Response
     */
    public function handleInvoicePaymentSucceeded($payload)
    {
        // 处理事件
    }
}

处理 Braintree Webhooks

失败的订阅

如果客户的信用卡过期怎么办?不用担心 - Cashier 包含一个 Webhook 控制器,可以轻松地为你取消客户的订阅。只需将路由指向控制器:

php
Route::post(
    'braintree/webhook',
    '\Laravel\Cashier\Http\Controllers\WebhookController@handleWebhook'
);

就是这样!失败的付款将由控制器捕获和处理。当 Braintree 确定订阅失败时(通常在三次付款失败后),控制器将取消客户的订阅。不要忘记:你需要在 Braintree 控制面板设置中配置 webhook URI。

由于 Braintree webhooks 需要绕过 Laravel 的CSRF 验证,请确保在 VerifyCsrfToken 中将 URI 列为例外,或将路由列在 web 中间件组之外:

php
protected $except = [
    'braintree/*',
];

其他 Webhooks

如果你有其他想要处理的 Braintree webhook 事件,只需扩展 Webhook 控制器。你的方法名称应符合 Braintree 的预期约定,具体来说,方法应以 handle 和你想要处理的 Braintree webhook 的“驼峰式”名称为前缀。例如,如果你想处理 dispute_opened webhook,你应该在控制器中添加一个 handleDisputeOpened 方法。

php
<?php

namespace App\Http\Controllers;

use Braintree\WebhookNotification;
use Laravel\Cashier\Http\Controllers\WebhookController as BaseController;

class WebhookController extends BaseController
{
    /**
     * 处理 Braintree webhook。
     *
     * @param  WebhookNotification  $webhook
     * @return Response
     */
    public function handleDisputeOpened(WebhookNotification $notification)
    {
        // 处理事件
    }
}

单次收费

简单收费

lightbulb

使用 Stripe 时,charge 方法接受你希望以应用程序使用的货币的最低分母收取的金额。然而,使用 Braintree 时,你应该将完整的美元金额传递给 charge 方法:

如果你想对订阅客户的信用卡进行“一次性”收费,可以在可计费模型实例上使用 charge 方法。

php
// Stripe 接受以分为单位的收费...
$user->charge(100);

// Braintree 接受以美元为单位的收费...
$user->charge(1);

charge 方法接受一个数组作为其第二个参数,允许你将任何选项传递给底层的 Stripe / Braintree 收费创建:

php
$user->charge(100, [
    'custom_option' => $value,
]);

如果收费失败,charge 方法将抛出异常。如果收费成功,完整的 Stripe / Braintree 响应将从方法中返回:

php
try {
    $response = $user->charge(100);
} catch (Exception $e) {
    //
}

带发票的收费

有时你可能需要进行一次性收费,但也需要为收费生成发票,以便你可以向客户提供 PDF 收据。invoiceFor 方法可以让你做到这一点。例如,让我们为客户开具一张 $5.00 的“一次性费用”发票:

php
// Stripe 接受以分为单位的收费...
$user->invoiceFor('One Time Fee', 500);

// Braintree 接受以美元为单位的收费...
$user->invoiceFor('One Time Fee', 5);

发票将立即对用户的信用卡进行收费。invoiceFor 方法还接受一个数组作为其第三个参数,允许你将任何选项传递给底层的 Stripe / Braintree 收费创建:

php
$user->invoiceFor('One Time Fee', 500, [
    'custom-option' => $value,
]);
lightbulb

invoiceFor 方法将创建一个 Stripe 发票,该发票将重试失败的计费尝试。如果你不希望发票重试失败的收费,你需要在第一次失败的收费后使用 Stripe API 关闭它们。

发票

你可以使用 invoices 方法轻松检索可计费模型的发票数组:

php
$invoices = $user->invoices();

在为客户列出发票时,你可以使用发票的辅助方法来显示相关的发票信息。例如,你可能希望在表格中列出每张发票,允许用户轻松下载其中的任何一张:

php
<table>
    @foreach ($invoices as $invoice)
        <tr>
            <td>{{ $invoice->date()->toFormattedDateString() }}</td>
            <td>{{ $invoice->total() }}</td>
            <td><a href="/user/invoice/{{ $invoice->id }}">下载</a></td>
        </tr>
    @endforeach
</table>

生成发票 PDF

在生成发票 PDF 之前,你需要安装 dompdf PHP 库:

php
composer require dompdf/dompdf

在路由或控制器中,使用 downloadInvoice 方法生成发票的 PDF 下载。此方法将自动生成适当的 HTTP 响应以将下载发送到浏览器:

php
Route::get('user/invoice/{invoice}', function ($invoiceId) {
    return Auth::user()->downloadInvoice($invoiceId, [
        'vendor'  => 'Your Company',
        'product' => 'Your Product',
    ]);
});