laravel
10. 软件包
Laravel Cashier(Stripe)

介绍

Laravel Cashier Stripe (opens in a new tab)Stripe 的 (opens in a new tab) 订阅计费服务提供了一个富有表现力、流畅的接口。它处理了您可能不愿编写的几乎所有样板订阅计费代码。除了基本的订阅管理外,Cashier 还可以处理优惠券、交换订阅、订阅“数量”、取消宽限期,甚至可以生成发票 PDF。

升级 Cashier

在升级到新版本的 Cashier 时,仔细查看 升级指南 (opens in a new tab) 非常重要。

[!警告]
为防止出现破坏性更改,Cashier 使用固定的 Stripe API 版本。Cashier 15 使用 Stripe API 版本 2023 - 10 - 16。为了利用新的 Stripe 功能和改进,Stripe API 版本将在小版本更新中进行更新。

安装

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

composer require laravel/cashier

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

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

然后,迁移您的数据库:

php artisan migrate

Cashier 的迁移将向您的 users 表中添加几个列。它们还将创建一个新的 subscriptions 表来保存您所有客户的订阅,以及一个 subscription_items 表用于具有多种价格的订阅。

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

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

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

[!警告]
Stripe 建议用于存储 Stripe 标识符的任何列都应该是区分大小写的。因此,当使用 MySQL 时,您应该确保 stripe_id 列的列排序规则设置为 utf8_bin。更多关于此的信息可以在 Stripe 文档 (opens in a new tab) 中找到。

配置

可计费模型

在使用 Cashier 之前,将 Billable 特征添加到您的可计费模型定义中。通常,这将是 App\Models\User 模型。此特征提供了各种方法,允许您执行常见的计费任务,例如创建订阅、应用优惠券和更新支付方式信息:

use Laravel\Cashier\Billable;

class User extends Authenticatable
{
    use Billable;
}

Cashier 假定您的可计费模型将是随 Laravel 提供的 App\Models\User 类。如果您希望更改此设置,可以通过 useCustomerModel 方法指定不同的模型。此方法通常应在您的 AppServiceProvider 类的 boot 方法中调用:

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

/**
 * 引导任何应用程序服务。
 */
public function boot(): void
{
    Cashier::useCustomerModel(User::class);
}

[!警告]
如果您使用的模型不是 Laravel 提供的 App\Models\User 模型,则需要发布并修改 提供的 Cashier 迁移,以匹配您的替代模型的表名。

API 密钥

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

STRIPE_KEY=您的 Stripe 密钥
STRIPE_SECRET=您的 Stripe 密钥
STRIPE_WEBHOOK_SECRET=您的 Stripe Webhook 密钥

[!警告]
您应该确保在应用程序的 .env 文件中定义了 STRIPE_WEBHOOK_SECRET 环境变量,因为此变量用于确保传入的 Webhook 实际上来自 Stripe。

货币配置

默认的 Cashier 货币是美元(USD)。您可以通过在应用程序的 .env 文件中设置 CASHIER_CURRENCY 环境变量来更改默认货币:

CASHIER_CURRENCY=eur

除了配置 Cashier 的货币外,您还可以指定一个区域设置,用于在为发票显示格式化货币值时使用。在内部,Cashier 利用 PHP 的 NumberFormatter (opens in a new tab) 来设置货币区域设置:

CASHIER_CURRENCY_LOCALE=nl_BE

[!警告]
为了使用除 en 以外的区域设置,确保在您的服务器上安装并配置了 ext-intl PHP 扩展。

税务配置

借助 Stripe Tax (opens in a new tab),可以为 Stripe 生成的所有发票自动计算税费。您可以通过在应用程序的 App\Providers\AppServiceProvider 类的 boot 方法中调用 calculateTaxes 方法来启用自动税务计算:

use Laravel\Cashier\Cashier;

/**
 * 引导任何应用程序服务。
 */
public function boot(): void
{
    Cashier::calculateTaxes();
}

一旦启用了税务计算,任何新的订阅和生成的任何一次性发票都将获得自动税务计算。

为了使此功能正常工作,您需要将客户的计费详细信息,例如客户的姓名、地址和税务 ID,同步到 Stripe。您可以使用 Cashier 提供的 客户数据同步税务 ID 方法来实现此目的。

日志记录

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

CASHIER_LOGGER=stack

对 Stripe 的 API 调用生成的异常将通过您应用程序的默认日志通道进行记录。

使用自定义模型

您可以自由扩展 Cashier 内部使用的模型,方法是定义自己的模型并扩展相应的 Cashier 模型:

use Laravel\Cashier\Subscription as CashierSubscription;

class Subscription extends CashierSubscription
{
    //...
}

定义模型后,您可以通过 Laravel\Cashier\Cashier 类指示 Cashier 使用您的自定义模型。通常,您应该在应用程序的 App\Providers\AppServiceProvider 类的 boot 方法中告知 Cashier 您的自定义模型:

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

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

快速入门

销售产品

[!注意]
在使用 Stripe Checkout 之前,您应该在 Stripe 仪表板中定义具有固定价格的产品。此外,您应该 配置 Cashier 的 Webhook 处理

通过您的应用程序提供产品和订阅计费可能会令人生畏。然而,多亏了 Cashier 和 Stripe Checkout (opens in a new tab),您可以轻松构建现代、强大的支付集成。

为了向客户收取非周期性的单次收费产品的费用,我们将使用 Cashier 将客户引导至 Stripe Checkout,在那里他们将提供支付详细信息并确认购买。通过 Checkout 完成支付后,客户将被重定向到您在应用程序中选择的成功 URL:

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 时,“价格”是指 为特定产品定义的价格 (opens in a new tab)

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

向 Stripe Checkout 提供元数据

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

要实现此目的,您可以向 checkout 方法提供一个 metadata 数组。让我们想象一下,当用户开始结账过程时,我们的应用程序中会创建一个待处理的 Order。请记住,此示例中的 CartOrder 模型是说明性的,并非由 Cashier 提供。您可以根据自己的应用程序的需求自由实现这些概念:

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 价格标识符。当然,您的应用程序负责在客户添加这些项目时将它们与“购物车”或订单相关联。我们还通过 metadata 数组向 Stripe Checkout 会话提供订单的 ID。最后,我们将 CHECKOUT_SESSION_ID 模板变量添加到了 Checkout 成功路由中。当 Stripe 将客户重定向回您的应用程序时,此模板变量将自动填充 Checkout 会话 ID。

接下来,让我们构建 Checkout 成功路由。这是用户通过 Stripe Checkout 完成购买后将被重定向到的路由。在此路由中,我们可以检索 Stripe Checkout 会话 ID 和相关的 Stripe Checkout 实例,以便访问我们提供的元数据并相应地更新客户的订单:

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 会话对象包含的数据 (opens in a new tab) 的更多信息,请参考 Stripe 的文档。

销售订阅

[!注意]
在使用 Stripe Checkout 之前,您应该在 Stripe 仪表板中定义具有固定价格的产品。此外,您应该 配置 Cashier 的 Webhook 处理

通过您的应用程序提供产品和订阅计费可能会令人生畏。然而,多亏了 Cashier 和 Stripe Checkout (opens in a new tab),您可以轻松构建现代、强大的支付集成。

为了了解如何使用 Cashier 和 Stripe Checkout 销售订阅,让我们考虑一个具有基本月度(price_basic_monthly)和年度

删除支付方式

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

$paymentMethod->delete();

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

$user->deletePaymentMethod('pm_visa');

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

$user->deletePaymentMethods();

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

$user->deletePaymentMethods('sepa_debit');

[!警告]
如果用户有一个活跃的订阅,您的应用程序不应允许他们删除其默认支付方式。

订阅

订阅为您的客户提供了一种设置定期付款的方式。由 Cashier 管理的 Stripe 订阅支持多种订阅价格、订阅数量、试用等。

创建订阅

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

use Illuminate\Http\Request;

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

    //...
});

传递给 newSubscription 方法的第一个参数应该是订阅的内部类型。如果您的应用程序只提供一个订阅,您可以将其称为 defaultprimary。这种订阅类型仅用于内部应用程序使用,不应该向用户展示。此外,它不应包含空格,并且在创建订阅后不应更改。第二个参数是用户订阅的特定价格。该值应与 Stripe 中价格的标识符相对应。

create 方法接受 一个 Stripe 支付方式标识符 或 Stripe PaymentMethod 对象,它将开始订阅并使用可计费模型的 Stripe 客户 ID 和其他相关计费信息更新您的数据库。

[!警告]
将支付方式标识符直接传递给 create 订阅方法也会自动将其添加到用户存储的支付方式中。

通过发票电子邮件收集定期付款

您可以指示 Stripe 在每次客户的定期付款到期时向客户发送发票电子邮件,而不是自动收集客户的定期付款。然后,客户可以在收到发票后手动支付。通过发票收集定期付款时,客户不需要预先提供支付方式:

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

在订阅被取消之前客户有多长时间来支付发票,这是由 days_until_due 选项决定的。默认情况下,这是 30 天;但是,如果您愿意,可以为该选项提供一个特定的值:

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

数量

如果您想在创建订阅时为价格设置特定的 数量 (opens in a new tab),您应该在创建订阅之前在订阅构建器上调用 quantity 方法:

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

其他细节

如果您想指定 Stripe 支持的其他 客户 (opens in a new tab)订阅 (opens in a new tab) 选项,您可以通过将它们作为 create 方法的第二个和第三个参数来实现:

$user->newSubscription('default', 'price_monthly')->create($paymentMethod, [
    'email' => $email,
], [
    'metadata' => ['note' => '一些额外信息。'],
]);

优惠券

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

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

或者,如果您想应用 Stripe 促销代码 (opens in a new tab),您可以使用 withPromotionCode 方法:

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

给定的促销代码 ID 应该是分配给促销代码的 Stripe API ID,而不是面向客户的促销代码。如果您需要根据给定的面向客户的促销代码查找促销代码 ID,可以使用 findPromotionCode 方法:

// 通过面向客户的代码查找促销代码 ID...
$promotionCode = $user->findPromotionCode('SUMMERSALE');

// 通过面向客户的代码查找有效的促销代码 ID...
$promotionCode = $user->findActivePromotionCode('SUMMERSALE');

在上面的示例中,返回的 $promotionCode 对象是 Laravel\Cashier\PromotionCode 的一个实例。这个类装饰了一个底层的 Stripe\PromotionCode 对象。您可以通过调用 coupon 方法来检索与促销代码相关的优惠券:

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

优惠券实例允许您确定折扣金额以及优惠券是代表固定折扣还是百分比折扣:

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

您还可以检索当前应用于客户或订阅的折扣:

$discount = $billable->discount();

$discount = $subscription->discount();

返回的 Laravel\Cashier\Discount 实例装饰了一个底层的 Stripe\Discount 对象实例。您可以通过调用 coupon 方法来检索与此折扣相关的优惠券:

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

如果您想向客户或订阅应用新的优惠券或促销代码,可以通过 applyCouponapplyPromotionCode 方法来实现:

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

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

请记住,您应该使用分配给促销代码的 Stripe API ID,而不是面向客户的促销代码。在给定的时间内,只能向客户或订阅应用一个优惠券或促销代码。

有关此主题的更多信息,请查阅 Stripe 文档中关于 优惠券 (opens in a new tab)促销代码 (opens in a new tab) 的内容。

添加订阅

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

use App\Models\User;

$user = User::find(1);

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

从 Stripe 仪表板创建订阅

您也可以从 Stripe 仪表板本身创建订阅。当这样做时,Cashier 将同步新添加的订阅并为它们分配一个 default 类型。要自定义分配给仪表板创建的订阅的订阅类型,[定义 Webhook 事件处理程序](#定义 Webhook 事件处理程序)。

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

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

检查订阅状态

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

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

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

<?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 方法。这个方法对于确定您是否应该向用户显示他们仍在试用期内的警告很有用:

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

subscribedToProduct 方法可用于根据给定的 Stripe 产品标识符确定用户是否订阅了给定的产品。在 Stripe 中,产品是价格的集合。在这个例子中,我们将确定用户的 default 订阅是否积极订阅了应用程序的 "premium" 产品。给定的 Stripe 产品标识符应该与 Stripe 仪表板中您的产品标识符之一相对应:

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

通过向 subscribedToProduct 方法传递一个数组,您可以确定用户的 default 订阅是否积极订阅了应用程序的 "basic" 或 "premium" 产品:

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

subscribedToPrice 方法可用于确定客户的订阅是否对应于给定的价格 ID:

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

recurring 方法可用于确定用户当前是否已订阅且不再处于试用期内:

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

[!警告]
如果用户有两个具有相同类型的订阅,subscription 方法将始终返回最近的订阅。例如,用户可能有两个类型为 default 的订阅记录;然而,其中一个订阅可能是旧的、已过期的订阅,而另一个是当前的、活跃的订阅。始终会返回最近的订阅,而较旧的订阅则保留在数据库中以供历史审查。

已取消的订阅状态

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

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

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

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

要确定用户是否已取消订阅且不再处于“宽限期”内,您可以使用 ended 方法:

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

不完整和过期状态

如果订阅在创建后需要二次支付操作,订阅将被标记为 incomplete。订阅状态存储在 Cashier 的 subscriptions 数据库表的 stripe_status 列中。

同样,如果在交换价格时需要二次支付操作,订阅将被标记为 past_due。当您的订阅处于这两种状态中的任何一种时,在客户确认付款之前,它将不会处于活动状态。可以使用可计费模型或订阅实例上的 hasIncompletePayment 方法来确定订阅是否有未完成的付款:

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

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

当订阅有未完成的付款时,您应该将用户引导到 Cashier 的付款确认页面,传递 latestPayment 标识符。您可以使用订阅实例上可用的 latestPayment 方法来检索此标识符:

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

如果您希望订阅在处于 past_dueincomplete 状态时仍被视为有效,您可以使用 Cashier 提供的 keepPastDueSubscriptionsActivekeepIncompleteSubscriptionsActive 方法。通常,这些方法应该在您的 App\Providers\AppServiceProviderregister 方法中调用:

use Laravel\Cashier\Cashier;

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

[!警告]
当订阅处于 incomplete 状态时,在付款确认之前不能进行更改。因此,当订阅处于 incomplete 状态时,swapupdateQuantity 方法将抛出异常。

订阅范围

大多数订阅状态也可以作为查询范围使用,以便您可以轻松地查询数据库中处于给定状态的订阅:

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

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

以下是可用范围的完整列表:

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 价格标识符相对应:

use App\Models\User;

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

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

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

如果您想交换价格并取消客户当前正在进行的任何试用期,您可以调用 skipTrial 方法:

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

如果您想交换价格并立即向客户开具发票而不是等待他们的下一个计费周期,您可以使用 swapAndInvoice 方法:

$user = User::find(1);

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

按比例分配

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

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

有关订阅按比例分配的更多信息,请查阅 Stripe 文档 (opens in a new tab)

[!警告]
swapAndInvoice 方法之前执行 noProrate 方法对按比例分配没有影响。发票将始终被开具。

订阅数量

有时订阅会受到“数量”的影响。例如,一个项目管理应用程序可能每月每个项目收取 10 美元。您可以使用 incrementQuantitydecrementQuantity 方法轻松地增加或减少您的订阅数量:

use App\Models\User;

$user = User::find(1);

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

// 将订阅的当前数量增加 5...
$user->subscription('default')->incrementQuantity(5);

$user->subscription('default')->decrement

在 Stripe / Cashier 中定义试用天数

您可以选择在 Stripe 仪表板中定义您的价格所获得的试用天数,或者始终使用 Cashier 明确地传递它们。如果您选择在 Stripe 中定义价格的试用天数,您应该注意,新的订阅(包括过去有过订阅的客户的新订阅)将始终获得试用期,除非您明确调用 skipTrial() 方法。

不预先收集付款方式信息的试用

如果您想在不预先收集用户付款方式信息的情况下提供试用期,您可以将用户记录上的 trial_ends_at 列设置为您期望的试用结束日期。这通常在用户注册期间完成:

use App\Models\User;

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

[!WARNING]
请确保在您的计费模型类定义中为 trial_ends_at 属性添加日期转换

Cashier 将这种类型的试用称为“通用试用”,因为它不与任何现有订阅相关联。如果当前日期未超过 trial_ends_at 的值,计费模型实例上的 onTrial 方法将返回 true

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

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

$user = User::find(1);

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

要检索用户的试用结束日期,您可以使用 trialEndsAt 方法。如果用户正在试用,此方法将返回一个 Carbon 日期实例,如果没有则返回 null。如果您想获取除默认订阅之外的特定订阅的试用结束日期,您还可以传递一个可选的订阅类型参数:

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

如果您想确切地知道用户处于他们的“通用”试用期间并且尚未创建实际订阅,您可以使用 onGenericTrial 方法:

if ($user->onGenericTrial()) {
    // 用户处于他们的“通用”试用期间...
}

延长试用

extendTrial 方法允许您在创建订阅后延长订阅的试用期。如果试用期已经过期并且客户已经为订阅计费,您仍然可以为他们提供延长的试用期。试用期内花费的时间将从客户的下一张发票中扣除:

use App\Models\User;

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

// 在 7 天后结束试用...
$subscription->extendTrial(
    now()->addDays(7)
);

// 为试用期再增加 5 天...
$subscription->extendTrial(
    $subscription->trial_ends_at->addDays(5)
);

处理 Stripe Webhooks

[!NOTE]
您可以使用Stripe CLI (opens in a new tab)在本地开发期间帮助测试 webhooks。

Stripe 可以通过 webhooks 通知您的应用程序各种事件。默认情况下,指向 Cashier 的 webhook 控制器的路由会由 Cashier 服务提供程序自动注册。此控制器将处理所有传入的 webhook 请求。

默认情况下,Cashier webhook 控制器将自动处理因收费失败次数过多(根据您的 Stripe 设置定义)而取消的订阅、客户更新、客户删除、订阅更新和付款方式更改;然而,正如我们即将发现的,您可以扩展此控制器以处理您喜欢的任何 Stripe webhook 事件。

为确保您的应用程序可以处理 Stripe webhooks,请务必在 Stripe 控制面板中配置 webhook URL。默认情况下,Cashier 的 webhook 控制器响应 /stripe/webhook URL 路径。您应该在 Stripe 控制面板中启用的所有 webhooks 的完整列表如下:

  • 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 所需的所有事件:

php artisan cashier:webhook

默认情况下,创建的 webhook 将指向由 APP_URL 环境变量和与 Cashier 一起包含的 cashier.webhook 路由定义的 URL。如果您想使用不同的 URL,可以在调用命令时提供 --url 选项:

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

创建的 webhook 将使用与您的 Cashier 版本兼容的 Stripe API 版本。如果您想使用不同的 Stripe 版本,可以提供 --api-version 选项:

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

创建后,webhook 将立即激活。如果您希望创建 webhook 但在准备好之前将其禁用,可以在调用命令时提供 --disabled 选项:

php artisan cashier:webhook --disabled

[!WARNING]
确保使用 Cashier 包含的webhook 签名验证中间件来保护传入的 Stripe webhook 请求。

Webhooks 和 CSRF 保护

由于 Stripe webhooks 需要绕过 Laravel 的CSRF 保护,您应该确保 Laravel 不会尝试验证传入的 Stripe webhooks 的 CSRF 令牌。要实现这一点,您应该在应用程序的 bootstrap/app.php 文件中从 CSRF 保护中排除 stripe/*

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

定义 Webhook 事件处理程序

Cashier 会自动处理因收费失败而取消的订阅以及其他常见的 Stripe webhook 事件。但是,如果您有其他想要处理的 webhook 事件,您可以通过监听 Cashier 分发的以下事件来实现:

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

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

<?php

namespace App\Listeners;

use Laravel\Cashier\Events\WebhookReceived;

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

验证 Webhook 签名

为了保护您的 webhooks,您可以使用Stripe 的 webhook 签名 (opens in a new tab)。为了方便起见,Cashier 自动包含一个中间件,用于验证传入的 Stripe webhook 请求是否有效。

要启用 webhook 验证,请确保在应用程序的 .env 文件中设置了 STRIPE_WEBHOOK_SECRET 环境变量。webhook secret 可以从您的 Stripe 账户仪表板中检索。

单次收费

简单收费

如果您想对客户进行一次性收费,您可以在计费模型实例上使用 charge 方法。您需要提供付款方式标识符作为 charge 方法的第二个参数:

use Illuminate\Http\Request;

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

    //...
});

charge 方法将一个数组作为其第三个参数,允许您将任何您希望传递给底层 Stripe 收费创建的选项传递给它。有关在创建收费时您可以使用的选项的更多信息,可以在Stripe 文档 (opens in a new tab)中找到:

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

您也可以在没有底层客户或用户的情况下使用 charge 方法。要实现这一点,在您的应用程序的计费模型的新实例上调用 charge 方法:

use App\Models\User;

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

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

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

[!WARNING]
charge 方法接受的付款金额是以您的应用程序使用的货币的最小分母表示的。例如,如果客户以美元付款,金额应以美分指定。

带发票的收费

有时您可能需要进行一次性收费并为您的客户提供 PDF 发票。invoicePrice 方法可以实现这一点。例如,让我们为客户开具五件新衬衫的发票:

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

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

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

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

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

或者,您可以使用 invoiceFor 方法对客户的默认付款方式进行“一次性”收费:

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

虽然 invoiceFor 方法可供您使用,但建议您使用具有预定义价格的 invoicePricetabPrice 方法。通过这样做,您将能够在您的 Stripe 仪表板中获得关于您的每个产品销售的更好的分析和数据。

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

创建付款意向

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

use Illuminate\Http\Request;

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

    return $payment->client_secret;
});

创建付款意向后,您可以将客户端密钥返回给您的应用程序的前端,以便用户可以在他们的浏览器中完成付款。要了解更多关于使用 Stripe 付款意向构建整个付款流程的信息,请查阅Stripe 文档 (opens in a new tab)

当使用 pay 方法时,您的 Stripe 仪表板中启用的默认付款方式将可供客户使用。或者,如果您只想允许使用某些特定的付款方式,您可以使用 payWith 方法:

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 作为其第一个参数:

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

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

发票

检索发票

您可以使用 invoices 方法轻松检索计费模型的发票数组。invoices 方法返回一个 Laravel\Cashier\Invoice 实例的集合:

$invoices = $user->invoices();

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

$invoices = $user->invoicesIncludingPending();

您可以使用 findInvoice 方法通过其 ID 检索特定的发票:

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

显示发票信息

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

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

即将到来的发票

要检索客户的即将到来的发票,您可以使用 upcomingInvoice 方法:

$invoice = $user->upcomingInvoice();

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

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

预览订阅发票

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

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

您可以将价格数组传递给 previewInvoice 方法,以便预览具有多个新价格的发票:

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

生成发票 PDF

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

composer require dompdf/dompdf

在路由或控制器内部,您可以使用 downloadInvoice 方法生成给定发票的 PDF 下载。此方法将自动生成下载发票所需的正确 HTTP 响应:

use Illuminate\Http\Request;

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

默认情况下,发票上的所有数据都来自存储在 Stripe 中的客户和发票数据。文件名基于您的 app.name 配置值。但是,您可以通过将一个数组作为 downloadInvoice 方法的第二个参数来自定义其中的一些数据。此数组允许您自定义诸如您的公司和产品详细信息等信息:

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

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

自定义发票渲染器

Cashier 还可以使用自定义发票渲染器。默认情况下,Cashier 使用 DompdfInvoiceRenderer 实现,它利用[dom