Skip to content

Laravel Cashier(Stripe) ​

简介 ​

Laravel Cashier Stripe 为 Stripe 订阅计费服务提供了富有表现力且流畅的接口。它可以处理几乎所有你不想手写的订阅计费样板代码。除了基础的订阅管理外,Cashier 还可以处理优惠券、订阅切换、订阅“数量”、取消宽限期,甚至生成发票 PDF。

升级 Cashier ​

升级到新的 Cashier 版本时,务必仔细阅读升级指南。

WARNING

为了避免破坏性变更,Cashier 使用固定的 Stripe API 版本。Cashier 16 使用的 Stripe API 版本为 2025-06-30.basil。为了使用 Stripe 的新特性和改进,Stripe API 版本会在次要版本发布时更新。

安装 ​

首先,使用 Composer 包管理器安装适用于 Stripe 的 Cashier 包:

shell
composer require laravel/cashier

安装完成后,使用 vendor:publish Artisan 命令发布 Cashier 的迁移文件:

shell
php artisan vendor:publish --tag="cashier-migrations"

然后执行数据库迁移:

shell
php artisan migrate

Cashier 的迁移会向你的 users 表添加多个字段。它还会创建新的 subscriptions 表来保存客户的所有订阅,以及为多价格订阅创建 subscription_items 表。

如果你愿意,也可以使用 vendor:publish Artisan 命令发布 Cashier 的配置文件:

shell
php artisan vendor:publish --tag="cashier-config"

最后,为确保 Cashier 能正确处理所有 Stripe 事件,请记得配置 Cashier 的 webhook 处理。

WARNING

Stripe 建议任何用于存储 Stripe 标识符的字段都应区分大小写。因此,使用 MySQL 时,你应确保 stripe_id 字段的排序规则为 utf8_bin。更多信息请参阅 Stripe 文档。

配置 ​

Billable 模型 ​

在使用 Cashier 之前,请将 Billable trait 添加到你的可计费模型定义中。通常这会是 App\Models\User 模型。这个 trait 提供了多种方法,方便你执行常见的计费任务,例如创建订阅、应用优惠券以及更新支付方式信息:

php
use Laravel\Cashier\Billable;

class User extends Authenticatable
{
    use Billable;
}

Cashier 默认假定你的可计费模型是 Laravel 自带的 App\Models\User 类。如果你想更改它,可以通过 useCustomerModel 方法指定其他模型。通常,这个方法应在 AppServiceProvider 类的 boot 方法中调用:

php
use App\Models\Cashier\User;
use Laravel\Cashier\Cashier;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Cashier::useCustomerModel(User::class);
}

WARNING

如果你使用的不是 Laravel 提供的 App\Models\User 模型,则需要发布并修改提供的 Cashier 迁移文件,以匹配你替代模型所使用的数据表名称。

API 密钥 ​

接下来,你应在应用的 .env 文件中配置 Stripe API 密钥。你可以在 Stripe 控制台中获取这些密钥:

ini
STRIPE_KEY=your-stripe-key
STRIPE_SECRET=your-stripe-secret
STRIPE_WEBHOOK_SECRET=your-stripe-webhook-secret

WARNING

你应确保在应用的 .env 文件中定义了 STRIPE_WEBHOOK_SECRET 环境变量,因为这个变量用于确保传入的 webhook 确实来自 Stripe。

货币配置 ​

Cashier 默认使用美元(USD)作为货币。你可以在应用的 .env 文件中设置 CASHIER_CURRENCY 环境变量来更改默认货币:

ini
CASHIER_CURRENCY=eur

除了配置 Cashier 的货币外,你还可以指定在发票中格式化金额时使用的区域设置。Cashier 内部使用 PHP 的 NumberFormatter 类 来设置货币区域:

ini
CASHIER_CURRENCY_LOCALE=nl_BE

WARNING

如果你要使用 en 以外的区域设置,请确保服务器已安装并配置 ext-intl PHP 扩展。

税务配置 ​

借助 Stripe Tax,可以自动计算 Stripe 生成的所有发票税额。你可以在应用 App\Providers\AppServiceProvider 类的 boot 方法中调用 calculateTaxes 方法来启用自动税额计算:

php
use Laravel\Cashier\Cashier;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Cashier::calculateTaxes();
}

启用税额计算后,任何新订阅以及生成的任何一次性发票都会自动计算税额。

为了使此功能正常工作,你需要将客户的计费信息同步到 Stripe,例如客户姓名、地址和税号。你可以使用 Cashier 提供的客户数据同步和 Tax ID 相关方法来完成此操作。

日志记录 ​

Cashier 允许你指定在记录 Stripe 致命错误时使用的日志通道。你可以在应用的 .env 文件中定义 CASHIER_LOGGER 环境变量来指定日志通道:

ini
CASHIER_LOGGER=stack

通过 Stripe API 调用产生的异常会使用应用默认的日志通道进行记录。

使用自定义模型 ​

你可以通过定义自己的模型并继承对应的 Cashier 模型,来扩展 Cashier 内部使用的模型:

php
use Laravel\Cashier\Subscription as CashierSubscription;

class Subscription extends CashierSubscription
{
    // ...
}

定义模型后,你可以通过 Laravel\Cashier\Cashier 类通知 Cashier 使用你的自定义模型。通常,你应在应用 App\Providers\AppServiceProvider 类的 boot 方法中配置这些自定义模型:

php
use App\Models\Cashier\Subscription;
use App\Models\Cashier\SubscriptionItem;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Cashier::useSubscriptionModel(Subscription::class);
    Cashier::useSubscriptionItemModel(SubscriptionItem::class);
}

快速开始 ​

销售产品 ​

NOTE

在使用 Stripe Checkout 之前,你应先在 Stripe 控制台中定义固定价格的 Product。另外,你还应配置 Cashier 的 webhook 处理。

在应用中提供产品和订阅计费能力,通常会让人望而生畏。但借助 Cashier 和 Stripe Checkout,你可以轻松构建现代且可靠的支付集成。

要对非周期性的一次性商品收费,我们将使用 Cashier 将客户引导到 Stripe Checkout,在那里他们可以填写支付信息并确认购买。通过 Checkout 完成付款后,客户会被重定向到你在应用中指定的成功 URL:

php
use Illuminate\Http\Request;

Route::get('/checkout', function (Request $request) {
    $stripePriceId = 'price_deluxe_album';

    $quantity = 1;

    return $request->user()->checkout([$stripePriceId => $quantity], [
        'success_url' => route('checkout-success'),
        'cancel_url' => route('checkout-cancel'),
    ]);
})->name('checkout');

Route::view('/checkout/success', 'checkout.success')->name('checkout-success');
Route::view('/checkout/cancel', 'checkout.cancel')->name('checkout-cancel');

如上例所示,我们将使用 Cashier 提供的 checkout 方法,把客户重定向到某个“price 标识符”对应的 Stripe Checkout 页面。使用 Stripe 时,“price” 指的是为特定产品预先定义的价格。

如有需要,checkout 方法会自动在 Stripe 中创建 customer,并将该 Stripe customer 记录与应用数据库中的对应用户关联起来。完成 checkout 会话后,客户会被重定向到专门的成功页或取消页,你可以在这些页面中向客户展示提示信息。

向 Stripe Checkout 提供元数据 ​

销售产品时,通常会通过你自己应用定义的 Cart 和 Order 模型来跟踪已完成订单和购买的商品。当把客户重定向到 Stripe Checkout 完成购买时,你可能需要传递一个已有的订单标识符,以便客户返回应用后将已完成的购买与对应订单关联起来。

为此,你可以向 checkout 方法传递一个 metadata 数组。假设用户开始 checkout 流程时,我们会在应用中先创建一个待处理的 Order。请注意,此示例中的 Cart 和 Order 模型只是说明用途,并不是 Cashier 提供的。你可以根据自己应用的需求自由实现这些概念:

php
use App\Models\Cart;
use App\Models\Order;
use Illuminate\Http\Request;

Route::get('/cart/{cart}/checkout', function (Request $request, Cart $cart) {
    $order = Order::create([
        'cart_id' => $cart->id,
        'price_ids' => $cart->price_ids,
        'status' => 'incomplete',
    ]);

    return $request->user()->checkout($order->price_ids, [
        'success_url' => route('checkout-success').'?session_id={CHECKOUT_SESSION_ID}',
        'cancel_url' => route('checkout-cancel'),
        'metadata' => ['order_id' => $order->id],
    ]);
})->name('checkout');

如上例所示,当用户开始 checkout 流程时,我们会将购物车 / 订单关联的 Stripe price 标识符全部传给 checkout 方法。当然,应用需要自己负责在客户向购物车或订单中添加商品时维护这些关联。我们还通过 metadata 数组把订单 ID 提供给 Stripe Checkout 会话。最后,我们在 Checkout 成功路由中加入了 CHECKOUT_SESSION_ID 模板变量。当 Stripe 把客户重定向回你的应用时,这个模板变量会自动填入实际的 Checkout session ID。

接下来,我们来构建 Checkout 成功路由。用户通过 Stripe Checkout 完成购买后,会被重定向到这里。在这个路由中,我们可以取回 Stripe Checkout session ID 以及对应的 Stripe Checkout 实例,从而读取之前传入的元数据,并相应更新客户订单:

php
use App\Models\Order;
use Illuminate\Http\Request;
use Laravel\Cashier\Cashier;

Route::get('/checkout/success', function (Request $request) {
    $sessionId = $request->get('session_id');

    if ($sessionId === null) {
        return;
    }

    $session = Cashier::stripe()->checkout->sessions->retrieve($sessionId);

    if ($session->payment_status !== 'paid') {
        return;
    }

    $orderId = $session['metadata']['order_id'] ?? null;

    $order = Order::findOrFail($orderId);

    $order->update(['status' => 'completed']);

    return view('checkout-success', ['order' => $order]);
})->name('checkout-success');

有关 Checkout session 对象所包含数据的更多信息,请参阅 Stripe 关于 Checkout session 对象的文档。

销售订阅 ​

NOTE

在使用 Stripe Checkout 之前,你应先在 Stripe 控制台中定义固定价格的 Product。另外,你还应配置 Cashier 的 webhook 处理。

在应用中提供产品和订阅计费能力,通常会让人望而生畏。但借助 Cashier 和 Stripe Checkout,你可以轻松构建现代且可靠的支付集成。

为了说明如何使用 Cashier 和 Stripe Checkout 销售订阅,我们假设有一个简单的订阅服务,提供基础月付方案(price_basic_monthly)和年付方案(price_basic_yearly)。这两个价格可以在 Stripe 控制台中归属于同一个 “Basic” product(pro_basic)。此外,订阅服务还可能提供一个 pro_expert 的 Expert 套餐。

首先,我们来看客户如何订阅服务。你可以想象客户会在应用的定价页面点击 Basic 套餐的“订阅”按钮。这个按钮或链接应指向一个 Laravel 路由,由它为所选方案创建 Stripe Checkout 会话:

php
use Illuminate\Http\Request;

Route::get('/subscription-checkout', function (Request $request) {
    return $request->user()
        ->newSubscription('default', 'price_basic_monthly')
        ->trialDays(5)
        ->allowPromotionCodes()
        ->checkout([
            'success_url' => route('your-success-route'),
            'cancel_url' => route('your-cancel-route'),
        ]);
});

如上例所示,我们会把客户重定向到一个 Stripe Checkout 会话,让他们可以订阅 Basic 套餐。成功 checkout 或取消后,客户会被重定向到传给 checkout 方法的 URL。为了知道订阅何时真正开始生效(因为某些支付方式需要几秒钟来处理),我们还需要配置 Cashier 的 webhook 处理。

既然客户已经能够发起订阅,我们还需要限制应用中的某些区域,只允许已订阅用户访问。当然,我们始终可以通过 Cashier Billable trait 提供的 subscribed 方法判断用户当前是否已订阅:

blade
@if ($user->subscribed())
    <p>你已订阅。</p>
@endif

我们也可以很方便地判断用户是否订阅了特定 product 或 price:

blade
@if ($user->subscribedToProduct('pro_basic'))
    <p>你已订阅我们的 Basic 产品。</p>
@endif

@if ($user->subscribedToPrice('price_basic_monthly'))
    <p>你已订阅我们的 Basic 月付方案。</p>
@endif

构建 Subscribed middleware ​

为了方便,你可能希望创建一个 middleware,用于判断传入请求是否来自已订阅用户。定义好这个 middleware 后,就可以很方便地把它分配给某个路由,阻止未订阅用户访问:

php
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class Subscribed
{
    /**
     * Handle an incoming request.
     */
    public function handle(Request $request, Closure $next): Response
    {
        if (! $request->user()?->subscribed()) {
            // 将用户重定向到计费页面,并提示他们订阅...
            return redirect('/billing');
        }

        return $next($request);
    }
}

定义完成后,你可以把这个 middleware 分配给路由:

php
use App\Http\Middleware\Subscribed;

Route::get('/dashboard', function () {
    // ...
})->middleware([Subscribed::class]);

允许客户管理自己的计费方案 ​

客户当然可能希望把订阅方案切换到另一个 product 或“层级”。最简单的方式是将客户引导到 Stripe 的 Customer Billing Portal。它提供了托管式用户界面,让客户可以下载发票、更新支付方式以及修改订阅方案。

首先,在应用中定义一个链接或按钮,把用户导向一个 Laravel 路由,我们会利用它来发起 Billing Portal 会话:

blade
<a href="{{ route('billing') }}">
    Billing
</a>

接下来,定义这个路由,它会启动 Stripe Customer Billing Portal 会话并将用户重定向到 Portal。redirectToBillingPortal 方法接受一个 URL,用于指定用户退出 Portal 后应返回的位置:

php
use Illuminate\Http\Request;

Route::get('/billing', function (Request $request) {
    return $request->user()->redirectToBillingPortal(route('dashboard'));
})->middleware(['auth'])->name('billing');

NOTE

只要你已经配置了 Cashier 的 webhook 处理,Cashier 就会通过检查来自 Stripe 的传入 webhook,自动保持应用中与 Cashier 相关的数据表同步。例如,当用户通过 Stripe 的 Customer Billing Portal 取消订阅时,Cashier 会收到相应 webhook,并在应用数据库中将该订阅标记为“canceled”。

客户 ​

获取客户 ​

你可以使用 Cashier::findBillable 方法通过 Stripe ID 获取客户。这个方法会返回一个可计费模型实例:

php
use Laravel\Cashier\Cashier;

$user = Cashier::findBillable($stripeId);

创建客户 ​

有时你可能希望在不开始订阅的情况下创建一个 Stripe customer。你可以使用 createAsStripeCustomer 方法来完成:

php
$stripeCustomer = $user->createAsStripeCustomer();

客户在 Stripe 中创建完成后,你可以在稍后再开始订阅。你也可以提供一个可选的 $options 数组,传入 Stripe API 支持的其他客户创建参数:

php
$stripeCustomer = $user->createAsStripeCustomer($options);

如果你想返回可计费模型对应的 Stripe customer 对象,可以使用 asStripeCustomer 方法:

php
$stripeCustomer = $user->asStripeCustomer();

如果你想获取某个可计费模型对应的 Stripe customer 对象,但不确定它是否已经是 Stripe 中的 customer,可以使用 createOrGetStripeCustomer 方法。如果还不存在,该方法会在 Stripe 中创建一个新的 customer:

php
$stripeCustomer = $user->createOrGetStripeCustomer();

更新客户 ​

有时你可能希望直接向 Stripe customer 更新附加信息。你可以使用 updateStripeCustomer 方法来完成。这个方法接受一个数组,内容是 Stripe API 支持的客户更新选项:

php
$stripeCustomer = $user->updateStripeCustomer($options);

余额 ​

Stripe 允许你对客户“余额”进行充值或扣减。之后,这个余额会在新的发票中被抵扣或追加。要检查客户的总余额,你可以使用可计费模型上的 balance 方法。它会返回一个按客户货币格式化后的余额字符串:

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

要为客户余额充值,可以向 creditBalance 方法传入一个值。如有需要,你也可以提供一段描述:

php
$user->creditBalance(500, '高级客户充值。');

向 debitBalance 方法传入一个值,则会扣减客户余额:

php
$user->debitBalance(300, '不当使用罚金。');

applyBalance 方法会为客户创建新的余额交易记录。你可以通过 balanceTransactions 方法获取这些交易记录,这有助于向客户展示充值与扣减日志:

php
// 获取所有交易...
$transactions = $user->balanceTransactions();

foreach ($transactions as $transaction) {
    // 交易金额...
    $amount = $transaction->amount(); // $2.31

    // 在有对应发票时,获取关联发票...
    $invoice = $transaction->invoice();
}

Tax ID ​

Cashier 提供了便捷的方式来管理客户的税号。例如,可以使用 taxIds 方法以集合形式获取分配给客户的所有 tax ID:

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

你也可以通过标识符获取客户的某个特定 tax ID:

php
$taxId = $user->findTaxId('txi_belgium');

你可以通过向 createTaxId 方法提供合法的 type 和值来创建新的 Tax ID:

php
$taxId = $user->createTaxId('eu_vat', 'BE0123456789');

createTaxId 方法会立即把 VAT ID 添加到客户账户中。VAT ID 的校验也由 Stripe 完成;不过,这是一个异步过程。你可以订阅 customer.tax_id.updated webhook 事件,并检查 VAT ID 的 verification 参数,从而获知校验更新。有关处理 webhook 的更多信息,请参阅定义 webhook 处理器文档。

你可以使用 deleteTaxId 方法删除 tax ID:

php
$user->deleteTaxId('txi_belgium');

将客户数据同步到 Stripe ​

通常,当应用中的用户更新他们的姓名、邮箱地址或其他同样也存储在 Stripe 中的信息时,你应将这些更新通知给 Stripe。这样 Stripe 中保存的信息副本才会与你的应用保持同步。

要自动化这个过程,你可以在可计费模型上定义一个事件监听器,监听模型的 updated 事件。然后在监听器中调用模型上的 syncStripeCustomerDetails 方法:

php
use App\Models\User;
use function Illuminate\Events\queueable;

/**
 * 模型的 "booted" 方法。
 */
protected static function booted(): void
{
    static::updated(queueable(function (User $customer) {
        if ($customer->hasStripeId()) {
            $customer->syncStripeCustomerDetails();
        }
    }));
}

这样一来,每次客户模型被更新时,它的信息都会同步到 Stripe。为了方便,Cashier 在首次创建 customer 时也会自动把客户信息同步到 Stripe。

你可以通过覆写 Cashier 提供的一组方法,来自定义用于同步客户信息到 Stripe 的字段。例如,你可以覆写 stripeName 方法,以自定义 Cashier 将哪个属性视为 customer 的“name”并同步到 Stripe:

php
/**
 * 获取应同步到 Stripe 的客户名称。
 */
public function stripeName(): string|null
{
    return $this->company_name;
}

类似地,你也可以覆写 stripeEmail、stripePhone(最长 20 个字符)、stripeAddress 以及 stripePreferredLocales 方法。当更新 Stripe customer 对象时,这些方法会把信息同步到对应的 customer 参数中。如果你希望完全掌控 customer 信息同步流程,也可以覆写 syncStripeCustomerDetails 方法。

Billing Portal ​

Stripe 提供了一种简单方式来搭建 billing portal,让客户可以管理自己的订阅、支付方式并查看计费历史。你可以在控制器或路由中,通过对可计费模型调用 redirectToBillingPortal 方法,把用户重定向到 billing portal:

php
use Illuminate\Http\Request;

Route::get('/billing-portal', function (Request $request) {
    return $request->user()->redirectToBillingPortal();
});

默认情况下,当用户在 Stripe billing portal 中完成订阅管理后,可以通过 portal 内部链接返回应用的 home 路由。你也可以向 redirectToBillingPortal 方法传入 URL 参数,以指定用户应返回的自定义 URL:

php
use Illuminate\Http\Request;

Route::get('/billing-portal', function (Request $request) {
    return $request->user()->redirectToBillingPortal(route('billing'));
});

如果你想在不生成 HTTP 重定向响应的情况下获取 billing portal URL,可以调用 billingPortalUrl 方法:

php
$url = $request->user()->billingPortalUrl(route('billing'));

支付方式 ​

存储支付方式 ​

为了通过 Stripe 创建订阅或执行“一次性”收费,你的应用需要安全地收集客户的支付详情。具体做法取决于你计划存储支付方式用于未来订阅,还是立即处理单次收费,因此下面分别说明。

Stripe 的 Payment Element 可用于支持多种支付方式,例如信用卡、Apple Pay、Google Pay 和 iDEAL。

订阅的 Payment Element ​

首先,创建一个 Setup Intent 并将其传给视图:

php
return view('subscribe', [
    'intent' => $user->createSetupIntent()
]);

使用 Setup Intent 的 client_secret 挂载 Payment Element:

html
<div id="payment-element"></div>
<button id="submit">Subscribe</button>

<script src="https://js.stripe.com/v3/"></script>
<script>
    const stripe = Stripe('stripe-public-key');

    const elements = stripe.elements({
        clientSecret: '{{ $intent->client_secret }}'
    });

    const paymentElement = elements.create('payment');

    paymentElement.mount('#payment-element');

    document.getElementById('submit').addEventListener('click', async () => {
        const { error } = await stripe.confirmSetup({
            elements,
            confirmParams: {
                return_url: '{{ route("subscription.complete") }}',
            },
        });

        if (error) {
            // 向用户显示 "error.message"...
        }
    });
</script>

Stripe 重定向到你的 return_url 后,setup_intent ID 会作为查询字符串参数提供。你可以使用这个值获取支付方式并创建订阅:

php
use Illuminate\Http\Request;

Route::get('/subscription/complete', function (Request $request) {
    $setupIntent = $request->user()->findSetupIntent(
        $request->setup_intent
    );

    $paymentMethod = $setupIntent->payment_method;

    $request->user()
        ->newSubscription('default', 'price_xxx')
        ->create($paymentMethod);

    return redirect('/dashboard');
})->name('subscription.complete');

如果你使用 Payment Element 更新客户的默认支付方式,而非创建订阅,可以将支付方式标识符传给 updateDefaultPaymentMethod 方法。

单次收费的 Payment Element ​

对于一次性付款,请使用 Cashier 的 pay 方法创建 Payment Intent。通常,你应将 Payment Intent ID 存储到应用中对应的订单上,以便 Stripe 将客户重定向回应用后能够找回该订单。下面的示例假设你的应用有一个 Order model,并包含 user_id、amount、status 和 stripe_payment_intent_id 列:

php
use App\Models\Order;
use Illuminate\Http\Request;

Route::post('/pay', function (Request $request) {
    $amount = 1000;

    $payment = $request->user()->pay($amount);

    $order = Order::create([
        'user_id' => $request->user()->id,
        'amount' => $amount,
        'status' => 'pending',
        'stripe_payment_intent_id' => $payment->id,
    ]);

    return view('checkout', [
        'clientSecret' => $payment->client_secret,
        'order' => $order,
    ]);
});

然后,挂载 Payment Element 并确认付款:

html
<div id="payment-element"></div>
<button id="submit">Pay Now</button>

<script src="https://js.stripe.com/v3/"></script>
<script>
    const stripe = Stripe('stripe-public-key');

    const elements = stripe.elements({
        clientSecret: '{{ $clientSecret }}'
    });

    const paymentElement = elements.create('payment');

    paymentElement.mount('#payment-element');

    document.getElementById('submit').addEventListener('click', async () => {
        const { error } = await stripe.confirmPayment({
            elements,
            confirmParams: {
                return_url: '{{ route("payment.complete") }}',
            },
        });

        if (error) {
            // 向用户显示 "error.message"...
        }
    });
</script>

重定向后,你可以使用 payment_intent 查询字符串参数获取对应的订单和 Payment Intent。在履行订单前,你应验证该订单属于已认证客户,并验证 Payment Intent 属于已认证客户且已成功:

php
use App\Models\Order;
use Illuminate\Http\Request;

Route::get('/payment/complete', function (Request $request) {
    $order = Order::where('user_id', $request->user()->id)
        ->where('stripe_payment_intent_id', $request->payment_intent)
        ->firstOrFail();

    $paymentIntent = $request->user()
        ->stripe()
        ->paymentIntents
        ->retrieve($request->payment_intent);

    if ($paymentIntent->customer === $request->user()->stripe_id &&
        $paymentIntent->status === 'succeeded') {
        $order->update(['status' => 'paid']);

        // 履行订单...
    }

    return redirect('/dashboard');
})->name('payment.complete');

获取支付方式 ​

可计费模型实例上的 paymentMethods 方法会返回一个由 Laravel\Cashier\PaymentMethod 实例组成的集合:

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

默认情况下,此方法会返回所有类型的支付方式。若要获取特定类型的支付方式,可以将 type 作为参数传给该方法:

php
$paymentMethods = $user->paymentMethods('sepa_debit');

要获取客户的默认支付方式,可以使用 defaultPaymentMethod 方法:

php
$paymentMethod = $user->defaultPaymentMethod();

你也可以使用 findPaymentMethod 方法,获取附加到该可计费模型上的某个特定支付方式:

php
$paymentMethod = $user->findPaymentMethod($paymentMethodId);

支付方式是否存在 ​

要判断某个可计费模型的账户是否附加了默认支付方式,可以调用 hasDefaultPaymentMethod 方法:

php
if ($user->hasDefaultPaymentMethod()) {
    // ...
}

你可以使用 hasPaymentMethod 方法来判断某个可计费模型的账户上是否至少附加了一个支付方式:

php
if ($user->hasPaymentMethod()) {
    // ...
}

这个方法会判断可计费模型是否存在任何支付方式。若要判断模型上是否存在某种特定类型的支付方式,可以将 type 作为参数传给该方法:

php
if ($user->hasPaymentMethod('sepa_debit')) {
    // ...
}

更新默认支付方式 ​

可以使用 updateDefaultPaymentMethod 方法来更新客户的默认支付方式信息。这个方法接受 Stripe 支付方式标识符,并会将新支付方式设为默认计费支付方式:

php
$user->updateDefaultPaymentMethod($paymentMethod);

如果你想把默认支付方式信息与 Stripe 中客户的默认支付方式信息同步,可以使用 updateDefaultPaymentMethodFromStripe 方法:

php
$user->updateDefaultPaymentMethodFromStripe();

WARNING

customer 上的默认支付方式只能用于发票和创建新订阅。由于 Stripe 的限制,它不能用于单次收费。

添加支付方式 ​

要添加新的支付方式,可以在可计费模型上调用 addPaymentMethod 方法,并传入支付方式标识符:

php
$user->addPaymentMethod($paymentMethod);

NOTE

关于如何获取支付方式标识符,请参阅支付方式存储文档。

删除支付方式 ​

要删除某个支付方式,可以在希望删除的 Laravel\Cashier\PaymentMethod 实例上调用 delete 方法:

php
$paymentMethod->delete();

deletePaymentMethod 方法会从可计费模型中删除某个特定支付方式:

php
$user->deletePaymentMethod('pm_visa');

deletePaymentMethods 方法会删除可计费模型的所有支付方式信息:

php
$user->deletePaymentMethods();

默认情况下,此方法会删除所有类型的支付方式。若要删除特定类型的支付方式,可以将 type 作为参数传给该方法:

php
$user->deletePaymentMethods('sepa_debit');

WARNING

如果用户有活跃订阅,应用不应允许他们删除默认支付方式。

订阅 ​

订阅提供了一种向客户设置周期性付款的方式。由 Cashier 管理的 Stripe 订阅支持多个订阅 price、订阅数量、试用期等能力。

创建订阅 ​

要创建订阅,先获取可计费模型的一个实例,通常会是 App\Models\User 的实例。获取模型实例后,你可以使用 newSubscription 方法来创建该模型的订阅:

php
use Illuminate\Http\Request;

Route::post('/user/subscribe', function (Request $request) {
    $request->user()->newSubscription(
        'default', 'price_monthly'
    )->create($request->paymentMethodId);

    // ...
});

传给 newSubscription 方法的第一个参数应是订阅在应用内部使用的类型。如果你的应用只提供单一订阅,你可以把它命名为 default 或 primary。这个订阅类型仅供应用内部使用,不应展示给用户。另外,它不应包含空格,而且在订阅创建后不应再修改。第二个参数是用户订阅的具体 price。这个值应对应 Stripe 中该 price 的标识符。

create 方法接受Stripe 支付方式标识符或 Stripe PaymentMethod 对象。它不仅会启动订阅,还会更新数据库中的 Stripe customer ID 以及其他相关计费信息。

WARNING

直接将支付方式标识符传给订阅的 create 方法时,它也会自动被添加到用户已存储的支付方式中。

通过发票邮件收取周期性付款 ​

与其自动收取客户的周期性付款,你也可以指示 Stripe 在每次应付款到期时向客户发送发票邮件。之后客户可以在收到发票后手动付款。使用这种方式收取周期性付款时,客户无需预先提供支付方式:

php
$user->newSubscription('default', 'price_monthly')->createAndSendInvoice();

客户在订阅被取消前可支付发票的时间,由 days_until_due 选项决定。默认值为 30 天;但如果你愿意,也可以传入特定值:

php
$user->newSubscription('default', 'price_monthly')->createAndSendInvoice([], [
    'days_until_due' => 30
]);

数量 ​

如果你想在创建订阅时为某个 price 指定特定的数量,应在创建订阅之前,在 subscription builder 上调用 quantity 方法:

php
$user->newSubscription('default', 'price_monthly')
    ->quantity(5)
    ->create($paymentMethod);

额外详情 ​

如果你想指定 Stripe 支持的其他 customer 或 subscription 选项,可以把它们作为 create 方法的第二和第三个参数传入:

php
$user->newSubscription('default', 'price_monthly')->create($paymentMethod, [
    'email' => $email,
], [
    'metadata' => ['note' => 'Some extra information.'],
]);

优惠券 ​

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

php
$user->newSubscription('default', 'price_monthly')
    ->withCoupon('code')
    ->create($paymentMethod);

或者,如果你想应用 Stripe promotion code,可以使用 withPromotionCode 方法:

php
$user->newSubscription('default', 'price_monthly')
    ->withPromotionCode('promo_code_id')
    ->create($paymentMethod);

这里给出的 promotion code ID 应是 Stripe API 分配给该 promotion code 的 ID,而不是面向客户展示的 promotion code。如果你需要根据面向客户展示的 promotion code 查找对应 ID,可以使用 findPromotionCode 方法:

php
// 根据面向客户的 code 查找 promotion code ID...
$promotionCode = $user->findPromotionCode('SUMMERSALE');

// 根据面向客户的 code 查找处于激活状态的 promotion code ID...
$promotionCode = $user->findActivePromotionCode('SUMMERSALE');

上面的示例中,返回的 $promotionCode 对象是 Laravel\Cashier\PromotionCode 的实例。这个类包装了底层的 Stripe\PromotionCode 对象。你可以调用 coupon 方法来获取与 promotion code 相关联的 coupon:

php
$coupon = $user->findPromotionCode('SUMMERSALE')->coupon();

coupon 实例允许你判断折扣金额,以及该 coupon 是固定金额折扣还是百分比折扣:

php
if ($coupon->isPercentage()) {
    return $coupon->percentOff().'%'; // 21.5%
} else {
    return $coupon->amountOff(); // $5.99
}

你也可以获取当前应用在 customer 或订阅上的折扣:

php
$discount = $billable->discount();

$discount = $subscription->discount();

返回的 Laravel\Cashier\Discount 实例包装了底层的 Stripe\Discount 对象实例。你可以调用 coupon 方法来获取与该折扣关联的 coupon:

php
$coupon = $subscription->discount()->coupon();

如果你想向 customer 或订阅应用新的 coupon 或 promotion code,可以通过 applyCoupon 或 applyPromotionCode 方法实现:

php
$billable->applyCoupon('coupon_id');
$billable->applyPromotionCode('promotion_code_id');

$subscription->applyCoupon('coupon_id');
$subscription->applyPromotionCode('promotion_code_id');

请记住,你应使用 Stripe API 分配给 promotion code 的 ID,而不是面向客户展示的 promotion code。同一时刻,一个 customer 或订阅只能应用一个 coupon 或 promotion code。

关于这个主题的更多信息,请参阅 Stripe 关于 coupon 和 promotion code 的文档。

添加订阅 ​

如果你想给已经有默认支付方式的 customer 添加订阅,可以在 subscription builder 上调用 add 方法:

php
use App\Models\User;

$user = User::find(1);

$user->newSubscription('default', 'price_monthly')->add();

从 Stripe Dashboard 创建订阅 ​

你也可以直接在 Stripe Dashboard 中创建订阅。这样做时,Cashier 会同步这些新订阅,并为它们分配 default 类型。若要自定义 Dashboard 创建订阅时分配的订阅类型,请定义 webhook 事件处理器。

此外,你只能通过 Stripe Dashboard 创建一种类型的订阅。如果你的应用提供多个使用不同类型的订阅,则只能通过 Stripe Dashboard 添加其中一种类型。

最后,你应始终确保应用提供的每一种订阅类型只存在一个活跃订阅。如果某个 customer 有两个 default 订阅,即使二者都会同步到应用数据库,Cashier 也只会使用最近添加的那个订阅。

检查订阅状态 ​

客户订阅你的应用后,你可以通过多种便捷方法轻松检查订阅状态。首先,subscribed 方法会在客户拥有活跃订阅时返回 true,即使订阅当前仍在试用期中。subscribed 方法第一个参数接受订阅类型:

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

subscribed 方法也非常适合作为路由 middleware,从而根据用户订阅状态过滤对路由和控制器的访问:

php
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class EnsureUserIsSubscribed
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        if ($request->user() && ! $request->user()->subscribed('default')) {
            // 该用户不是付费客户...
            return redirect('/billing');
        }

        return $next($request);
    }
}

如果你想判断用户是否仍在试用期内,可以使用 onTrial 方法。这个方法适合用于决定是否向用户显示试用期提醒:

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

可以使用 subscribedToProduct 方法,根据给定的 Stripe product 标识符判断用户是否订阅了某个 product。在 Stripe 中,product 是一组 price 的集合。下面的例子会判断用户的 default 订阅是否正在订阅应用的 “premium” product。给定的 Stripe product 标识符应对应 Stripe Dashboard 中某个 product 的标识符:

php
if ($user->subscribedToProduct('prod_premium', 'default')) {
    // ...
}

向 subscribedToProduct 方法传入数组后,你可以判断用户的 default 订阅是否正在订阅应用的 “basic” 或 “premium” product:

php
if ($user->subscribedToProduct(['prod_basic', 'prod_premium'], 'default')) {
    // ...
}

可以使用 subscribedToPrice 方法判断客户的订阅是否对应某个给定的 price ID:

php
if ($user->subscribedToPrice('price_basic_monthly', 'default')) {
    // ...
}

可以使用 recurring 方法判断用户当前是否已订阅,且不再处于试用期:

php
if ($user->subscription('default')->recurring()) {
    // ...
}

WARNING

如果某个用户有两个相同类型的订阅,subscription 方法始终会返回最近的那个订阅。例如,用户可能有两条类型为 default 的订阅记录;其中一个可能是旧的、已过期的订阅,另一个则是当前有效的订阅。最近的订阅总会被返回,而较旧的订阅则保留在数据库中供历史审查。

已取消订阅状态 ​

如果你想判断某个用户曾经是活跃订阅者但已经取消了订阅,可以使用 canceled 方法:

php
if ($user->subscription('default')->canceled()) {
    // ...
}

你也可以判断用户是否已取消订阅,但仍处于订阅正式到期前的“宽限期”中。例如,如果用户在 3 月 5 日取消了原本计划于 3 月 10 日到期的订阅,则到 3 月 10 日之前该用户都处于“宽限期”。注意,在这段时间里,subscribed 方法仍会返回 true:

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

如果你想判断用户是否已经取消订阅,且不再处于“宽限期”,可以使用 ended 方法:

php
if ($user->subscription('default')->ended()) {
    // ...
}

incomplete 与 past_due 状态 ​

如果订阅在创建后需要进行二次支付确认,则该订阅会被标记为 incomplete。订阅状态保存在 Cashier subscriptions 数据表的 stripe_status 字段中。

类似地,如果在切换 price 时需要二次支付确认,订阅会被标记为 past_due。当订阅处于这两种状态之一时,只有在客户完成支付确认后,订阅才会变为活跃。你可以在可计费模型或订阅实例上使用 hasIncompletePayment 方法来判断订阅是否存在未完成付款:

php
if ($user->hasIncompletePayment('default')) {
    // ...
}

if ($user->subscription('default')->hasIncompletePayment()) {
    // ...
}

当订阅存在未完成付款时,你应把用户引导到 Cashier 的支付确认页面,并传入 latestPayment 标识符。你可以使用订阅实例上的 latestPayment 方法获取这个标识符:

html
<a href="{{ route('cashier.payment', $subscription->latestPayment()->id) }}">
    请确认你的付款。
</a>

如果你希望订阅在 past_due 或 incomplete 状态下仍被视为活跃,可以使用 Cashier 提供的 keepPastDueSubscriptionsActive 和 keepIncompleteSubscriptionsActive 方法。通常,这些方法应在 App\Providers\AppServiceProvider 的 register 方法中调用:

php
use Laravel\Cashier\Cashier;

/**
 * Register any application services.
 */
public function register(): void
{
    Cashier::keepPastDueSubscriptionsActive();
    Cashier::keepIncompleteSubscriptionsActive();
}

WARNING

当订阅处于 incomplete 状态时,在支付确认之前不能对其进行修改。因此,在订阅处于 incomplete 状态时,swap 和 updateQuantity 方法都会抛出异常。

订阅作用域 ​

大多数订阅状态也提供了查询作用域,因此你可以很方便地查询数据库中处于某种状态的订阅:

php
// 获取所有活跃订阅...
$subscriptions = Subscription::query()->active()->get();

// 获取某个用户的所有已取消订阅...
$subscriptions = $user->subscriptions()->canceled()->get();

下面是所有可用作用域的完整列表:

php
Subscription::query()->active();
Subscription::query()->canceled();
Subscription::query()->ended();
Subscription::query()->incomplete();
Subscription::query()->notCanceled();
Subscription::query()->notOnGracePeriod();
Subscription::query()->notOnTrial();
Subscription::query()->onGracePeriod();
Subscription::query()->onTrial();
Subscription::query()->pastDue();
Subscription::query()->recurring();

更改价格 ​

客户订阅应用后,可能偶尔想切换到新的订阅 price。要把客户切换到新的 price,可将 Stripe price 标识符传给 swap 方法。切换 price 时,Cashier 会假定如果该订阅之前已经取消,则用户希望重新激活它。给定的 price 标识符应对应 Stripe Dashboard 中存在的某个 Stripe price 标识符:

php
use App\Models\User;

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

$user->subscription('default')->swap('price_yearly');

如果客户处于试用期,则试用期会被保留。另外,如果订阅设置了“数量”,这个数量也会被保留。

如果你希望在切换 price 的同时取消客户当前的试用期,可以调用 skipTrial 方法:

php
$user->subscription('default')
    ->skipTrial()
    ->swap('price_yearly');

如果你希望在切换 price 的同时立刻向客户开票,而不是等到下一个计费周期,可以使用 swapAndInvoice 方法:

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

$user->subscription('default')->swapAndInvoice('price_yearly');

按比例计费 ​

默认情况下,Stripe 会在 price 切换时按比例计算费用。你可以使用 noProrate 方法,在不按比例计费的情况下更新订阅价格:

php
$user->subscription('default')->noProrate()->swap('price_yearly');

有关订阅按比例计费的更多信息,请参阅 Stripe 文档。

WARNING

在 swapAndInvoice 方法之前调用 noProrate 方法对按比例计费不会有任何影响。发票始终会被开出。

订阅数量 ​

有时订阅会受到“数量”影响。例如,一个项目管理应用可能按每个项目每月收取 10 美元。你可以使用 incrementQuantity 和 decrementQuantity 方法来轻松增加或减少订阅数量:

php
use App\Models\User;

$user = User::find(1);

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

// 在当前订阅数量基础上加五...
$user->subscription('default')->incrementQuantity(5);

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

// 在当前订阅数量基础上减五...
$user->subscription('default')->decrementQuantity(5);

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

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

你可以使用 noProrate 方法,在不按比例计费的情况下更新订阅数量:

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

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

多产品订阅的数量 ​

如果你的订阅是多产品订阅,则在调用增加 / 减少数量方法时,应将你想增加或减少数量的 price ID 作为第二个参数传入:

php
$user->subscription('default')->incrementQuantity(1, 'price_chat');

多产品订阅 ​

多产品订阅允许你把多个计费 product 分配给同一个订阅。例如,假设你正在构建一个客服“帮助台”应用,它的基础订阅价格为每月 10 美元,同时提供额外每月 15 美元的在线聊天增值产品。多产品订阅的信息会存储在 Cashier 的 subscription_items 数据表中。

你可以将价格数组作为 newSubscription 方法的第二个参数传入,从而为某个订阅指定多个 product:

php
use Illuminate\Http\Request;

Route::post('/user/subscribe', function (Request $request) {
    $request->user()->newSubscription('default', [
        'price_monthly',
        'price_chat',
    ])->create($request->paymentMethodId);

    // ...
});

在上面的例子中,客户的 default 订阅会附带两个 price。两个 price 都会按照各自的计费周期收费。如有需要,你也可以使用 quantity 方法为每个 price 指定特定数量:

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

$user->newSubscription('default', ['price_monthly', 'price_chat'])
    ->quantity(5, 'price_chat')
    ->create($paymentMethod);

如果你想为现有订阅添加另一个 price,可以调用订阅的 addPrice 方法:

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

$user->subscription('default')->addPrice('price_chat');

上面的例子会添加新的 price,客户会在下一个计费周期为其付款。如果你想立刻向客户收费,可以使用 addPriceAndInvoice 方法:

php
$user->subscription('default')->addPriceAndInvoice('price_chat');

如果你想添加某个带特定数量的 price,可以将数量作为第二个参数传给 addPrice 或 addPriceAndInvoice 方法:

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

$user->subscription('default')->addPrice('price_chat', 5);

你可以使用 removePrice 方法从订阅中移除 price:

php
$user->subscription('default')->removePrice('price_chat');

WARNING

你不能移除订阅上的最后一个 price。遇到这种情况,你应直接取消订阅。

切换价格 ​

你也可以修改多产品订阅上附带的价格。例如,假设某个客户当前订阅了 price_basic,并带有 price_chat 增值产品,而你希望把客户从 price_basic 升级到 price_pro:

php
use App\Models\User;

$user = User::find(1);

$user->subscription('default')->swap(['price_pro', 'price_chat']);

执行上面的例子时,底层带有 price_basic 的订阅项会被删除,而 price_chat 对应的订阅项会被保留。另外,还会创建一个新的 price_pro 订阅项。

你也可以通过向 swap 方法传入键值对数组,来指定订阅项选项。例如,你可能需要指定订阅价格的数量:

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

$user->subscription('default')->swap([
    'price_pro' => ['quantity' => 5],
    'price_chat'
]);

如果你只想切换订阅中的某一个价格,可以直接在订阅项本身上使用 swap 方法。如果你希望保留该订阅其他价格上的现有 metadata,这种方式会特别有用:

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

$user->subscription('default')
    ->findItemOrFail('price_basic')
    ->swap('price_pro');

比例计费 ​

默认情况下,在为多产品订阅添加或删除价格时,Stripe 会按比例计算费用。如果你想在不进行比例计费的情况下调整价格,应在价格操作上链式调用 noProrate 方法:

php
$user->subscription('default')->noProrate()->removePrice('price_chat');

数量 ​

如果你想更新单个订阅价格上的数量,可以通过把 price ID 作为额外参数传给方法,来使用现有的数量方法:

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

$user->subscription('default')->incrementQuantity(5, 'price_chat');

$user->subscription('default')->decrementQuantity(3, 'price_chat');

$user->subscription('default')->updateQuantity(10, 'price_chat');

WARNING

当订阅包含多个 price 时,Subscription 模型上的 stripe_price 和 quantity 属性会为 null。要访问各个 price 的属性,你应使用 Subscription 模型上的 items 关联。

订阅项 ​

当一个订阅包含多个 price 时,你的数据库 subscription_items 表中会保存多个订阅“item”。你可以通过订阅上的 items 关联访问它们:

php
use App\Models\User;

$user = User::find(1);

$subscriptionItem = $user->subscription('default')->items->first();

// 获取某个特定 item 的 Stripe price 和数量...
$stripePrice = $subscriptionItem->stripe_price;
$quantity = $subscriptionItem->quantity;

你也可以使用 findItemOrFail 方法获取某个特定 price:

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

$subscriptionItem = $user->subscription('default')->findItemOrFail('price_chat');

多个订阅 ​

Stripe 允许你的客户同时拥有多个订阅。例如,你可能经营一家健身房,提供游泳订阅和力量训练订阅,而每个订阅都可能有不同定价。当然,客户应能订阅其中一个或两个方案。

当应用创建订阅时,你可以向 newSubscription 方法提供订阅类型。这个类型可以是任何能够表示用户当前发起哪种订阅的字符串:

php
use Illuminate\Http\Request;

Route::post('/swimming/subscribe', function (Request $request) {
    $request->user()->newSubscription('swimming')
        ->price('price_swimming_monthly')
        ->create($request->paymentMethodId);

    // ...
});

在这个例子中,我们为客户创建了按月收费的游泳订阅。当然,他们日后可能想切换到年付方案。此时,只需在 swimming 订阅上切换 price:

php
$user->subscription('swimming')->swap('price_swimming_yearly');

当然,你也可以彻底取消该订阅:

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

按使用量计费 ​

按使用量计费允许你根据客户在一个计费周期内的产品使用情况收费。例如,你可以按客户每月发送的短信或邮件数量收费。

要开始使用按使用量计费,首先需要在 Stripe Dashboard 中创建一个启用了按使用量计费模型的新 product,并配置一个 meter。创建 meter 之后,请保存对应的事件名称和 meter ID,后续上报与查询使用量都需要它们。然后,使用 meteredPrice 方法把 metered price ID 添加到客户订阅中:

php
use Illuminate\Http\Request;

Route::post('/user/subscribe', function (Request $request) {
    $request->user()->newSubscription('default')
        ->meteredPrice('price_metered')
        ->create($request->paymentMethodId);

    // ...
});

你也可以通过 Stripe Checkout 启动 metered subscription:

php
$checkout = Auth::user()
    ->newSubscription('default', [])
    ->meteredPrice('price_metered')
    ->checkout();

return view('your-checkout-view', [
    'checkout' => $checkout,
]);

上报使用量 ​

随着客户使用你的应用,你需要将他们的使用情况上报给 Stripe,确保计费准确。要上报某个 metered event 的使用量,可以在 Billable 模型上使用 reportMeterEvent 方法:

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

$user->reportMeterEvent('emails-sent');

默认情况下,会向当前计费周期增加 1 个“usage quantity”。你也可以传入一个特定的“usage”数量,添加到客户本周期的使用量中:

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

$user->reportMeterEvent('emails-sent', quantity: 15);

要获取客户在某个 meter 下的事件汇总,可以使用 Billable 实例上的 meterEventSummaries 方法:

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

$meterUsage = $user->meterEventSummaries($meterId);

$meterUsage->first()->aggregated_value; // 10

关于 meter event summary 的更多信息,请参阅 Stripe 的 Meter Event Summary object 文档。

如果你想列出所有 meter,可以使用 Billable 实例上的 meters 方法:

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

$user->meters();

订阅税率 ​

WARNING

与其手动计算 Tax Rate,你可以改用 Stripe Tax 自动计算税额

要指定用户在订阅上应支付的税率,你应在可计费模型上实现 taxRates 方法,并返回一个包含 Stripe tax rate ID 的数组。你可以在 Stripe Dashboard 中定义这些 tax rate:

php
/**
 * 应应用到客户订阅上的税率。
 *
 * @return array<int, string>
 */
public function taxRates(): array
{
    return ['txr_id'];
}

taxRates 方法让你能够按 customer 维度应用税率,这对于跨多个国家或税率区间的用户群非常有帮助。

如果你提供的是多产品订阅,还可以通过在可计费模型上实现 priceTaxRates 方法,为每个 price 定义不同税率:

php
/**
 * 应应用到客户订阅上的税率。
 *
 * @return array<string, array<int, string>>
 */
public function priceTaxRates(): array
{
    return [
        'price_monthly' => ['txr_id'],
    ];
}

WARNING

taxRates 方法只应用于订阅收费。如果你使用 Cashier 执行“一次性”收费,则需要在当时手动指定税率。

同步税率 ​

当你更改 taxRates 方法返回的硬编码 tax rate ID 时,用户现有订阅上的税务设置不会自动更新。如果你希望使用新的 taxRates 值更新已有订阅的税额,应在用户的订阅实例上调用 syncTaxRates 方法:

php
$user->subscription('default')->syncTaxRates();

这同样会同步多产品订阅中各个订阅项的税率。如果你的应用提供多产品订阅,应确保可计费模型实现了上文讨论过的 priceTaxRates 方法。

税务豁免 ​

Cashier 还提供了 isNotTaxExempt、isTaxExempt 和 reverseChargeApplies 方法,用于判断 customer 是否享有税务豁免。这些方法会调用 Stripe API 来确定 customer 的税务豁免状态:

php
use App\Models\User;

$user = User::find(1);

$user->isTaxExempt();
$user->isNotTaxExempt();
$user->reverseChargeApplies();

WARNING

这些方法同样可用于任何 Laravel\Cashier\Invoice 对象。不过,当在 Invoice 对象上调用时,它们会返回发票创建时对应的豁免状态。

订阅锚点日期 ​

默认情况下,计费周期锚点是订阅创建日期;如果使用试用期,则为试用结束日期。如果你想修改计费锚点日期,可以使用 anchorBillingCycleOn 方法:

php
use Illuminate\Http\Request;

Route::post('/user/subscribe', function (Request $request) {
    $anchor = Carbon::parse('first day of next month');

    $request->user()->newSubscription('default', 'price_monthly')
        ->anchorBillingCycleOn($anchor->startOfDay())
        ->create($request->paymentMethodId);

    // ...
});

有关如何管理订阅计费周期的更多信息,请参阅 Stripe 计费周期文档。

取消订阅 ​

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

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

当订阅被取消时,Cashier 会自动设置 subscriptions 数据表中的 ends_at 字段。这个字段用于判断 subscribed 方法何时开始返回 false。

例如,如果客户在 3 月 1 日取消订阅,但订阅原本要到 3 月 5 日才结束,那么 subscribed 方法在 3 月 5 日之前仍会返回 true。这是因为通常会允许用户一直使用应用直到计费周期结束。

你可以使用 onGracePeriod 方法来判断用户是否已经取消订阅,但仍处于“宽限期”中:

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

如果你希望立即取消订阅,可以在用户的订阅上调用 cancelNow 方法:

php
$user->subscription('default')->cancelNow();

如果你希望立即取消订阅,并为任何尚未开票的 metered usage 或新的 / 待处理的按比例计费发票项开票,可以在用户订阅上调用 cancelNowAndInvoice 方法:

php
$user->subscription('default')->cancelNowAndInvoice();

你也可以选择在某个特定时间点取消订阅:

php
$user->subscription('default')->cancelAt(
    now()->plus(days: 10)
);

最后,在删除关联用户模型之前,你应始终先取消该用户的订阅:

php
$user->subscription('default')->cancelNow();

$user->delete();

恢复订阅 ​

如果某个客户已经取消了订阅,而你希望恢复它,可以在该订阅上调用 resume 方法。客户必须仍处于“宽限期”内,才能恢复订阅:

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

如果客户取消订阅后,又在订阅完全过期之前恢复订阅,则不会立即向客户收费。取而代之的是,订阅会重新激活,客户会按照原始计费周期继续被收费。

订阅试用期 ​

预先收集支付方式 ​

如果你想在向客户提供试用期的同时,预先收集支付方式信息,应在创建订阅时使用 trialDays 方法:

php
use Illuminate\Http\Request;

Route::post('/user/subscribe', function (Request $request) {
    $request->user()->newSubscription('default', 'price_monthly')
        ->trialDays(10)
        ->create($request->paymentMethodId);

    // ...
});

此方法会在数据库的订阅记录中设置试用结束日期,并指示 Stripe 在该日期之前不要开始收费。使用 trialDays 方法时,Cashier 会覆盖 Stripe 中为该 price 配置的默认试用期。

WARNING

如果客户的订阅在试用结束日期之前没有被取消,那么试用一旦到期就会立即收费,因此你应确保通知用户他们的试用结束日期。

trialUntil 方法允许你提供一个 DateTime 实例,用于指定试用期何时结束:

php
use Illuminate\Support\Carbon;

$user->newSubscription('default', 'price_monthly')
    ->trialUntil(Carbon::now()->plus(days: 10))
    ->create($paymentMethod);

你可以通过用户实例上的 onTrial 方法,或订阅实例上的 onTrial 方法来判断用户是否处于试用期内。下面两个例子是等价的:

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

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

你可以使用 endTrial 方法立即结束某个订阅试用期:

php
$user->subscription('default')->endTrial();

要判断现有试用期是否已经过期,可以使用 hasExpiredTrial 方法:

php
if ($user->hasExpiredTrial('default')) {
    // ...
}

if ($user->subscription('default')->hasExpiredTrial()) {
    // ...
}

在 Stripe / Cashier 中定义试用天数 ​

你可以选择在 Stripe Dashboard 中定义某个 price 应提供多少试用天数,也可以始终通过 Cashier 显式传入。如果你选择在 Stripe 中定义试用天数,那么你需要注意:新的订阅,包括过去订阅过的客户再次创建的新订阅,都会自动获得试用期,除非你显式调用 skipTrial() 方法。

不预先收集支付方式 ​

如果你希望在不预先收集用户支付方式信息的情况下提供试用期,可以在用户记录上设置 trial_ends_at 字段为你期望的试用结束日期。这通常在用户注册时完成:

php
use App\Models\User;

$user = User::create([
    // ...
    'trial_ends_at' => now()->plus(days: 10),
]);

WARNING

请确保在 billable 模型的类定义中,为 trial_ends_at 属性添加一个日期 cast。

Cashier 将这种试用称为“generic trial”,因为它不附属于任何现有订阅。如果当前日期尚未超过 trial_ends_at 的值,则 billable 模型实例上的 onTrial 方法会返回 true:

php
if ($user->onTrial()) {
    // 用户正处于试用期内...
}

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

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

$user->newSubscription('default', 'price_monthly')->create($paymentMethod);

要获取用户试用期结束日期,可以使用 trialEndsAt 方法。如果用户处于试用期,它会返回一个 Carbon 日期实例;如果不在试用期,则返回 null。如果你想获取某个非默认订阅类型的试用结束日期,也可以传入可选的订阅类型参数:

php
if ($user->onTrial()) {
    $trialEndsAt = $user->trialEndsAt('main');
}

如果你想明确判断用户是否仍处于“generic”试用期,且尚未创建实际订阅,也可以使用 onGenericTrial 方法:

php
if ($user->onGenericTrial()) {
    // 用户正处于“generic”试用期内...
}

延长试用期 ​

extendTrial 方法允许你在订阅创建后延长其试用期。即使试用已经结束,客户已经开始为订阅付款,你仍然可以向他们提供延长试用。客户在试用期中额外获得的时间,会从下一张发票中扣除:

php
use App\Models\User;

$subscription = User::find(1)->subscription('default');

// 将试用期结束时间改为 7 天后...
$subscription->extendTrial(
    now()->plus(days: 7)
);

// 在当前试用期基础上额外增加 5 天...
$subscription->extendTrial(
    $subscription->trial_ends_at->plus(days: 5)
);

处理 Stripe Webhook ​

NOTE

在本地开发期间,你可以使用 Stripe CLI 来帮助测试 webhook。

Stripe 可以通过 webhook 将各种事件通知你的应用。默认情况下,Cashier service provider 会自动注册一个指向 Cashier webhook controller 的路由。这个 controller 会处理所有传入的 webhook 请求。

默认情况下,Cashier webhook controller 会自动处理以下情况:因失败扣款次数过多而取消订阅(按 Stripe 设置定义)、customer 更新、customer 删除、订阅更新以及支付方式变更;不过正如后面会看到的那样,你也可以扩展这个 controller 来处理任何 Stripe webhook 事件。

为了确保你的应用能够处理 Stripe webhook,请务必在 Stripe 控制台中配置 webhook URL。默认情况下,Cashier 的 webhook controller 响应 /stripe/webhook 路径。你应在 Stripe 控制台中启用的完整 webhook 列表如下:

  • customer.subscription.created
  • customer.subscription.updated
  • customer.subscription.deleted
  • customer.updated
  • customer.deleted
  • payment_method.automatically_updated
  • invoice.payment_action_required
  • invoice.payment_succeeded

为了方便,Cashier 提供了 cashier:webhook Artisan 命令。这个命令会在 Stripe 中创建一个 webhook,监听 Cashier 所需的全部事件:

shell
php artisan cashier:webhook

默认情况下,创建出的 webhook 会指向由 APP_URL 环境变量和 Cashier 自带的 cashier.webhook 路由所定义的 URL。如果你想使用不同的 URL,可以在执行命令时提供 --url 选项:

shell
php artisan cashier:webhook --url "https://example.com/stripe/webhook"

创建的 webhook 会使用当前 Cashier 版本兼容的 Stripe API 版本。如果你想使用其他 Stripe 版本,可以提供 --api-version 选项:

shell
php artisan cashier:webhook --api-version="2019-12-03"

创建完成后,webhook 会立即生效。如果你希望先创建 webhook,但在准备好之前保持禁用状态,可以在执行命令时传入 --disabled 选项:

shell
php artisan cashier:webhook --disabled

WARNING

请确保使用 Cashier 自带的webhook 签名校验 middleware 来保护传入的 Stripe webhook 请求。

Webhook 与 CSRF 保护 ​

由于 Stripe webhook 需要绕过 Laravel 的 CSRF 保护,你应确保 Laravel 不会尝试校验传入 Stripe webhook 的 CSRF token。为此,你应在应用的 bootstrap/app.php 文件中将 stripe/* 从 CSRF 保护中排除:

php
->withMiddleware(function (Middleware $middleware): void {
    $middleware->preventRequestForgery(except: [
        'stripe/*',
    ]);
})

定义 Webhook 事件处理器 ​

Cashier 会自动处理失败扣款导致的订阅取消,以及其他常见的 Stripe webhook 事件。不过,如果你还有其他想要处理的 webhook 事件,可以通过监听 Cashier 分发的以下事件来实现:

  • Laravel\Cashier\Events\WebhookReceived
  • Laravel\Cashier\Events\WebhookHandled

这两个事件都包含 Stripe webhook 的完整负载。例如,如果你想处理 invoice.payment_succeeded webhook,可以注册一个listener来处理该事件:

php
<?php

namespace App\Listeners;

use Laravel\Cashier\Events\WebhookReceived;

class StripeEventListener
{
    /**
     * Handle received Stripe webhooks.
     */
    public function handle(WebhookReceived $event): void
    {
        if ($event->payload['type'] === 'invoice.payment_succeeded') {
            // 处理传入事件...
        }
    }
}

校验 Webhook 签名 ​

为了保护你的 webhook,你可以使用 Stripe 的 webhook 签名。为了方便,Cashier 已自动内置了一个 middleware,用于校验传入的 Stripe webhook 请求是否合法。

要启用 webhook 校验,请确保在应用的 .env 文件中设置了 STRIPE_WEBHOOK_SECRET 环境变量。这个 webhook secret 可以从 Stripe 账户 Dashboard 中获取。

单次收费 ​

简单收费 ​

如果你想使用支付方式标识符对某个 customer 执行一次性收费,可以在 billable 模型实例上使用 charge 方法。如果你需要在处理一次性收费前收集客户的支付详情,请参阅单次收费的 Payment Element 文档:

php
use Illuminate\Http\Request;

Route::post('/purchase', function (Request $request) {
    $payment = $request->user()->charge(
        100, $request->paymentMethodId
    );

    // ...
});

charge 方法接受一个数组作为第三个参数,使你可以把任何想传给底层 Stripe Payment Intent 创建接口的选项传入。关于创建 Payment Intent 时可用选项的更多信息,请参阅 Stripe 文档:

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

你也可以在没有底层 customer 或 user 的情况下使用 charge 方法。为此,请在应用的 billable 模型新实例上调用 charge 方法:

php
use App\Models\User;

$payment = (new User)->charge(100, $paymentMethod);

如果收费失败,charge 方法会抛出异常。如果收费成功,则该方法会返回一个 Laravel\Cashier\Payment 实例:

php
try {
    $payment = $user->charge(100, $paymentMethod);
} catch (Exception $e) {
    // ...
}

WARNING

charge 方法接受的金额单位是应用所用货币的最小计量单位。例如,如果客户使用美元付款,则金额应以美分表示。

搭配发票的收费 ​

有时你可能需要执行一次性收费,同时向客户提供 PDF 发票。invoicePrice 方法正是为此而生。例如,下面我们为客户开具 5 件新 T 恤的发票:

php
$user->invoicePrice('price_tshirt', 5);

发票会立即使用用户的默认支付方式扣款。invoicePrice 方法还接受一个数组作为第三个参数,该数组包含发票项的计费选项。方法的第四个参数同样是数组,应包含发票本身的计费选项:

php
$user->invoicePrice('price_tshirt', 5, [
    'discounts' => [
        ['coupon' => 'SUMMER21SALE']
    ],
], [
    'default_tax_rates' => ['txr_id'],
]);

与 invoicePrice 类似,你也可以使用 tabPrice 方法,通过先把多个项目添加到 customer 的“tab”中,再为其开票,从而创建一次性收费(每张发票最多 250 个项目)。例如,我们可以为客户开具 5 件 T 恤和 2 个马克杯的发票:

php
$user->tabPrice('price_tshirt', 5);
$user->tabPrice('price_mug', 2);
$user->invoice();

或者,你也可以使用 invoiceFor 方法,对客户的默认支付方式执行“一次性”收费:

php
$user->invoiceFor('一次性费用', 500);

虽然你可以使用 invoiceFor 方法,但更推荐你使用预定义 price 的 invoicePrice 和 tabPrice 方法。这样,你就可以在 Stripe Dashboard 中获得更好的销售分析和按产品维度的数据。

WARNING

invoice、invoicePrice 和 invoiceFor 方法都会创建 Stripe 发票,并在收费失败时自动重试。如果你不希望发票在失败后重试,则需要在首次收费失败后通过 Stripe API 将发票关闭。

创建 Payment Intent ​

你可以在 billable 模型实例上调用 pay 方法来创建新的 Stripe payment intent。调用此方法后,会创建一个被包装为 Laravel\Cashier\Payment 实例的 payment intent:

php
use Illuminate\Http\Request;

Route::post('/pay', function (Request $request) {
    $payment = $request->user()->pay(
        $request->get('amount')
    );

    return $payment->client_secret;
});

创建 payment intent 之后,你可以把 client secret 返回给应用前端,以便用户在浏览器中完成付款。关于如何利用 Stripe payment intent 构建完整支付流程的更多内容,请参阅 Stripe 文档。

使用 pay 方法时,Stripe Dashboard 中已启用的默认支付方式都会对客户可用。或者,如果你只想允许使用部分指定支付方式,可以使用 payWith 方法:

php
use Illuminate\Http\Request;

Route::post('/pay', function (Request $request) {
    $payment = $request->user()->payWith(
        $request->get('amount'), ['card', 'bancontact']
    );

    return $payment->client_secret;
});

WARNING

pay 和 payWith 方法接受的金额单位是应用所用货币的最小计量单位。例如,如果客户使用美元付款,则金额应以美分表示。

退款 ​

如果你需要为某个 Stripe 付款退款,可以使用 refund 方法。这个方法的第一个参数接受 Stripe Payment Intent ID:

php
$payment = $user->charge(100, $paymentMethodId);

$user->refund($payment->id);

发票 ​

获取发票 ​

你可以使用 invoices 方法,轻松获取某个 billable 模型的发票数组。invoices 方法返回的是 Laravel\Cashier\Invoice 实例集合:

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

如果你想在结果中包含待处理发票,可以使用 invoicesIncludingPending 方法:

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

你可以使用 findInvoice 方法,通过 ID 获取某张特定发票:

php
$invoice = $user->findInvoice($invoiceId);

显示发票信息 ​

在列出客户发票时,你可以使用发票对象的方法来展示相关发票信息。例如,你可能希望在表格中列出每一张发票,并允许用户轻松下载:

blade
<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>

即将到来的发票 ​

要获取某个 customer 的下一张发票,可以使用 upcomingInvoice 方法:

php
$invoice = $user->upcomingInvoice();

同样地,如果该 customer 有多个订阅,你也可以获取某个特定订阅的下一张发票:

php
$invoice = $user->subscription('default')->upcomingInvoice();

预览订阅发票 ​

使用 previewInvoice 方法,你可以在变更 price 之前预览发票。这可以帮助你判断,当某个价格变更发生后,客户收到的发票会是什么样子:

php
$invoice = $user->subscription('default')->previewInvoice('price_yearly');

你也可以向 previewInvoice 方法传入一个价格数组,以便预览包含多个新价格的发票:

php
$invoice = $user->subscription('default')->previewInvoice(['price_yearly', 'price_metered']);

生成发票 PDF ​

在生成发票 PDF 之前,你应使用 Composer 安装 Dompdf 库,它是 Cashier 默认的发票渲染器:

shell
composer require dompdf/dompdf

在路由或控制器中,你可以使用 downloadInvoice 方法,为指定发票生成 PDF 下载。这个方法会自动生成下载发票所需的正确 HTTP 响应:

php
use Illuminate\Http\Request;

Route::get('/user/invoice/{invoice}', function (Request $request, string $invoiceId) {
    return $request->user()->downloadInvoice($invoiceId);
});

默认情况下,发票上的所有数据都来源于 Stripe 中存储的 customer 与 invoice 数据。文件名基于你的 app.name 配置值。不过,你也可以向 downloadInvoice 方法传入第二个参数数组,自定义其中部分数据。这个数组可让你自定义公司与产品等信息:

php
return $request->user()->downloadInvoice($invoiceId, [
    'vendor' => 'Your Company',
    'product' => 'Your Product',
    'street' => 'Main Str. 1',
    'location' => '2000 Antwerp, Belgium',
    'phone' => '+32 499 00 00 00',
    'email' => 'info@example.com',
    'url' => 'https://example.com',
    'vendorVat' => 'BE123456789',
]);

downloadInvoice 方法还支持通过第三个参数自定义文件名。该文件名会自动追加 .pdf 后缀:

php
return $request->user()->downloadInvoice($invoiceId, [], 'my-invoice');

自定义发票渲染器 ​

Cashier 也支持使用自定义发票渲染器。默认情况下,Cashier 使用 DompdfInvoiceRenderer 实现,它借助 dompdf PHP 库来生成 Cashier 发票。不过,你也可以通过实现 Laravel\Cashier\Contracts\InvoiceRenderer 接口来使用任意你想要的渲染器。例如,你可能希望通过调用第三方 PDF 渲染服务的 API 来生成发票 PDF:

php
use Illuminate\Support\Facades\Http;
use Laravel\Cashier\Contracts\InvoiceRenderer;
use Laravel\Cashier\Invoice;

class ApiInvoiceRenderer implements InvoiceRenderer
{
    /**
     * Render the given invoice and return the raw PDF bytes.
     */
    public function render(Invoice $invoice, array $data = [], array $options = []): string
    {
        $html = $invoice->view($data)->render();

        return Http::get('https://example.com/html-to-pdf', ['html' => $html])->get()->body();
    }
}

实现好发票渲染器契约后,你应更新应用 config/cashier.php 配置文件中的 cashier.invoices.renderer 配置值。这个配置值应设置为你的自定义渲染器实现类名。

Checkout ​

Cashier Stripe 同样支持 Stripe Checkout。Stripe Checkout 通过提供预构建的托管支付页面,免去了你自己实现自定义支付页面的负担。

下面的文档介绍了如何使用 Cashier 开始接入 Stripe Checkout。若要进一步了解 Stripe Checkout,你还应阅读 Stripe 自己的 Checkout 文档。

Product Checkout ​

你可以在 billable 模型上使用 checkout 方法,对 Stripe Dashboard 中已存在的 product 发起 checkout。checkout 方法会启动一个新的 Stripe Checkout 会话。默认情况下,你需要传入一个 Stripe Price ID:

php
use Illuminate\Http\Request;

Route::get('/product-checkout', function (Request $request) {
    return $request->user()->checkout('price_tshirt');
});

如有需要,你也可以指定 product 数量:

php
use Illuminate\Http\Request;

Route::get('/product-checkout', function (Request $request) {
    return $request->user()->checkout(['price_tshirt' => 15]);
});

当客户访问这个路由时,会被重定向到 Stripe 的 Checkout 页面。默认情况下,当用户成功完成购买或取消购买时,会被重定向到你的 home 路由;不过你也可以通过 success_url 和 cancel_url 选项指定自定义回调 URL:

php
use Illuminate\Http\Request;

Route::get('/product-checkout', function (Request $request) {
    return $request->user()->checkout(['price_tshirt' => 1], [
        'success_url' => route('your-success-route'),
        'cancel_url' => route('your-cancel-route'),
    ]);
});

在定义 success_url checkout 选项时,你可以指示 Stripe 在调用该 URL 时,把 checkout session ID 作为查询字符串参数追加进去。为此,只需把字面字符串 {CHECKOUT_SESSION_ID} 添加到 success_url 查询字符串中。Stripe 会用真实的 checkout session ID 替换这个占位符:

php
use Illuminate\Http\Request;
use Stripe\Checkout\Session;
use Stripe\Customer;

Route::get('/product-checkout', function (Request $request) {
    return $request->user()->checkout(['price_tshirt' => 1], [
        'success_url' => route('checkout-success').'?session_id={CHECKOUT_SESSION_ID}',
        'cancel_url' => route('checkout-cancel'),
    ]);
});

Route::get('/checkout-success', function (Request $request) {
    $checkoutSession = $request->user()->stripe()->checkout->sessions->retrieve($request->get('session_id'));

    return view('checkout.success', ['checkoutSession' => $checkoutSession]);
})->name('checkout-success');

Promotion Code ​

默认情况下,Stripe Checkout 不允许使用用户可兑换的 promotion code。幸运的是,有一种简单方式可以为你的 Checkout 页面启用它们。你可以调用 allowPromotionCodes 方法:

php
use Illuminate\Http\Request;

Route::get('/product-checkout', function (Request $request) {
    return $request->user()
        ->allowPromotionCodes()
        ->checkout('price_tshirt');
});

单次收费 Checkout ​

你也可以为一个未在 Stripe Dashboard 中创建的临时 product 执行简单收费。为此,可以在 billable 模型上使用 checkoutCharge 方法,并传入收费金额、产品名称以及可选数量。客户访问该路由时,会被重定向到 Stripe 的 Checkout 页面:

php
use Illuminate\Http\Request;

Route::get('/charge-checkout', function (Request $request) {
    return $request->user()->checkoutCharge(1200, 'T-Shirt', 5);
});

WARNING

使用 checkoutCharge 方法时,Stripe 总会在 Dashboard 中创建新的 product 和 price。因此,更推荐你预先在 Stripe Dashboard 中创建 product,然后改用 checkout 方法。

订阅 Checkout ​

WARNING

对订阅使用 Stripe Checkout,要求你在 Stripe Dashboard 中启用 customer.subscription.created webhook。这个 webhook 会在你的数据库中创建订阅记录,并保存所有相关订阅项。

你也可以使用 Stripe Checkout 来发起订阅。在使用 Cashier 的 subscription builder 方法定义好订阅之后,可以调用 checkout 方法。客户访问该路由时,会被重定向到 Stripe 的 Checkout 页面:

php
use Illuminate\Http\Request;

Route::get('/subscription-checkout', function (Request $request) {
    return $request->user()
        ->newSubscription('default', 'price_monthly')
        ->checkout();
});

和 product checkout 一样,你也可以自定义成功与取消的 URL:

php
use Illuminate\Http\Request;

Route::get('/subscription-checkout', function (Request $request) {
    return $request->user()
        ->newSubscription('default', 'price_monthly')
        ->checkout([
            'success_url' => route('your-success-route'),
            'cancel_url' => route('your-cancel-route'),
        ]);
});

当然,你也可以为订阅 checkout 启用 promotion code:

php
use Illuminate\Http\Request;

Route::get('/subscription-checkout', function (Request $request) {
    return $request->user()
        ->newSubscription('default', 'price_monthly')
        ->allowPromotionCodes()
        ->checkout();
});

WARNING

很遗憾,Stripe Checkout 在启动订阅时并不支持所有订阅计费选项。在 subscription builder 上使用 anchorBillingCycleOn 方法、设置按比例计费行为,或设置支付行为,在 Stripe Checkout 会话中都不会生效。请参阅 Stripe Checkout Session API 文档,查看哪些参数是可用的。

Stripe Checkout 与试用期 ​

当然,你也可以在构建订阅时定义试用期,然后通过 Stripe Checkout 完成:

php
$checkout = Auth::user()->newSubscription('default', 'price_monthly')
    ->trialDays(3)
    ->checkout();

不过,试用期至少必须为 48 小时,这是 Stripe Checkout 支持的最短试用时长。

订阅与 Webhook ​

请记住,Stripe 和 Cashier 是通过 webhook 来更新订阅状态的。因此,客户在输入支付信息后返回应用时,订阅有可能尚未真正激活。为了处理这种情况,你可能需要向用户显示一条消息,提示他们付款或订阅仍在处理中。

收集 Tax ID ​

Checkout 也支持收集客户的 Tax ID。要在 checkout 会话中启用这个功能,可以在创建会话时调用 collectTaxIds 方法:

php
$checkout = $user->collectTaxIds()->checkout('price_tshirt');

调用此方法后,客户界面中会出现一个新的复选框,允许他们表明自己是否以公司身份购买。如果是,他们就有机会提供自己的 Tax ID 编号。

WARNING

如果你已经在应用的 service provider 中配置了自动税额收集,则该功能会自动启用,无需再调用 collectTaxIds 方法。

Guest Checkout ​

使用 Checkout::guest 方法,你可以为应用中的访客发起 checkout 会话,即使他们没有“账户”:

php
use Illuminate\Http\Request;
use Laravel\Cashier\Checkout;

Route::get('/product-checkout', function (Request $request) {
    return Checkout::guest()->create('price_tshirt', [
        'success_url' => route('your-success-route'),
        'cancel_url' => route('your-cancel-route'),
    ]);
});

与为已有用户创建 checkout 会话类似,你也可以利用 Laravel\Cashier\CheckoutBuilder 实例上提供的其他方法,自定义 guest checkout 会话:

php
use Illuminate\Http\Request;
use Laravel\Cashier\Checkout;

Route::get('/product-checkout', function (Request $request) {
    return Checkout::guest()
        ->withPromotionCode('promo-code')
        ->create('price_tshirt', [
            'success_url' => route('your-success-route'),
            'cancel_url' => route('your-cancel-route'),
        ]);
});

guest checkout 完成后,Stripe 可以派发 checkout.session.completed webhook 事件,因此请确保你已配置 Stripe webhook,让该事件实际发送到你的应用。你在 Stripe Dashboard 中启用此 webhook 后,就可以让 Cashier 处理该 webhook。webhook 负载中包含的对象会是一个 checkout 对象,你可以检查它以完成客户订单。

处理失败付款 ​

有时,无论是订阅付款还是单次收费都可能失败。发生这种情况时,Cashier 会抛出 Laravel\Cashier\Exceptions\IncompletePayment 异常,通知你发生了该情况。捕获这个异常后,你有两种处理方式。

首先,你可以把客户重定向到 Cashier 自带的专用支付确认页面。这个页面已经有一个由 Cashier service provider 注册的命名路由。因此,你可以捕获 IncompletePayment 异常,然后把用户重定向到支付确认页面:

php
use Laravel\Cashier\Exceptions\IncompletePayment;

try {
    $subscription = $user->newSubscription('default', 'price_monthly')
        ->create($paymentMethod);
} catch (IncompletePayment $exception) {
    return redirect()->route(
        'cashier.payment',
        [$exception->payment->id, 'redirect' => route('home')]
    );
}

在支付确认页面中,客户会被提示再次输入信用卡信息,并执行 Stripe 要求的额外动作,例如 “3D Secure” 确认。支付确认完成后,用户会被重定向到上面 redirect 参数指定的 URL。重定向时,URL 上还会附加 message(字符串)和 success(整数)两个查询字符串变量。当前支付页面支持以下支付方式类型:

  • 信用卡
  • Alipay
  • Bancontact
  • BECS Direct Debit
  • EPS
  • Giropay
  • iDEAL
  • SEPA Direct Debit

或者,你也可以让 Stripe 代你完成支付确认。在这种情况下,不需要重定向到支付确认页面,而是可以在 Stripe Dashboard 中配置 Stripe 的自动计费邮件。不过,如果捕获到了 IncompletePayment 异常,你仍应通知用户,他们会收到一封包含后续支付确认说明的邮件。

以下方法都可能抛出支付异常:使用 Billable trait 的模型上的 charge、invoiceFor 和 invoice 方法;对于订阅而言,SubscriptionBuilder 上的 create 方法,以及 Subscription 和 SubscriptionItem 模型上的 incrementAndInvoice 与 swapAndInvoice 方法,都可能抛出 incomplete payment 异常。

你可以在可计费模型或订阅实例上使用 hasIncompletePayment 方法,判断现有订阅是否存在未完成付款:

php
if ($user->hasIncompletePayment('default')) {
    // ...
}

if ($user->subscription('default')->hasIncompletePayment()) {
    // ...
}

你可以通过检查异常实例上的 payment 属性,推导出未完成付款的具体状态:

php
use Laravel\Cashier\Exceptions\IncompletePayment;

try {
    $user->charge(1000, 'pm_card_threeDSecure2Required');
} catch (IncompletePayment $exception) {
    // 获取 payment intent 状态...
    $exception->payment->status;

    // 检查特定条件...
    if ($exception->payment->requiresPaymentMethod()) {
        // ...
    } elseif ($exception->payment->requiresConfirmation()) {
        // ...
    }
}

确认付款 ​

某些支付方式在确认付款时需要额外数据。例如,SEPA 支付方式在支付过程中需要额外的“mandate”数据。你可以使用 withPaymentConfirmationOptions 方法将这些数据提供给 Cashier:

php
$subscription->withPaymentConfirmationOptions([
    'mandate_data' => '...',
])->swap('price_xxx');

你可以参阅 Stripe API 文档,查看确认付款时可接受的所有选项。

强客户认证(SCA) ​

如果你的企业,或者你的某位客户位于欧洲,则你需要遵守欧盟的强客户认证(SCA)法规。这些法规由欧盟在 2019 年 9 月实施,用于防止支付欺诈。幸运的是,Stripe 和 Cashier 都已经为构建符合 SCA 的应用做好了准备。

WARNING

在开始之前,请先阅读 Stripe 关于 PSD2 与 SCA 的指南,以及他们关于新 SCA API 的文档。

需要额外确认的付款 ​

SCA 法规通常要求进行额外验证,才能确认并处理付款。发生这种情况时,Cashier 会抛出 Laravel\Cashier\Exceptions\IncompletePayment 异常,通知你还需要额外验证。关于如何处理这些异常的更多信息,请参阅处理失败付款文档。

Stripe 或 Cashier 展示的支付确认页面,可能会根据具体银行或发卡机构的支付流程进行定制,其中可能包括额外的银行卡确认、临时小额扣款、独立设备验证或其他验证方式。

incomplete 与 past_due 状态 ​

当付款需要额外确认时,订阅会保持在 incomplete 或 past_due 状态,这由其 stripe_status 数据库字段反映。付款确认完成后,且 Stripe 通过 webhook 通知应用确认已完成时,Cashier 会自动激活客户订阅。

关于 incomplete 与 past_due 状态的更多信息,请参阅这些状态的补充文档。

离线会话付款通知 ​

由于 SCA 法规要求客户即使在订阅已经处于活跃状态时,也可能需要偶尔再次确认支付信息,因此当离线会话付款需要确认时,Cashier 可以向客户发送通知。例如,这种情况可能出现在订阅续费时。你可以通过将 CASHIER_PAYMENT_NOTIFICATION 环境变量设置为某个通知类,来启用 Cashier 的支付通知。默认情况下,该通知是关闭的。当然,Cashier 也内置了一个可直接使用的通知类,如果需要你也完全可以提供自己的通知类:

ini
CASHIER_PAYMENT_NOTIFICATION=Laravel\Cashier\Notifications\ConfirmPayment

为了确保离线会话付款确认通知能够送达,请确认你的应用已配置 Stripe webhook,并且已在 Stripe Dashboard 中启用 invoice.payment_action_required webhook。另外,你的 Billable 模型还应使用 Laravel 的 Illuminate\Notifications\Notifiable trait。

WARNING

即使客户正在手动处理一笔需要额外确认的付款,系统也仍然会发送通知。遗憾的是,Stripe 无法知道这笔付款是手动完成的还是“off-session”触发的。不过,如果客户在已经确认付款之后再访问支付页面,只会看到“付款成功”的提示。系统不会允许客户意外重复确认同一笔付款,从而产生第二次误收费。

Stripe SDK ​

Cashier 中的许多对象都是对 Stripe SDK 对象的封装。如果你希望直接与 Stripe 对象交互,可以方便地使用 asStripe 方法获取它们:

php
$stripeSubscription = $subscription->asStripeSubscription();

$stripeSubscription->application_fee_percent = 5;

$stripeSubscription->save();

你也可以使用 updateStripeSubscription 方法,直接更新 Stripe 订阅:

php
$subscription->updateStripeSubscription(['application_fee_percent' => 5]);

如果你想直接使用 Stripe\StripeClient 客户端,可以在 Cashier 类上调用 stripe 方法。例如,你可以通过这个方法访问 StripeClient 实例,并从你的 Stripe 账户中获取价格列表:

php
use Laravel\Cashier\Cashier;

$prices = Cashier::stripe()->prices->all();

测试 ​

在测试使用 Cashier 的应用时,你可以模拟对 Stripe API 的真实 HTTP 请求;但这要求你部分重写 Cashier 自己的行为。因此,我们更推荐让测试直接访问真实的 Stripe API。虽然这样会更慢,但它能更有把握地验证应用是否按预期工作,而且任何较慢的测试都可以单独放进自己的 Pest / PHPUnit 测试组中。

测试时请记住,Cashier 本身已经拥有完善的测试套件,因此你只需要专注于测试自己应用中的订阅与支付流程,而不是重新测试 Cashier 的每一个底层行为。

首先,把 Stripe secret 的测试环境版本添加到你的 phpunit.xml 文件中:

xml
<env name="STRIPE_SECRET" value="sk_test_<your-key>"/>

这样一来,在测试过程中只要你与 Cashier 交互,它就会向 Stripe 的测试环境发送真实的 API 请求。为了方便起见,你应预先在 Stripe 测试账户中准备好测试时会使用到的 subscriptions / prices。

NOTE

为了测试各种计费场景,例如信用卡被拒或支付失败,你可以使用 Stripe 提供的大量测试卡号与 token。