Skip to content

Laravel Cashier (Stripe)

简介

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

升级 Cashier

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

WARNING

为了防止破坏性更改,Cashier 使用固定的 Stripe API 版本。Cashier 16 使用 Stripe API 版本 2025-06-30.basil。Stripe API 版本将在次要版本中更新,以便利用新的 Stripe 功能和改进。

安装

首先,使用 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 文档中找到。

配置

可计费模型

在使用 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;

/**
 * 启动任何应用服务。
 */
public function boot(): void
{
    Cashier::useCustomerModel(User::class);
}

WARNING

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

API 密钥

接下来,你应该在应用程序的 .env 文件中配置 Stripe API 密钥。你可以从 Stripe 控制面板获取 Stripe API 密钥:

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;

/**
 * 启动任何应用服务。
 */
public function boot(): void
{
    Cashier::calculateTaxes();
}

启用税费计算后,任何新订阅和任何一次性发票都将获得自动税费计算。

为了使此功能正常工作,你的客户账单详细信息(例如客户姓名、地址和税务 ID)需要同步到 Stripe。你可以使用 Cashier 提供的客户数据同步税务 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 方法中告知 Cashier 你的自定义模型:

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

/**
 * 启动任何应用服务。
 */
public function boot(): void
{
    Cashier::useSubscriptionModel(Subscription::class);
    Cashier::useSubscriptionItemModel(SubscriptionItem::class);
}

快速入门

销售产品

NOTE

在使用 Stripe Checkout 之前,你应该在 Stripe 控制面板中定义具有固定价格的产品。此外,你应该配置 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 方法将客户重定向到 Stripe Checkout 以进行给定的「价格标识符」。使用 Stripe 时,「价格」指的是特定产品的定义价格

如果需要,checkout 方法将自动在 Stripe 中创建客户,并将该 Stripe 客户记录连接到应用程序数据库中的相应用户。完成结账会话后,客户将被重定向到专用的成功或取消页面,你可以在那里向客户显示信息消息。

向 Stripe Checkout 提供元数据

在销售产品时,通常通过应用程序自己定义的 CartOrder 模型来跟踪已完成的订单和已购买的产品。当将客户重定向到 Stripe Checkout 完成购买时,你可能需要提供现有的订单标识符,以便在客户重定向回你的应用程序时将完成的购买与相应的订单关联起来。

为此,你可以向 checkout 方法提供 metadata 数组。让我们想象一下,当用户开始结账流程时,会在我们的应用程序中创建一个待处理的 Order。请记住,此示例中的 CartOrder 模型是说明性的,不是由 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');

如上例所示,当用户开始结账流程时,我们将购物车/订单的所有关联 Stripe 价格标识符提供给 checkout 方法。当然,你的应用程序负责在客户添加这些项目时将它们与「购物车」或订单关联起来。我们还将订单的 ID 通过 metadata 数组提供给 Stripe Checkout 会话。最后,我们在 Checkout 成功路由中添加了 CHECKOUT_SESSION_ID 模板变量。当 Stripe 将客户重定向回你的应用程序时,此模板变量将自动填充为 Checkout 会话 ID。

接下来,让我们构建 Checkout 成功路由。这是用户通过 Stripe Checkout 完成购买后将被重定向到的路由。在此路由中,我们可以检索 Stripe Checkout 会话 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');

请参阅 Stripe 文档以获取有关 Checkout 会话对象包含的数据的更多信息。

销售订阅

NOTE

在使用 Stripe Checkout 之前,你应该在 Stripe 控制面板中定义具有固定价格的产品。此外,你应该配置 Cashier 的 webhook 处理

通过应用程序提供产品和订阅计费可能令人望而生畏。然而,借助 Cashier 和 Stripe Checkout,你可以轻松构建现代、稳健的支付集成。

要了解如何使用 Cashier 和 Stripe Checkout 销售订阅,让我们考虑一个简单的场景:一个订阅服务提供基本月度(price_basic_monthly)和年度(price_basic_yearly)计划。这两个价格可以在我们的 Stripe 控制面板中归类为「Basic」产品(pro_basic)。此外,我们的订阅服务可能提供 Expert 计划作为 pro_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 方法的 URL。要知道他们的订阅何时真正开始(因为某些支付方式需要几秒钟来处理),我们还需要配置 Cashier 的 webhook 处理

现在客户可以开始订阅,我们需要限制应用程序的某些部分,以便只有订阅用户才能访问。当然,我们始终可以通过 Cashier 的 Billable trait 提供的 subscribed 方法确定用户当前的订阅状态:

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

我们甚至可以轻松确定用户是否订阅了特定产品或价格:

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

@if ($user->subscribedToPrice('price_basic_monthly'))
    <p>您已订阅我们的月度 Basic 计划。</p>
@endif

构建订阅中间件

为方便起见,你可能希望创建一个中间件,用于确定传入的请求是否来自订阅用户。定义此中间件后,你可以轻松将其分配给路由,以防止未订阅的用户访问该路由:

php
<?php

namespace App\Http\Middleware;

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

class Subscribed
{
    /**
     * 处理传入请求。
     */
    public function handle(Request $request, Closure $next): Response
    {
        if (! $request->user()?->subscribed()) {
            // 将用户重定向到账单页面并要求他们订阅...
            return redirect('/billing');
        }

        return $next($request);
    }
}

定义中间件后,你可以将其分配给路由:

php
use App\Http\Middleware\Subscribed;

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

允许客户管理他们的计费计划

当然,客户可能希望将他们的订阅计划更改为另一个产品或「层级」。最简单的方法是将客户引导至 Stripe 的客户计费门户,该门户提供了一个托管用户界面,允许客户下载发票、更新支付方式和更改订阅计划。

要允许客户管理他们的订阅,你可以将他们重定向到计费门户 URL,该 URL 由 Cashier 的 billingPortal 方法提供:

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

默认情况下,当客户完成管理订阅后,他们将被重定向回 home 路由。你可以通过将 URL 作为参数传递给 billingPortal 方法来自定义返回 URL:

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

客户

获取客户

你可以使用 Cashier::findBillable 方法按其 Stripe ID 获取客户。此方法返回可计费模型的实例:

php
use Laravel\Cashier\Cashier;

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

创建客户

有时,你可能希望创建一个 Stripe 客户而不开始订阅。你可以使用 createAsStripeCustomer 方法完成此操作:

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

创建客户后,你可以在稍后开始订阅。你可以传递一个数组作为 createAsStripeCustomer 方法的第二个参数,以提供 Stripe API 支持的任何其他客户创建选项:

php
$stripeCustomer = $user->createAsStripeCustomer([
    'email' => $user->email,
    'name' => $user->name,
]);

如果客户记录已存在于 Stripe 中,你可以使用 updateStripeCustomer 方法更新客户信息:

php
$stripeCustomer = $user->updateStripeCustomer([
    'email' => $user->email,
    'name' => $user->name,
]);

更新客户

你可以使用 updateStripeCustomer 方法更新 Stripe 客户信息。此方法接受一个选项数组,该数组对应于 Stripe API 支持的参数:

php
$user->updateStripeCustomer([
    'email' => $user->email,
    'name' => $user->name,
]);

余额

Stripe 允许你为客户的账户余额充值或扣除金额。这些余额将在后续发票中自动应用。你可以使用 balance 方法获取客户的当前余额:

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

如果余额为正数,则表示客户有可用于未来购买的信用额度。如果余额为负数,则表示客户欠款。要为客户充值余额,你可以使用 creditBalance 方法:

php
$user->creditBalance(500, 'Premium customer top-up');

要从客户余额中扣除金额,你可以使用 debitBalance 方法:

php
$user->debitBalance(300, 'Usage adjustment');

税务 ID

Cashier 提供了一种简单的方法来管理客户的税务 ID。例如,taxIds 方法返回客户的所有税务 ID:

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

你可以使用 findTaxId 方法按 ID 检索特定税务 ID:

php
$taxId = $user->findTaxId($id);

你可以使用 createTaxId 方法创建新的税务 ID:

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

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

php
$user->deleteTaxId($id);

与 Stripe 同步客户数据

默认情况下,Cashier 会在订阅创建或更新时自动将客户的姓名、电子邮件地址和电话号码同步到 Stripe。但是,你可能希望自定义此行为或添加其他数据同步。

要自定义同步的数据,你可以在可计费模型上定义 stripeAddress 方法。此方法应返回一个包含客户地址信息的数组:

php
public function stripeAddress()
{
    return [
        'line1' => $this->address_line_1,
        'line2' => $this->address_line_2,
        'city' => $this->city,
        'state' => $this->state,
        'postal_code' => $this->postal_code,
        'country' => $this->country,
    ];
}

你还可以在可计费模型上定义 stripeNamestripeEmail 方法来自定义同步的姓名和电子邮件:

php
public function stripeName()
{
    return $this->company_name ?? $this->name;
}

public function stripeEmail()
{
    return $this->billing_email ?? $this->email;
}

计费门户

Stripe 提供了一个计费门户,允许客户管理他们的订阅、支付方式和发票。你可以使用 billingPortal 方法将客户重定向到计费门户:

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

默认情况下,当客户完成管理订阅后,他们将被重定向回 home 路由。你可以通过将 URL 作为参数传递给 billingPortal 方法来自定义返回 URL:

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

支付方式

存储支付方式

在向客户收费之前,他们通常需要添加支付方式。Cashier 提供了几种方法来管理支付方式。

要添加新的支付方式,你可以使用 addPaymentMethod 方法:

php
use Stripe\PaymentMethod;

$paymentMethod = PaymentMethod::retrieve($paymentMethodId);

$user->addPaymentMethod($paymentMethod);

或者,你可以使用 updateDefaultPaymentMethod 方法添加支付方式并将其设置为默认:

php
$user->updateDefaultPaymentMethod($paymentMethodId);

获取支付方式

你可以使用 paymentMethods 方法获取客户的所有支付方式:

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

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

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

你可以使用 findPaymentMethod 方法按 ID 检索特定支付方式:

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

支付方式是否存在

要确定客户是否已添加支付方式,你可以使用 hasDefaultPaymentMethod 方法:

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

要确定客户是否有任何支付方式,你可以使用 hasPaymentMethod 方法:

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

更新默认支付方式

要更新客户的默认支付方式,你可以使用 updateDefaultPaymentMethod 方法:

php
$user->updateDefaultPaymentMethod($paymentMethodId);

添加支付方式

要向客户添加新的支付方式,你可以使用 addPaymentMethod 方法:

php
$user->addPaymentMethod($paymentMethodId);

删除支付方式

要删除支付方式,你可以使用 deletePaymentMethod 方法:

php
$user->deletePaymentMethod($paymentMethodId);

要删除所有支付方式,你可以使用 deletePaymentMethods 方法:

php
$user->deletePaymentMethods();

订阅

创建订阅

要创建订阅,首先应该获取可计费模型的实例,通常是 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 方法的第一个参数应该是订阅的内部类型。如果你的应用程序只提供一种订阅,你可以将其命名为 defaultprimary。此订阅类型仅供内部应用程序使用,不应向用户显示。第二个参数是用户订阅的 Stripe 价格 ID。

create 方法接受一个支付方式标识符,它将创建订阅并将客户 ID 和其他相关账单信息添加到数据库中。

试用期

如果你想在创建订阅时提供试用期,可以在调用 create 方法之前链式调用 trialDays 方法:

php
$user->newSubscription('default', 'price_monthly')
    ->trialDays(10)
    ->create($request->paymentMethodId);

或者,你可能希望试用在特定日期结束,这可以使用 trialUntil 方法:

php
use Carbon\Carbon;

$user->newSubscription('default', 'price_monthly')
    ->trialUntil(Carbon::now()->addDays(10))
    ->create($request->paymentMethodId);

优惠券

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

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

或者,如果你想应用促销代码,可以使用 withPromotionCode 方法:

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

订阅元数据

如果你想向订阅添加元数据,可以使用 withMetadata 方法:

php
$user->newSubscription('default', 'price_monthly')
    ->withMetadata(['key' => 'value'])
    ->create($request->paymentMethodId);

添加订阅

如果你想为已有默认支付方式的客户添加订阅,可以在订阅构建器上调用 add 方法:

php
use App\Models\User;

$user = User::find(1);

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

从 Stripe 控制面板创建订阅

你也可以从 Stripe 控制面板本身创建订阅。这样做时,Cashier 将同步新添加的订阅并为其分配 default 类型。要自定义分配给控制面板创建的订阅的订阅类型,请定义 webhook 事件处理器

此外,你只能通过 Stripe 控制面板创建一种类型的订阅。如果你的应用程序提供使用不同类型的多个订阅,则只能通过 Stripe 控制面板添加一种类型的订阅。

最后,你应该始终确保应用程序提供的每种订阅类型只有一个活动订阅。如果客户有两个 default 订阅,Cashier 将只使用最近添加的订阅,尽管两者都会与你的应用程序数据库同步。

检查订阅状态

一旦客户订阅了你的应用程序,你就可以使用各种便捷方法轻松检查他们的订阅状态。首先,如果客户有活动订阅,subscribed 方法返回 true,即使订阅当前处于试用期。subscribed 方法接受订阅类型作为第一个参数:

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

subscribed 方法也非常适合作为路由中间件,允许你根据用户的订阅状态过滤对路由和控制器的访问:

php
<?php

namespace App\Http\Middleware;

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

class EnsureUserIsSubscribed
{
    /**
     * 处理传入请求。
     *
     * @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 产品标识符确定用户是否订阅了给定产品。在 Stripe 中,产品是价格的集合。在此示例中,我们将确定用户的 default 订阅是否主动订阅了应用程序的「premium」产品。给定的 Stripe 产品标识符应与 Stripe 控制面板中的产品标识符之一对应:

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

通过将数组传递给 subscribedToProduct 方法,你可以确定用户的 default 订阅是否主动订阅了应用程序的「basic」或「premium」产品:

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

subscribedToPrice 方法可用于确定客户的订阅是否对应给定的价格 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。订阅状态存储在 Cashier 的 subscriptions 数据库表的 stripe_status 列中。

同样,如果在切换价格时需要二次支付操作,则订阅将被标记为 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_dueincomplete 状态时仍被视为活动,可以使用 Cashier 提供的 keepPastDueSubscriptionsActivekeepIncompleteSubscriptionsActive 方法。通常,这些方法应该在 App\Providers\AppServiceProviderregister 方法中调用:

php
use Laravel\Cashier\Cashier;

/**
 * 注册任何应用服务。
 */
public function register(): void
{
    Cashier::keepPastDueSubscriptionsActive();
    Cashier::keepIncompleteSubscriptionsActive();
}

WARNING

当订阅处于 incomplete 状态时,在付款确认之前无法更改。因此,当订阅处于 incomplete 状态时,swapupdateQuantity 方法将抛出异常。

订阅作用域

大多数订阅状态也可用作查询作用域,以便你可以轻松查询数据库以获取处于给定状态的订阅:

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();

更改价格

客户订阅你的应用程序后,他们可能偶尔想要更改为新的订阅价格。要将客户切换到新价格,请将 Stripe 价格标识符传递给 swap 方法。切换价格时,假设用户希望重新激活他们的订阅(如果之前已取消)。给定的价格标识符应与 Stripe 控制面板中可用的 Stripe 价格标识符对应:

php
use App\Models\User;

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

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

如果客户处于试用期,试用期将被保留。此外,如果订阅存在「数量」,该数量也将被保留。

如果你想切换价格并取消客户当前所处的任何试用期,可以调用 skipTrial 方法:

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

如果你想切换价格并立即向客户开票而不是等待下一个计费周期,可以使用 swapAndInvoice 方法:

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

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

按比例分配

默认情况下,Stripe 在价格之间切换时按比例分配费用。可以使用 noProrate 方法更新订阅价格而不按比例分配费用:

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

有关订阅按比例分配的更多信息,请查阅 Stripe 文档

WARNING

swapAndInvoice 方法之前执行 noProrate 方法对按比例分配没有影响。发票将始终发出。

订阅数量

有时订阅受「数量」影响。例如,项目管理应用程序可能每月每个项目收费 10 美元。你可以使用 incrementQuantitydecrementQuantity 方法轻松增加或减少订阅数量:

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 文档

多产品订阅的数量

如果你的订阅是多产品订阅,你应该将要增加或减少数量的价格 ID 作为增量/减量方法的第二个参数传递:

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

多产品订阅

多产品订阅允许你将多个计费产品分配给单个订阅。例如,想象你正在构建一个客户服务「帮助台」应用程序,其基础订阅价格为每月 10 美元,但提供实时聊天附加产品,每月额外收费 15 美元。多产品订阅的信息存储在 Cashier 的 subscription_items 数据库表中。

你可以通过将价格数组作为 newSubscription 方法的第二个参数来为给定订阅指定多个产品:

php
use Illuminate\Http\Request;

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

    // ...
});

在上面的示例中,客户将在其 default 订阅上附加两个价格。两个价格将按各自的计费间隔收费。如果需要,你可以使用 quantity 方法为每个价格指定特定数量:

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

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

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

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

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

上面的示例将添加新价格,客户将在下一个计费周期被收费。如果你想立即向客户收费,可以使用 addPriceAndInvoice 方法:

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

如果你想添加具有特定数量的价格,可以将数量作为 addPriceaddPriceAndInvoice 方法的第二个参数传递:

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

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

你可以使用 removePrice 方法从订阅中删除价格:

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

WARNING

你不能删除订阅上的最后一个价格。相反,你应该直接取消订阅。

切换价格

你还可以更改附加到多产品订阅的价格。例如,想象客户有一个带有 price_chat 附加产品的 price_basic 订阅,你想将客户从 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 方法。如果你想保留订阅其他价格上的所有现有元数据,这种方法特别有用:

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

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

按比例分配

默认情况下,当从多产品订阅添加或删除价格时,Stripe 将按比例分配费用。如果你想进行价格调整而不按比例分配,应该将 noProrate 方法链接到你的价格操作:

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

数量

如果你想更新单个订阅价格的数量,可以使用现有数量方法,将价格 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

当订阅有多个价格时,Subscription 模型上的 stripe_pricequantity 属性将为 null。要访问单个价格属性,你应该使用 Subscription 模型上可用的 items 关系。

订阅项目

当订阅有多个价格时,它将在数据库的 subscription_items 表中有多个订阅「项目」。你可以通过订阅上的 items 关系访问这些项目:

php
use App\Models\User;

$user = User::find(1);

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

// 获取特定项目的 Stripe 价格和数量...
$stripePrice = $subscriptionItem->stripe_price;
$quantity = $subscriptionItem->quantity;

你还可以使用 findItemOrFail 方法检索特定价格:

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 订阅上的价格:

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

当然,你也可以完全取消订阅:

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

基于使用量的计费

基于使用量的计费允许你根据客户在计费周期内的使用量向他们收费。要使用基于使用量的计费,你需要在 Stripe 控制面板中创建一个具有「计量」计费方式的价格。

要报告使用量,你可以使用 reportUsage 方法:

php
$user->subscription('default')->reportUsage(5);

你还可以为特定订阅项目报告使用量:

php
$user->subscription('default')->reportUsageFor('price_metered', 5);

要检索使用量记录,可以使用 usageRecords 方法:

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

订阅税费

要指定用户为订阅支付的税率,你应该在可计费模型上实现 taxRates 方法并返回一个包含 Stripe 税率 ID 的数组:

php
public function taxRates(): array
{
    return ['txr_id'];
}

这使你能够基于每个客户应用不同的税率,这对于跨多个国家和税率运营的企业可能很有帮助。

如果你的订阅是多产品订阅,你可以在可计费模型上实现 priceTaxRates 方法来定义每个价格的不同税率:

php
public function priceTaxRates(): array
{
    return [
        'price_monthly' => ['txr_id'],
        'price_yearly' => ['txr_id'],
    ];
}

WARNING

taxRates 方法使你能够动态确定客户订阅的税率。如果你使用的是 Stripe Tax,则此方法将不会被调用。

订阅锚定日期

默认情况下,计费周期锚点是创建订阅的日期。如果你想更改计费周期锚点,可以使用 anchorBillingCycleOn 方法:

php
use Illuminate\Http\Request;

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

    // ...
});

有关订阅计费周期锚点的更多信息,请查阅 Stripe 文档

取消订阅

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

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

取消订阅后,Cashier 将自动在数据库中更新 ends_at 列。此列用于了解 subscribed 方法何时应开始返回 false

如果你想立即取消订阅,可以调用 cancelNow 方法:

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

如果你想取消订阅但保留订阅直到当前计费周期结束,可以调用 cancelAtEndOfPeriod 方法:

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

你还可以在特定时间取消订阅:

php
$user->subscription('default')->cancelAt(now()->addDays(10));

恢复订阅

如果客户已取消订阅但你想恢复它,可以调用 resume 方法。客户必须仍处于其「宽限期」才能恢复订阅:

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

如果订阅已取消但在订阅仍处于「宽限期」时客户想要恢复,你可以使用 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 在此日期之后才开始向客户收费。

WARNING

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

你可以使用 onTrial 方法确定用户是否处于试用期:

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

你还可以使用 onGenericTrial 方法确定用户是否处于其任何订阅的「通用」试用期内:

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

不预先提供支付方式

如果你想在不预先收集支付方式信息的情况下提供试用期,可以将用户记录上的 trial_ends_at 列设置为你希望试用期结束的日期:

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

$user->trial_ends_at = now()->addDays(10);

$user->save();

你可以使用 onGenericTrial 方法确定用户是否处于其任何订阅的「通用」试用期内:

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

延长试用期

你可以使用 extendTrial 方法延长现有订阅的试用期:

php
$user->subscription('default')->extendTrial(now()->addDays(5));

处理 Stripe Webhooks

Cashier 可以自动处理 Stripe webhook,以保持你的应用程序数据库与 Stripe 同步。要启用 webhook 处理,请确保在应用程序的 .env 文件中配置了 STRIPE_WEBHOOK_SECRET 环境变量。

然后,将 webhook URL 配置到你的 Stripe 控制面板中。默认情况下,Cashier 的 webhook 路由响应 /stripe/webhook URL 路径。

WARNING

确保你在 Stripe 控制面板中启用了 Cashier 处理的所有 webhook 事件。

定义 Webhook 事件处理器

Cashier 会自动处理订阅取消、更新和删除等事件。但是,你可能希望定义自己的事件处理器来处理其他 webhook 事件。

为此,请在 App\Providers\AppServiceProvider 类的 boot 方法中注册你的事件监听器:

php
use Laravel\Cashier\Events\WebhookReceived;

/**
 * 启动任何应用服务。
 */
public function boot(): void
{
    Event::listen(function (WebhookReceived $event) {
        if ($event->payload['type'] === 'invoice.payment_succeeded') {
            // 处理事件...
        }
    });
}

或者,你可以创建一个事件监听器类:

php
<?php

namespace App\Listeners;

use Laravel\Cashier\Events\WebhookReceived;

class StripeEventListener
{
    /**
     * 处理接收到的 Stripe webhook。
     */
    public function handle(WebhookReceived $event): void
    {
        if ($event->payload['type'] === 'invoice.payment_succeeded') {
            // 处理事件...
        }
    }
}

然后在 App\Providers\EventServiceProvider 中注册监听器:

php
use App\Listeners\StripeEventListener;
use Laravel\Cashier\Events\WebhookReceived;

protected $listen = [
    WebhookReceived::class => [
        StripeEventListener::class,
    ],
];

验证 Webhook 签名

为了保护你的 webhook 路由,Cashier 会自动验证传入的 webhook 请求是否来自 Stripe。这是通过检查 Stripe 在请求中包含的签名来完成的。

要启用签名验证,请确保在应用程序的 .env 文件中设置了 STRIPE_WEBHOOK_SECRET 环境变量:

ini
STRIPE_WEBHOOK_SECRET=your-stripe-webhook-secret

单次收费

简单收费

如果你想对客户进行一次性收费,可以使用可计费模型实例上的 charge 方法:

php
use App\Models\User;

$stripeCharge = $user->charge(100, $paymentMethod);

charge 方法接受以最低货币单位表示的金额(例如,如果货币是美元,则为美分)。第二个参数应该是支付方式标识符:

php
use App\Models\User;

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

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

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

WARNING

charge 方法接受应用程序使用的货币的最低单位的支付金额。例如,如果客户以美元支付,金额应以美分为单位指定。

带发票收费

有时你可能需要进行一次性收费并向客户提供 PDF 发票。invoicePrice 方法可以做到这一点。例如,让我们向客户收取五件新衬衫的费用:

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

发票将立即从用户的默认支付方式中收费。invoicePrice 方法还接受一个数组作为第三个参数。此数组包含发票项目的计费选项。该方法接受的第四个参数也是一个数组,应包含发票本身的计费选项:

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

invoicePrice 类似,你可以使用 tabPrice 方法通过将多个项目(每张发票最多 250 个项目)添加到客户的「标签页」然后向客户开票来创建一次性收费。例如,我们可以向客户收取五件衬衫和两个杯子的费用:

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

或者,你可以使用 invoiceFor 方法对客户的默认支付方式进行「一次性」收费:

php
$user->invoiceFor('One Time Fee', 500);

虽然 invoiceFor 方法可供你使用,但建议你使用带有预定义价格的 invoicePricetabPrice 方法。这样做,你将能够在 Stripe 控制面板中访问更好的分析和数据,了解每个产品的销售情况。

WARNING

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

创建支付意图

你可以通过在可计费模型实例上调用 pay 方法来创建新的 Stripe 支付意图。调用此方法将创建一个包装在 Laravel\Cashier\Payment 实例中的支付意图:

php
use Illuminate\Http\Request;

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

    return $payment->client_secret;
});

创建支付意图后,你可以将客户端密钥返回给应用程序的前端,以便用户可以在浏览器中完成支付。要了解有关使用 Stripe 支付意图构建完整支付流程的更多信息,请查阅 Stripe 文档

使用 pay 方法时,Stripe 控制面板中启用的默认支付方式将可供客户使用。或者,如果你只想允许使用某些特定支付方式,可以使用 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

paypayWith 方法接受应用程序使用的货币的最低单位的支付金额。例如,如果客户以美元支付,金额应以美分为单位指定。

退款

如果你需要退还 Stripe 收费,可以使用 refund 方法。此方法接受 Stripe 支付意图 ID作为第一个参数:

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

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

发票

获取发票

你可以使用 invoices 方法轻松获取可计费模型的发票数组。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>

即将到来的发票

要获取客户即将到来的发票,可以使用 upcomingInvoice 方法:

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

同样,如果客户有多个订阅,你也可以获取特定订阅的即将到来的发票:

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

预览订阅发票

使用 previewInvoice 方法,你可以在进行价格更改之前预览发票。这将允许你确定在进行给定价格更改时客户的发票会是什么样子:

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 中的客户和发票数据。文件名基于你的 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
{
    /**
     * 渲染给定发票并返回原始 PDF 字节。
     */
    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 配置值。此配置值应设置为自定义渲染器实现的类名。

结账

Cashier Stripe 还支持 Stripe Checkout。Stripe Checkout 通过提供预构建的托管支付页面,消除了实现自定义支付页面的痛苦。

以下文档包含有关如何开始使用 Cashier 的 Stripe Checkout 的信息。要了解有关 Stripe Checkout 的更多信息,你还应该考虑查看 Stripe 自己的 Checkout 文档

产品结账

你可以使用可计费模型上的 checkout 方法为在 Stripe 控制面板中创建的现有产品执行结账。checkout 方法将启动一个新的 Stripe Checkout 会话。默认情况下,你需要传递 Stripe 价格 ID:

php
use Illuminate\Http\Request;

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

如果需要,你还可以指定产品数量:

php
use Illuminate\Http\Request;

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

当客户访问此路由时,他们将被重定向到 Stripe 的 Checkout 页面。默认情况下,当用户成功完成或取消购买时,他们将被重定向到你的 home 路由位置,但你可以使用 success_urlcancel_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 结账选项时,你可以指示 Stripe 在调用你的 URL 时将结账会话 ID 添加为查询字符串参数。为此,请将文字字符串 {CHECKOUT_SESSION_ID} 添加到你的 success_url 查询字符串中。Stripe 将用实际的结账会话 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');

促销代码

默认情况下,Stripe Checkout 不允许用户可兑换的促销代码。幸运的是,有一种简单的方法可以为你的 Checkout 页面启用这些功能。为此,你可以调用 allowPromotionCodes 方法:

php
use Illuminate\Http\Request;

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

单次收费结账

你还可以为未在 Stripe 控制面板中创建的临时产品执行简单收费。为此,你可以在可计费模型上使用 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 将始终在你的 Stripe 控制面板中创建新产品和价格。因此,我们建议你提前在 Stripe 控制面板中创建产品并使用 checkout 方法。

订阅结账

WARNING

使用 Stripe Checkout 进行订阅需要你在 Stripe 控制面板中启用 customer.subscription.created webhook。此 webhook 将在数据库中创建订阅记录并存储所有相关的订阅项目。

你还可以使用 Stripe Checkout 发起订阅。使用 Cashier 的订阅构建器方法定义订阅后,可以调用 checkout 方法。当客户访问此路由时,他们将被重定向到 Stripe 的 Checkout 页面:

php
use Illuminate\Http\Request;

Route::get('/subscription-checkout', function (Request $request) {
    return $request->user()
        ->newSubscription('default', 'price_monthly')
        ->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'),
        ]);
});

当然,你也可以为订阅结账启用促销代码:

php
use Illuminate\Http\Request;

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

WARNING

遗憾的是,Stripe Checkout 在开始订阅时不支持所有订阅计费选项。在订阅构建器上使用 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 更新订阅状态,因此当客户在输入支付信息后返回应用程序时,订阅可能尚未活动。要处理这种情况,你可能希望显示一条消息,通知用户他们的付款或订阅正在等待处理。

收集税务 ID

Checkout 还支持收集客户的税务 ID。要在结账会话上启用此功能,请在创建会话时调用 collectTaxIds 方法:

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

调用此方法时,客户将看到一个新复选框,允许他们指示是否作为公司购买。如果是,他们将有机会提供税务 ID 号码。

WARNING

如果你已在应用程序的服务提供者中配置了自动税费收集,则此功能将自动启用,无需调用 collectTaxIds 方法。

访客结账

使用 Checkout::guest 方法,你可以为没有「账户」的应用程序访客发起结账会话:

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'),
    ]);
});

与为现有用户创建结账会话类似,你可以利用 Laravel\Cashier\CheckoutBuilder 实例上可用的其他方法来自定义访客结账会话:

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'),
        ]);
});

访客结账完成后,Stripe 可以发送 checkout.session.completed webhook 事件,因此请确保配置你的 Stripe webhook 以实际将此事件发送到你的应用程序。在 Stripe 控制面板中启用 webhook 后,你可以使用 Cashier 处理 webhook。Webhook 负载中包含的对象将是一个 checkout 对象,你可以检查它以完成客户的订单。

处理失败支付

有时,订阅或单次收费的支付可能会失败。发生这种情况时,Cashier 将抛出 Laravel\Cashier\Exceptions\IncompletePayment 异常,通知你发生了这种情况。捕获此异常后,你有两个选择如何继续。

首先,你可以将客户重定向到 Cashier 附带的专用付款确认页面。此页面已经有一个通过 Cashier 的服务提供者注册的关联命名路由。因此,你可以捕获 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')]
    );
}

如果你希望客户自动重定向到付款确认页面,你可以在可计费模型或订阅实例上使用 latestPayment 方法检索付款标识符:

php
Route::get('/fix-incomplete-payment', function (Request $request) {
    $payment = $request->user()->latestPayment();

    if ($payment && $payment->requiresPaymentMethod()) {
        return redirect()->route('cashier.payment', [
            $payment->id,
            'redirect' => route('home')
        ]);
    }

    return redirect()->route('home');
});

在付款确认页面,客户将被提示输入他们的支付方式并完成付款。一旦付款完成,用户将被重定向到通过 redirect 参数指定的 URL。

确认支付

某些支付方式需要额外的数据验证以确认付款。例如,SEPA 直接借记支付方式需要客户签署「授权书」。Cashier 提供了一种简单的方法来处理这些确认要求。

如果付款需要确认,客户将被重定向到付款确认页面,在那里他们可以提供所需的确认信息。一旦确认完成,付款将被处理。

你可以使用 requiresConfirmation 方法确定付款是否需要确认:

php
if ($payment->requiresConfirmation()) {
    // 重定向到付款确认页面...
}

强客户认证 (SCA)

如果你的业务位于欧洲,你需要遵守强客户认证 (SCA) 法规。这些法规要求企业在处理支付时验证客户的身份。

Stripe 和 Cashier 提供了对 SCA 的内置支持。当付款需要额外验证时,Cashier 将抛出 IncompletePayment 异常,允许你引导客户完成验证过程。

需要额外确认的支付

当付款需要额外确认时,客户将被重定向到付款确认页面。在那里,他们可以完成所需的验证步骤。

对于订阅,如果付款需要额外确认,订阅将被标记为 incompletepast_due。你可以使用 hasIncompletePayment 方法检查是否存在不完整的付款:

php
if ($user->hasIncompletePayment('default')) {
    // 重定向到付款确认页面...
}

非会话支付通知

SCA 法规要求在某些情况下需要客户验证,即使客户不在「会话中」(即主动使用你的应用程序)。例如,当订阅续费时,可能需要客户验证。

在这种情况下,Stripe 可以发送通知给客户,要求他们验证付款。你可以配置 Stripe 发送这些通知,或者你可以使用 Cashier 的通知系统。

要启用非会话支付通知,你应该确保你的应用程序配置了正确的 Stripe webhook。当收到需要客户操作的付款通知时,Cashier 可以发送通知给你的用户。

你可以在 App\Providers\AppServiceProvider 中自定义这些通知:

php
use Laravel\Cashier\Cashier;

/**
 * 启动任何应用服务。
 */
public function boot(): void
{
    Cashier::calculateTaxes();
}

Stripe SDK

Cashier 提供了对 Stripe SDK 的便捷访问。你可以使用 Cashier::stripe 方法获取 Stripe 客户端实例:

php
use Laravel\Cashier\Cashier;

$stripe = Cashier::stripe();

你还可以在可计费模型实例上使用 stripe 方法:

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

这允许你使用任何 Stripe SDK 提供的功能:

php
$customers = $user->stripe()->customers->all();

测试

在测试使用 Cashier 的应用程序时,你可以模拟对 Stripe API 的请求。这允许你在不实际调用 Stripe API 的情况下测试你的应用程序。

要模拟 Stripe API,你可以使用 Cashier::fake 方法:

php
use Laravel\Cashier\Cashier;

public function test_subscription_can_be_created()
{
    Cashier::fake();

    $user = User::factory()->create();

    $subscription = $user->newSubscription('default', 'price_monthly')
        ->create('pm_card_visa');

    $this->assertTrue($user->subscribed('default'));
}

你还可以使用 Stripe::fake 方法模拟特定的 Stripe API 响应:

php
use Stripe\Stripe;

public function test_invoice_can_be_retrieved()
{
    Stripe::fake();

    // 测试代码...
}

测试支付方式

在测试时,你可以使用 Stripe 提供的测试支付方式标识符。这些标识符以 pm_card_ 开头:

php
// 成功支付
$paymentMethod = 'pm_card_visa';

// 被拒绝的支付
$paymentMethod = 'pm_card_chargeCustomerFail';

// 需要验证的支付
$paymentMethod = 'pm_card_threeDSecure2Required';

有关 Stripe 测试支付方式的完整列表,请查阅 Stripe 测试文档

测试 Webhook

要测试 webhook 处理,你可以使用 Cashier::fake 方法并手动触发 webhook 事件:

php
use Laravel\Cashier\Events\WebhookReceived;

public function test_webhook_handling()
{
    Cashier::fake();

    $payload = [
        'type' => 'invoice.payment_succeeded',
        'data' => [
            'object' => [
                'id' => 'in_1234567890',
            ],
        ],
    ];

    event(new WebhookReceived($payload));

    // 断言...
}

这使你能够测试你的 webhook 处理逻辑,而无需实际接收来自 Stripe 的 webhook 请求。