Laravel Cashier Stripe 提供了一个富有表现力、流畅的接口,用于连接 Stripe 的 订阅计费服务。它处理了几乎所有你害怕编写的样板订阅计费代码。除了基本的订阅管理之外,Cashier 可以处理优惠券、交换订阅、订阅“数量”、取消宽限期,甚至可以生成发票 PDF。
当升级到新版本的 Cashier 时,务必仔细查阅 升级指南。
[!WARNING]
为防止破坏性更改,Cashier 使用固定的 Stripe API 版本。Cashier 16 使用 Stripe API 版本2025-06-30.basil。Stripe API 版本将在次要版本发布时更新,以便使用新的 Stripe 功能和改进。
首先,使用 Composer 包管理器安装适用于 Stripe 的 Cashier 包:
composer require laravel/cashier安装软件包后,使用 vendor:publish Artisan 命令发布 Cashier 的迁移:
php artisan vendor:publish --tag="cashier-migrations"然后,迁移您的数据库:
php artisan migrateCashier的迁移将向您的 users 表添加几个列。它们还将创建一个新的 subscriptions 表来保存您客户的所有订阅,以及一个 subscription_items 表用于具有多个价格的订阅。
如果您愿意,您也可以使用 vendor:publish Artisan 命令发布 Cashier 的配置文件:
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 提供了各种方法,以允许你执行常见的计费任务,例如创建订阅、应用优惠券,以及更新支付方式信息:
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;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Cashier::useCustomerModel(User::class);
}[!警告]
如果您使用的模型不是 Laravel 提供的App\Models\User模型,您需要发布并修改提供的 Cashier 迁移 以匹配您替代模型的表名。
接下来,你应该在你的应用程序的.env文件配置你的 Stripe API 密钥. 你可以从 Stripe 控制面板获取你的 Stripe API 密钥:
STRIPE_KEY=your-stripe-key
STRIPE_SECRET=your-stripe-secret
STRIPE_WEBHOOK_SECRET=your-stripe-webhook-secret[!警告]
您应该确保STRIPE_WEBHOOK_SECRET环境变量在您的应用程序的.env文件中定义,因为此变量用于确保传入的 webhook 确实来自 Stripe。
Cashier 的默认货币是美元 (USD)。 您可以通过设置 CASHIER_CURRENCY 环境变量在您的应用程序的 .env 文件中更改默认货币:
CASHIER_CURRENCY=eur除了配置 Cashier 的货币外,您还可以指定一个区域设置,用于格式化发票上显示的金额值。在内部,Cashier 利用 PHP 的 NumberFormatter 类 来设置货币区域设置:
CASHIER_CURRENCY_LOCALE=nl_BE[!WARNING]
为了使用en之外的语言环境,请确保您的服务器上已安装并配置了ext-intlPHP 扩展。
得益于 Stripe Tax,可以自动计算 Stripe 生成的所有发票的税款。您可以通过在应用程序的 App\Providers\AppServiceProvider 类的 boot 方法中调用 calculateTaxes 方法来启用自动税款计算:
use Laravel\Cashier\Cashier;
/**
* Bootstrap any application services.
*/
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;
/**
* Bootstrap any application services.
*/
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:
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 客户记录连接到您的应用程序数据库中对应的用户。完成结账会话后,客户将被重定向到一个专门的成功或取消页面,您可以在该页面向客户显示一条信息。
在销售产品时,通常会通过您自己的应用程序定义的 Cart 和 Order 模型来跟踪已完成的订单和已购买的产品。将客户重定向到 Stripe Checkout 以完成购买时,您可能需要提供一个现有订单标识符,以便在客户重定向回您的应用程序时,您可以将已完成的购买与相应的订单关联起来。
为此,您可以提供一个 metadata 数组给 checkout 方法。 让我们设想一下,当用户开始结账流程时,在我们的应用程序中会创建一个待处理的 Order。 请记住,本例中的 Cart 和 Order 模型仅作说明之用,并非由 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');如您在上方示例中看到的,当用户开始结账流程时,我们将提供购物车/订单所有相关的 Stripe 价格标识符给 checkout 方法。 当然,您的应用程序负责在客户添加商品时,将这些商品关联到“购物车”或订单。 我们还将订单 ID 通过 metadata 数组提供给 Stripe 结账会话。 最后,我们已将 CHECKOUT_SESSION_ID 模板变量添加到结账成功路由中。 当 Stripe 将客户重定向回您的应用程序时,这个模板变量将自动填充结账会话 ID。
接下来,让我们构建结账成功路由。这是用户通过 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');请参阅 Stripe 文档,了解有关 Checkout 会话对象所包含的数据 的更多信息.
[!NOTE]
在使用 Stripe Checkout 之前,你应该在你的 Stripe 控制面板中定义具有固定价格的产品。此外,你应该配置 Cashier 的 webhook 处理。
通过您的应用程序提供产品和订阅计费可能会令人望而生畏。然而,多亏了 Cashier 和 Stripe 结账,您可以轻松构建现代化、强大的支付集成。
为了了解如何使用 Cashier 和 Stripe Checkout 销售订阅,让我们考虑一个简单的订阅服务场景,该服务包含一个基本的按月 (price_basic_monthly) 和按年 (price_basic_yearly) 计划。这两个价格可以归入我们的 Stripe 控制面板中的“Basic”产品 (pro_basic) 下。此外,我们的订阅服务可能还会提供一个 Expert 计划,作为 pro_expert。
首先,让我们了解客户如何订阅我们的服务。当然,你可以想象客户可能会在我们的应用程序定价页面上点击Basic套餐的“订阅”按钮。此按钮或链接应将用户引导至一个Laravel路由,该路由为其所选套餐创建Stripe Checkout会话:
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 结账会话,这将允许他们订阅我们的 Basic 套餐。成功结账或取消后,客户将被重定向回我们提供给 checkout 方法的 URL。为了了解他们的订阅何时实际开始(因为某些支付方式需要几秒钟来处理),我们还需要配置 Cashier 的 webhook 处理。
既然客户可以开始订阅,我们需要限制我们应用程序的某些部分,以便只有已订阅用户才能访问它们。当然,我们始终可以通过由 Cashier 的 Billable trait 提供的 subscribed 方法来确定用户的当前订阅状态:
@if ($user->subscribed())
<p>You are subscribed.</p>
@endif我们甚至可以轻松地确定某个用户是否订阅了特定产品或价格:
@if ($user->subscribedToProduct('pro_basic'))
<p>You are subscribed to our Basic product.</p>
@endif
@if ($user->subscribedToPrice('price_basic_monthly'))
<p>You are subscribed to our monthly Basic plan.</p>
@endif为方便起见,您可能希望创建一个中间件,用于确定传入请求是否来自订阅用户。一旦此中间件被定义,您可以轻松地将其分配给路由,以阻止未订阅用户访问该路由:
<?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()) {
// Redirect user to billing page and ask them to subscribe...
return redirect('/billing');
}
return $next($request);
}
}一旦定义了中间件,您就可以将其分配给路由:
use App\Http\Middleware\Subscribed;
Route::get('/dashboard', function () {
// ...
})->middleware([Subscribed::class]);当然,客户可能希望将其订阅计划更改为另一个产品或“层级”。允许这样做最简单的方法是引导客户到 Stripe 的 客户账单门户,它提供了一个托管的用户界面,允许客户下载账单、更新付款方式和更改订阅计划。
首先,在你的应用中定义一个链接或按钮,将用户引导至一个 Laravel 路由,我们将利用该路由来启动一个账单门户会话:
<a href="{{ route('billing') }}">
Billing
</a>接下来,我们来定义用于启动 Stripe 客户账单门户会话并将用户重定向到门户的路由。redirectToBillingPortal 方法接受一个 URL,用户退出门户后将被返回到该 URL:
use Illuminate\Http\Request;
Route::get('/billing', function (Request $request) {
return $request->user()->redirectToBillingPortal(route('dashboard'));
})->middleware(['auth'])->name('billing');[!注意]
只要你配置了 Cashier 的 webhook 处理,Cashier 就会通过检查来自 Stripe 的传入 webhook,自动保持你的应用程序中与 Cashier 相关的数据库表同步。因此,例如,当用户通过 Stripe 的客户账单门户取消订阅时,Cashier 将收到相应的 webhook,并在你的应用程序的数据库中将该订阅标记为“已取消”。
您可以通过他们的 Stripe ID 使用 Cashier::findBillable 方法来检索客户。此方法将返回一个可计费模型的实例:
use Laravel\Cashier\Cashier;
$user = Cashier::findBillable($stripeId);偶尔,您可能希望创建一个 Stripe 客户,而不开始订阅。您可以使用 createAsStripeCustomer 方法来完成此操作:
$stripeCustomer = $user->createAsStripeCustomer();客户在 Stripe 中创建后,您可以在稍后开始订阅。您可以提供一个可选的 $options 数组,以传入任何额外的 Stripe API 支持的客户创建参数:
$stripeCustomer = $user->createAsStripeCustomer($options);您可以使用 asStripeCustomer 方法,如果您想返回可计费模型的 Stripe 客户对象:
$stripeCustomer = $user->asStripeCustomer();createOrGetStripeCustomer 方法可在您需要检索某个可计费模型的 Stripe 客户对象时使用,但您又不确定该可计费模型是否已是 Stripe 中的客户。如果 Stripe 中尚不存在客户,此方法将创建一个新客户:
$stripeCustomer = $user->createOrGetStripeCustomer();偶尔,您可能希望直接使用附加信息更新 Stripe 客户。您可以使用 updateStripeCustomer 方法完成此操作。此方法接受一个数组,其中包含 Stripe API 支持的客户更新选项:
$stripeCustomer = $user->updateStripeCustomer($options);Stripe 允许您贷记或借记客户的"余额". 稍后, 此余额将在新发票上贷记或借记. 要检查客户的总余额, 您可以使用可计费模型上提供的 balance 方法. balance 方法将以客户的货币返回余额的格式化字符串表示形式:
$balance = $user->balance();要贷记客户的余额,您可以为 creditBalance 方法提供一个值。如果您愿意,您也可以提供一个描述:
$user->creditBalance(500, 'Premium customer top-up.');向 debitBalance 方法提供一个值将借记客户的余额:
$user->debitBalance(300, 'Bad usage penalty.');applyBalance 方法将为客户创建新的客户余额交易。您可以使用 balanceTransactions 方法检索这些交易记录,这可能有助于为客户提供一份贷记和借记日志供其查阅:
// Retrieve all transactions...
$transactions = $user->balanceTransactions();
foreach ($transactions as $transaction) {
// Transaction amount...
$amount = $transaction->amount(); // $2.31
// Retrieve the related invoice when available...
$invoice = $transaction->invoice();
}Cashier 提供一种简便的方法来管理客户的税务ID。例如,taxIds 方法可用于检索分配给客户的所有 税务ID 作为一个集合:
$taxIds = $user->taxIds();您也可以通过其标识符获取客户的特定税务ID:
$taxId = $user->findTaxId('txi_belgium');您可以通过提供一个有效的 type 和值给 createTaxId 方法来创建一个新的税务 ID:
$taxId = $user->createTaxId('eu_vat', 'BE0123456789');createTaxId 方法会将增值税 (VAT) ID 立即添加到客户账户。 增值税 (VAT) ID 的验证也由 Stripe 完成;然而,这是一个异步过程。 您可以通过订阅 customer.tax_id.updated webhook 事件并检查 增值税 (VAT) ID 的 verification 参数来获取验证更新通知。 有关处理 Webhook 的更多信息,请查阅 关于定义 Webhook 处理程序的文档。
您可以使用 deleteTaxId 方法删除税务 ID:
$user->deleteTaxId('txi_belgium');通常情况下,当您的应用程序用户更新其姓名、电子邮件地址或其他 Stripe 也存储的信息时,您应该通知 Stripe 这些更新。这样做,Stripe 的信息副本将与您的应用程序中的信息保持同步。
为了实现自动化,您可以在您的可计费模型上定义一个事件监听器,该监听器会响应模型的 updated 事件。然后,在您的事件监听器中,您可以在该模型上调用 syncStripeCustomerDetails 方法:
use App\Models\User;
use function Illuminate\Events\queueable;
/**
* The "booted" method of the model.
*/
protected static function booted(): void
{
static::updated(queueable(function (User $customer) {
if ($customer->hasStripeId()) {
$customer->syncStripeCustomerDetails();
}
}));
}现在,每当您的客户模型更新时,其信息将与 Stripe 同步。为方便起见,Cashier 将在客户初次创建时自动将您的客户信息与 Stripe 同步。
您可以通过覆盖 Cashier 提供的各种方法,自定义用于将客户信息同步到 Stripe 的列。例如,您可以覆盖 stripeName 方法,以自定义当 Cashier 将客户信息同步到 Stripe 时,应被视为客户“名称”的属性:
/**
* Get the customer name that should be synced to Stripe.
*/
public function stripeName(): string|null
{
return $this->company_name;
}类似地,您可以重写 stripeEmail,stripePhone (最多20个字符),stripeAddress,以及 stripePreferredLocales 方法。这些方法会在更新Stripe客户对象时,将信息同步到它们对应的客户参数。如果您希望完全控制客户信息同步过程,您可以重写 syncStripeCustomerDetails 方法。
Stripe 提供 一种简单的方式来设置一个账单门户 以便您的客户可以管理他们的订阅、支付方式以及查看他们的账单历史记录。 您可以通过从控制器或路由调用计费模型上的 redirectToBillingPortal 方法,将您的用户重定向到账单门户:
use Illuminate\Http\Request;
Route::get('/billing-portal', function (Request $request) {
return $request->user()->redirectToBillingPortal();
});默认情况下,当用户完成订阅管理后,他们将能够通过 Stripe 账单门户中的链接返回到你的应用程序的home路由。你可以通过将 URL 作为参数传递给redirectToBillingPortal方法来提供一个自定义的返回 URL:
use Illuminate\Http\Request;
Route::get('/billing-portal', function (Request $request) {
return $request->user()->redirectToBillingPortal(route('billing'));
});如果您想生成指向账单门户的 URL,而不生成 HTTP 重定向响应,您可以调用 billingPortalUrl 方法:
$url = $request->user()->billingPortalUrl(route('billing'));为了使用 Stripe 创建订阅或执行“一次性”收费,您将需要存储一种支付方式并从 Stripe 检索其标识符。实现此目的的方法因您是否计划将该支付方式用于订阅或单次收费而异,因此我们将在下面探讨这两种情况。
当存储客户的信用卡信息供订阅将来使用时,必须使用 Stripe 的 "Setup Intents" API 安全地收集客户的支付方式详情。一个 "Setup Intent" 向 Stripe 表明对客户支付方式进行收费的意图。Cashier 的 Billable trait 包含 createSetupIntent 方法,以方便地创建一个新的 Setup Intent。您应该从将渲染收集客户支付方式详情的表单的路由或控制器中调用此方法:
return view('update-payment-method', [
'intent' => $user->createSetupIntent()
]);在您创建了 Setup Intent 并将其传递给视图后,您应该将其密钥附加到将收集支付方式的元素上。例如,考虑以下“更新支付方式”表单:
<input id="card-holder-name" type="text">
<!-- Stripe Elements Placeholder -->
<div id="card-element"></div>
<button id="card-button" data-secret="{{ $intent->client_secret }}">
Update Payment Method
</button>接下来,可以使用 Stripe.js 库将 Stripe Element 附加到表单并安全地收集客户的支付详情:
<script src="https://js.stripe.com/v3/"></script>
<script>
const stripe = Stripe('stripe-public-key');
const elements = stripe.elements();
const cardElement = elements.create('card');
cardElement.mount('#card-element');
</script>接下来,该卡可以被验证并可使用Stripe的confirmCardSetup方法从Stripe获取一个安全的“支付方式标识符”:
const cardHolderName = document.getElementById('card-holder-name');
const cardButton = document.getElementById('card-button');
const clientSecret = cardButton.dataset.secret;
cardButton.addEventListener('click', async (e) => {
const { setupIntent, error } = await stripe.confirmCardSetup(
clientSecret, {
payment_method: {
card: cardElement,
billing_details: { name: cardHolderName.value }
}
}
);
if (error) {
// Display "error.message" to the user...
} else {
// The card has been verified successfully...
}
});在卡片经过 Stripe 验证后,你可以将生成的 setupIntent.payment_method 标识符传递给你的 Laravel 应用程序,在那里它可以附加到客户。该支付方式可以 添加为新的支付方式 或 用于更新默认支付方式。你也可以立即使用该支付方式标识符来 创建新的订阅。
[!注意]
如果您想了解更多关于 Setup Intent 以及收集客户付款详情的信息,请查阅 Stripe 提供的此概述。
当然,当对客户的支付方式进行单次扣款时,我们只需要使用一次支付方式标识符。由于Stripe的限制,您不能使用客户存储的默认支付方式进行单次扣款。您必须允许客户使用Stripe.js库输入其支付方式详情。例如,请考虑以下表单:
<input id="card-holder-name" type="text">
<!-- Stripe Elements Placeholder -->
<div id="card-element"></div>
<button id="card-button">
Process Payment
</button>在定义了这样一个表单之后,Stripe.js 库可用于将一个Stripe Element附加到该表单,并安全地收集客户的支付详情:
<script src="https://js.stripe.com/v3/"></script>
<script>
const stripe = Stripe('stripe-public-key');
const elements = stripe.elements();
const cardElement = elements.create('card');
cardElement.mount('#card-element');
</script>接下来,卡片可以被验证,并且可以使用Stripe 的 createPaymentMethod 方法从 Stripe 检索到一个安全的“支付方式标识符”:
const cardHolderName = document.getElementById('card-holder-name');
const cardButton = document.getElementById('card-button');
cardButton.addEventListener('click', async (e) => {
const { paymentMethod, error } = await stripe.createPaymentMethod(
'card', cardElement, {
billing_details: { name: cardHolderName.value }
}
);
if (error) {
// Display "error.message" to the user...
} else {
// The card has been verified successfully...
}
});如果卡验证成功,您可以将 paymentMethod.id 传递到您的 Laravel 应用程序并处理一笔 单次扣款。
可计费模型实例上的paymentMethods方法返回一个Laravel\Cashier\PaymentMethod实例的集合:
$paymentMethods = $user->paymentMethods();默认情况下,此方法将返回所有类型的支付方式。若要检索特定类型的支付方式,您可以将 type 作为参数传入该方法:
$paymentMethods = $user->paymentMethods('sepa_debit');要检索客户的默认支付方式,可以使用 defaultPaymentMethod 方法:
$paymentMethod = $user->defaultPaymentMethod();您可以使用 findPaymentMethod 方法检索附加到可计费模型上的特定支付方式:
$paymentMethod = $user->findPaymentMethod($paymentMethodId);要确定可计费模型是否已将其默认支付方式附加到其账户,调用 hasDefaultPaymentMethod 方法:
if ($user->hasDefaultPaymentMethod()) {
// ...
}您可以使用 hasPaymentMethod 方法来确定可计费模型是否至少有一种支付方式绑定到其账户:
if ($user->hasPaymentMethod()) {
// ...
}此方法将确定可计费模型是否拥有任何支付方式。要确定模型是否存在特定类型的支付方式,您可以将 type 作为参数传递给此方法:
if ($user->hasPaymentMethod('sepa_debit')) {
// ...
}updateDefaultPaymentMethod 方法可用于更新客户的默认支付方式信息。此方法接受一个 Stripe 支付方式标识符,并将新的支付方式指定为默认账单支付方式:
$user->updateDefaultPaymentMethod($paymentMethod);为了将您的默认支付方式信息与 Stripe 中客户的默认支付方式信息同步,您可以使用 updateDefaultPaymentMethodFromStripe 方法:
$user->updateDefaultPaymentMethodFromStripe();[!WARNING]
客户的默认支付方式只能用于开具发票和创建新的订阅。 由于Stripe施加的限制,它不能用于单次收费。
要添加新的支付方式,您可以在可计费模型上调用 addPaymentMethod 方法,并传入支付方式标识符:
$user->addPaymentMethod($paymentMethod);[!NOTE]
要了解如何检索支付方式标识符,请查阅支付方式存储文档。
要删除支付方式,您可以调用您希望删除的 Laravel\Cashier\PaymentMethod 实例上的 delete 方法:
$paymentMethod->delete();该 deletePaymentMethod 方法将从可计费模型中删除一个特定的支付方式:
$user->deletePaymentMethod('pm_visa');该 deletePaymentMethods 方法将删除计费模型的所有支付方式信息:
$user->deletePaymentMethods();默认情况下,此方法将删除所有类型的支付方式。要删除特定类型的支付方式,您可以将 type 作为参数传递给该方法:
$user->deletePaymentMethods('sepa_debit');[!WARNING]
如果用户拥有有效的订阅,您的应用程序不应允许他们删除其默认支付方式。
订阅提供了一种为您的客户设置循环支付的方式。由 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 方法的第一个参数应该是订阅的内部类型. 如果你的应用程序只提供一个订阅,你可能会将其称为 default 或 primary. 此订阅类型仅供内部应用程序使用,不应向用户显示. 此外,它不应包含空格,并且在创建订阅后不应更改. 第二个参数是用户订阅的具体价格. 此值应与 Stripe 中价格的标识符对应.
create 方法,接受 Stripe 支付方式标识符 或 Stripe PaymentMethod 对象,将开始订阅并使用可计费模型的 Stripe 客户 ID 和其他相关的账单信息来更新您的数据库。
[!WARNING]
将支付方式标识符直接传递给create订阅方法也将自动将其添加到用户的已存储支付方式中。
您可以不自动收取客户的循环付款,而是指示 Stripe 在每次循环付款到期时通过电子邮件向客户发送发票. 然后,客户可以在收到发票后手动支付该发票. 客户在通过发票收取循环付款时,无需预先提供付款方式:
$user->newSubscription('default', 'price_monthly')->createAndSendInvoice();客户在订阅被取消前支付发票的期限由 days_until_due 选项决定。默认情况下,这个期限是 30 天;但是,如果您愿意,可以为该选项提供一个具体值:
$user->newSubscription('default', 'price_monthly')->createAndSendInvoice([], [
'days_until_due' => 30
]);如果您希望在创建订阅时为价格设置特定的数量,您应该在创建订阅之前,在订阅构建器上调用 quantity 方法:
$user->newSubscription('default', 'price_monthly')
->quantity(5)
->create($paymentMethod);如果您想指定额外的 客户 或 订阅 Stripe 支持的选项,您可以通过将它们作为第二个和第三个参数传递给 create 方法:
$user->newSubscription('default', 'price_monthly')->create($paymentMethod, [
'email' => $email,
], [
'metadata' => ['note' => 'Some extra information.'],
]);如果您想在创建订阅时应用优惠券,您可以使用 withCoupon 方法:
$user->newSubscription('default', 'price_monthly')
->withCoupon('code')
->create($paymentMethod);或者,如果您想应用一个 Stripe 促销代码,您可以使用 withPromotionCode 方法:
$user->newSubscription('default', 'price_monthly')
->withPromotionCode('promo_code_id')
->create($paymentMethod);给定的促销码 ID 应该是分配给该促销码的 Stripe API ID,而不是面向客户的促销码。如果您需要根据一个给定的面向客户的促销码查找促销码 ID,您可以使用 findPromotionCode 方法:
// Find a promotion code ID by its customer facing code...
$promotionCode = $user->findPromotionCode('SUMMERSALE');
// Find an active promotion code ID by its customer facing code...
$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();如果您想对客户或订阅应用新的优惠券或促销代码,您可以通过 applyCoupon 或 applyPromotionCode 方法来完成:
$billable->applyCoupon('coupon_id');
$billable->applyPromotionCode('promotion_code_id');
$subscription->applyCoupon('coupon_id');
$subscription->applyPromotionCode('promotion_code_id');请记住,您应该使用分配给推广码的 Stripe API ID,而不是面向客户的推广码。一个客户或订阅在任何给定时间只能应用一张优惠券或一个推广码。
有关此主题的更多信息,请查阅 Stripe 关于 优惠券 和 促销代码 的文档。
如果您想为已拥有默认支付方式的客户添加订阅,您可以在订阅构建器上调用 add 方法:
use App\Models\User;
$user = User::find(1);
$user->newSubscription('default', 'price_monthly')->add();您也可以直接在 Stripe 控制面板中创建订阅。当您这样做时,Cashier 将同步新添加的订阅并为其分配 default 类型。要自定义分配给在控制面板中创建的订阅的订阅类型,请定义 webhook 事件处理器。
此外,您只能通过 Stripe 管理面板创建一种订阅类型。如果您的应用程序提供多种使用不同类型的订阅,则只能通过 Stripe 管理面板添加一种订阅类型。
最后,您应始终确保针对您的应用程序提供的每种订阅类型只添加一个有效订阅。如果客户有两个 default 订阅,只有最近添加的订阅才会被 Cashier 使用,即使两者都会同步到您的应用程序的数据库中。
一旦客户订阅了您的应用程序,您可以使用多种便捷方法轻松检查他们的订阅状态。首先,如果客户拥有活跃订阅(即使订阅目前处于试用期内),subscribed 方法会返回 true。subscribed 方法接受订阅类型作为其第一个参数:
if ($user->subscribed('default')) {
// ...
}subscribed 方法也是 路由中间件 的一个绝佳选择,它允许您根据用户的订阅状态过滤对路由和控制器的访问:
<?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')) {
// This user is not a paying customer...
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 订阅是否正在积极订阅应用程序的“基础版”或“高级版”产品:
if ($user->subscribedToProduct(['prod_basic', 'prod_premium'], 'default')) {
// ...
}subscribedToPrice 方法可用于确定客户的订阅是否与给定的价格 ID 对应:
if ($user->subscribedToPrice('price_basic_monthly', 'default')) {
// ...
}recurring 方法可用于确定用户当前是否已订阅且已超出试用期:
if ($user->subscription('default')->recurring()) {
// ...
}[!WARNING]
如果用户有两个同类型的订阅,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) }}">
Please confirm your payment.
</a>如果您希望订阅在处于 past_due 或 incomplete 状态时仍被视为活动状态,您可以使用 Cashier 提供的 keepPastDueSubscriptionsActive 和 keepIncompleteSubscriptionsActive 方法。通常,这些方法应在您的 App\Providers\AppServiceProvider 的 register 方法中调用:
use Laravel\Cashier\Cashier;
/**
* Register any application services.
*/
public function register(): void
{
Cashier::keepPastDueSubscriptionsActive();
Cashier::keepIncompleteSubscriptionsActive();
}[!WARNING]
当订阅处于incomplete状态时,在付款确认之前无法更改。因此,当订阅处于incomplete状态时,swap和updateQuantity方法将抛出异常。
大多数订阅状态也可用作查询作用域,以便您可以轻松查询您的数据库以查找处于给定状态的订阅:
// Get all active subscriptions...
$subscriptions = Subscription::query()->active()->get();
// Get all of the canceled subscriptions for a user...
$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 文档。
[!WARNING]
在swapAndInvoice方法之前执行noProrate方法将对按比例分配无效。始终会开具发票。
有时订阅会受到“数量”的影响。例如,项目管理应用程序可能按每个项目每月10美元收费。您可以使用 incrementQuantity 和 decrementQuantity 方法轻松地增加或减少您的订阅数量:
use App\Models\User;
$user = User::find(1);
$user->subscription('default')->incrementQuantity();
// Add five to the subscription's current quantity...
$user->subscription('default')->incrementQuantity(5);
$user->subscription('default')->decrementQuantity();
// Subtract five from the subscription's current quantity...
$user->subscription('default')->decrementQuantity(5);或者,您可以使用 updateQuantity 方法设置一个具体的数量:
$user->subscription('default')->updateQuantity(10);noProrate 方法可用于更新订阅数量,而无需按比例分摊费用:
$user->subscription('default')->noProrate()->updateQuantity(10);有关订阅数量的更多信息,请查阅 Stripe 文档。
如果您的订阅是多产品订阅,您应该将您希望增减其数量的价格的ID作为第二个参数传递给增量/减量方法:
$user->subscription('default')->incrementQuantity(1, 'price_chat');多产品订阅允许您将多个计费产品分配到一个订阅。例如,假设您正在构建一个客户服务“帮助台”应用程序,它有一个每月10美元的基本订阅价格,但提供一个每月额外15美元的实时聊天附加产品。多产品订阅的信息存储在 Cashier的 subscription_items 数据库表中。
您可以为给定订阅指定多个产品 借由将价格数组作为第二个参数传入 newSubscription 方法:
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$request->user()->newSubscription('default', [
'price_monthly',
'price_chat',
])->create($request->paymentMethodId);
// ...
});在上面的例子中,客户的 default 订阅将关联两个价格。这两个价格将分别按照各自的计费周期收费。如有必要,您可以使用 quantity 方法为每个价格指定一个具体数量:
$user = User::find(1);
$user->newSubscription('default', ['price_monthly', 'price_chat'])
->quantity(5, 'price_chat')
->create($paymentMethod);如果您想为现有订阅添加另一个价格,您可以调用该订阅的 addPrice 方法:
$user = User::find(1);
$user->subscription('default')->addPrice('price_chat');上述示例将添加新价格,客户将在其下个计费周期内为此付费。如果您希望立即向客户收费,您可以使用 addPriceAndInvoice 方法:
$user->subscription('default')->addPriceAndInvoice('price_chat');如果您想添加一个带特定数量的价格,您可以将数量作为第二个参数传递给 addPrice 或 addPriceAndInvoice 方法:
$user = User::find(1);
$user->subscription('default')->addPrice('price_chat', 5);您可以使用 removePrice 方法从订阅中移除价格:
$user->subscription('default')->removePrice('price_chat');[!WARNING]
您不能删除订阅中的最后一个价格。相反,您应该直接取消订阅。
You may also change the prices attached to a subscription with multiple products. For example, imagine a customer has a price_basic subscription with a price_chat add-on product and you want to upgrade the customer from the price_basic to the price_pro price:
use App\Models\User;
$user = User::find(1);
$user->subscription('default')->swap(['price_pro', 'price_chat']);执行上述示例时,带有 price_basic 的底层订阅项将被删除,而带有 price_chat 的订阅项将被保留。此外,将创建一个新的 price_pro 订阅项。
您还可以通过向 swap 方法传递一个键/值对数组来指定订阅项选项。例如,您可能需要指定订阅价格数量:
$user = User::find(1);
$user->subscription('default')->swap([
'price_pro' => ['quantity' => 5],
'price_chat'
]);如果您想在订阅上更换单个价格,您可以使用订阅项目本身的 swap 方法进行操作。这种方法特别有用,如果您希望保留订阅中其他价格的所有现有元数据:
$user = User::find(1);
$user->subscription('default')
->findItemOrFail('price_basic')
->swap('price_pro');默认情况下,Stripe 在包含多个产品的订阅中添加或删除价格时,会按比例计算费用。如果您希望进行价格调整而不按比例计算,您应该将 noProrate 方法链式调用到您的价格操作中:
$user->subscription('default')->noProrate()->removePrice('price_chat');如果您想更新单个订阅价格的数量,您可以使用现有数量方法通过将价格的ID作为附加参数传递给该方法来完成:
$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]
当订阅有多个价格时,stripe_price和quantity属性在Subscription模型上将为null。要访问单个价格属性,您应该使用Subscription模型上可用的items关系。
当一个订阅有多个价格时,它将在您的数据库的 subscription_items 表中存储多个订阅“项目”。您可以通过订阅上的 items 关系访问这些项目:
use App\Models\User;
$user = User::find(1);
$subscriptionItem = $user->subscription('default')->items->first();
// Retrieve the Stripe price and quantity for a specific item...
$stripePrice = $subscriptionItem->stripe_price;
$quantity = $subscriptionItem->quantity;您还可以使用 findItemOrFail 方法检索特定价格:
$user = User::find(1);
$subscriptionItem = $user->subscription('default')->findItemOrFail('price_chat');Stripe 允许您的客户同时拥有多个订阅。例如,您可能经营一家健身房,提供游泳订阅和举重订阅,并且每个订阅可能有不同的定价。当然,客户应该能够订阅其中一个或两个套餐。
当您的应用程序创建订阅时,您可以将订阅的类型提供给 newSubscription 方法。该类型可以是任何表示用户正在发起的订阅类型的字符串:
use Illuminate\Http\Request;
Route::post('/swimming/subscribe', function (Request $request) {
$request->user()->newSubscription('swimming')
->price('price_swimming_monthly')
->create($request->paymentMethodId);
// ...
});在此示例中,我们为客户开通了按月计费的游泳订阅服务。但是,他们可能希望在稍后时间切换到按年计费的订阅服务。在调整客户的订阅服务时,我们只需替换 swimming 订阅服务的价格即可:
$user->subscription('swimming')->swap('price_swimming_yearly');当然,您也可以完全取消订阅:
$user->subscription('swimming')->cancel();按使用量计费 允许您根据客户在一个计费周期内的产品使用量进行收费。例如,您可以根据他们每月发送的短信或电子邮件的数量向客户收费。
若要开始使用按用量计费,您首先需要在 Stripe 管理平台中创建一个新产品,该产品需包含一个 基于用量的计费模型 和一个 计量器。创建计量器后,存储相关的事件名称和计量器 ID,您将需要用它们来报告和检索用量。然后,使用 meteredPrice 方法将计量价格 ID 添加到客户订阅中:
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$request->user()->newSubscription('default')
->meteredPrice('price_metered')
->create($request->paymentMethodId);
// ...
});您也可以通过 Stripe 结账 启动按量计费订阅:
$checkout = Auth::user()
->newSubscription('default', [])
->meteredPrice('price_metered')
->checkout();
return view('your-checkout-view', [
'checkout' => $checkout,
]);随着您的客户使用您的应用程序,您将向 Stripe 报告他们的使用情况,以便对他们进行准确计费。要报告计量事件的使用情况,您可以使用您的 Billable 模型上的 reportMeterEvent 方法:
$user = User::find(1);
$user->reportMeterEvent('emails-sent');默认情况下,一个“使用量”为 1 的数值会被添加到计费周期。或者,您可以传入一个特定数量的“使用量”,以添加到客户在该计费周期的使用量中:
$user = User::find(1);
$user->reportMeterEvent('emails-sent', quantity: 15);要检索客户某仪表的事件摘要,您可以使用 Billable 实例的 meterEventSummaries 方法:
$user = User::find(1);
$meterUsage = $user->meterEventSummaries($meterId);
$meterUsage->first()->aggregated_value // 10请参考 Stripe 的 计量事件摘要对象文档 以获取有关计量事件摘要的更多信息。
要 列出所有计量器,您可以使用 Billable 实例的 meters 方法:
$user = User::find(1);
$user->meters();[!警告]
与其手动计算税率,您可以使用 Stripe Tax 自动计算税费
要指定用户在订阅上支付的税率,您应该在您的可计费模型上实现 taxRates 方法,并返回一个包含 Stripe 税率 ID 的数组。您可以在 您的 Stripe 控制面板 中定义这些税率:
/**
* The tax rates that should apply to the customer's subscriptions.
*
* @return array<int, string>
*/
public function taxRates(): array
{
return ['txr_id'];
}taxRates 方法使您能够按客户个体应用税率,这对于一个横跨多个国家和税率的用户群可能很有帮助。
如果你提供包含多个产品的订阅服务,你可以通过在你的可计费模型上实现 priceTaxRates 方法,为每个价格定义不同的税率:
/**
* The tax rates that should apply to the customer's subscriptions.
*
* @return array<string, array<int, string>>
*/
public function priceTaxRates(): array
{
return [
'price_monthly' => ['txr_id'],
];
}[!警告]
taxRates方法仅适用于订阅费用。如果您使用 Cashier 进行“一次性”收费,则需要手动指定当时的税率。
当更改由 taxRates 方法返回的硬编码税率 ID 时,用户任何现有订阅的税收设置将保持不变。如果您希望使用新的 taxRates 值更新现有订阅的税值,您应该在用户的订阅实例上调用 syncTaxRates 方法:
$user->subscription('default')->syncTaxRates();这还将同步多产品订阅的任何项目税率。如果您的应用程序提供包含多个产品的订阅,您应确保您的可计费模型实现了 priceTaxRates 方法 如上所述。
Cashier 还提供 isNotTaxExempt、isTaxExempt 和 reverseChargeApplies 方法来判断客户是否免税。这些方法将调用 Stripe API 来确定客户的免税状态:
use App\Models\User;
$user = User::find(1);
$user->isTaxExempt();
$user->isNotTaxExempt();
$user->reverseChargeApplies();[!警告]
这些方法也适用于任何Laravel\Cashier\Invoice对象。但是,当在Invoice对象上调用时,这些方法将根据发票创建时的情况确定豁免状态。
默认情况下,账单周期锚点是订阅创建的日期,或者,如果使用了试用期,是试用期结束的日期。如果你想修改账单锚点日期,你可以使用 anchorBillingCycleOn 方法:
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 方法:
$user->subscription('default')->cancel();当订阅被取消时,Cashier 会自动设置你 subscriptions 数据库表中的 ends_at 字段。这个字段用于确定 subscribed 方法何时应该开始返回 false。
例如,如果客户在3月1日取消订阅,但订阅原定于3月5日才结束,则 subscribed 方法将继续返回 true 直到3月5日。这样做是因为用户通常被允许继续使用应用程序直到他们的账单周期结束。
您可以判断用户是否已取消订阅但仍处于“宽限期”内使用 onGracePeriod 方法:
if ($user->subscription('default')->onGracePeriod()) {
// ...
}如果您希望立即取消订阅,请在用户的订阅上调用 cancelNow 方法:
$user->subscription('default')->cancelNow();如果您希望立即取消订阅并开具剩余未开票的计量使用费发票或新的/待处理的按比例分摊发票项目,请调用用户订阅上的 cancelNowAndInvoice 方法:
$user->subscription('default')->cancelNowAndInvoice();您也可以选择在特定时间点取消订阅:
$user->subscription('default')->cancelAt(
now()->addDays(10)
);最后,您应始终在删除关联用户模型之前取消用户订阅:
$user->subscription('default')->cancelNow();
$user->delete();如果客户已取消其订阅并且您希望恢复它,您可以对该订阅调用 resume 方法。客户必须仍在其“宽限期”内才能恢复订阅:
$user->subscription('default')->resume();如果客户取消订阅,然后在订阅完全到期之前恢复该订阅,客户将不会立即被计费。相反,他们的订阅将被重新激活,并且他们将在原始计费周期内被计费。
如果您希望向客户提供试用期,同时仍然预先收集支付方式信息,您应该在创建订阅时使用 trialDays 方法:
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 中为价格配置的任何默认试用期。
[!警告]
如果客户的订阅在试用期结束日期之前未取消,那么试用期一到期,他们就会被收费,因此,您务必通知您的用户他们的试用期结束日期。
该trialUntil方法允许您提供一个DateTime实例,该实例指定试用期何时结束:
use Illuminate\Support\Carbon;
$user->newSubscription('default', 'price_monthly')
->trialUntil(Carbon::now()->addDays(10))
->create($paymentMethod);你可以通过用户实例的 onTrial 方法或订阅实例的 onTrial 方法来判断用户是否处于试用期。下面两个示例是等效的:
if ($user->onTrial('default')) {
// ...
}
if ($user->subscription('default')->onTrial()) {
// ...
}您可以使用 endTrial 方法立即结束订阅试用:
$user->subscription('default')->endTrial();要确定现有试用期是否已过期,您可以使用 hasExpiredTrial 方法:
if ($user->hasExpiredTrial('default')) {
// ...
}
if ($user->subscription('default')->hasExpiredTrial()) {
// ...
}您可以在 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 将这种类型的试用期称为“通用试用期”,因为它不附属于任何现有订阅。可计费模型实例上的 onTrial 方法将返回 true,如果当前日期未超过 trial_ends_at 的值:
if ($user->onTrial()) {
// User is within their trial period...
}一旦您准备好为用户创建实际订阅,您就可以照常使用 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()) {
// User is within their "generic" trial period...
}extendTrial 方法允许您在订阅创建后延长订阅的试用期。如果试用期已过期,并且客户已经开始为订阅付费,您仍然可以为他们提供延长的试用期。在试用期内花费的时间将从客户的下一张发票中扣除:
use App\Models\User;
$subscription = User::find(1)->subscription('default');
// End the trial 7 days from now...
$subscription->extendTrial(
now()->addDays(7)
);
// Add an additional 5 days to the trial...
$subscription->extendTrial(
$subscription->trial_ends_at->addDays(5)
);[!注意]
您可以使用 Stripe CLI 来帮助在本地开发期间测试 Webhook。
Stripe 可以通过 Webhook 通知您的应用程序各种事件。默认情况下,Cashier 服务提供者会自动注册一个指向 Cashier 的 Webhook 控制器的路由。此控制器将处理所有传入的 Webhook 请求。
默认情况下,Cashier webhook 控制器将自动处理取消扣款失败次数过多(由您的 Stripe 设置定义)的订阅、客户更新、客户删除、订阅更新和支付方式变更;但是,正如我们很快就会发现的,您可以扩展此控制器以处理您喜欢的任何 Stripe webhook 事件。
为确保您的应用程序能够处理 Stripe webhook,请务必在 Stripe 控制面板中配置 webhook URL。默认情况下,Cashier 的 webhook 控制器响应 /stripe/webhook URL 路径。您应在 Stripe 控制面板中启用的所有 webhook 的完整列表是:
客户.订阅.创建客户.订阅.更新客户.订阅.已删除客户.已更新客户删除支付方式.自动更新发票.需进行支付操作发票支付成功为方便起见,Cashier 包含一个 cashier:webhook Artisan 命令。此命令将在 Stripe 中创建一个 webhook,该 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]
请确保您保护传入的 Stripe webhook 请求 通过 Cashier 附带的webhook 签名验证中间件。
由于 Stripe webhook 需要绕过 Laravel 的 CSRF 保护,你应该确保 Laravel 不会尝试验证传入的 Stripe webhook 的 CSRF 令牌。为此,你应该排除 stripe/* 从 CSRF 保护 在你的应用程序的 bootstrap/app.php 文件中:
->withMiddleware(function (Middleware $middleware): void {
$middleware->validateCsrfTokens(except: [
'stripe/*',
]);
})Cashier 会自动处理因支付失败导致的订阅取消以及其他常见的 Stripe webhook 事件。 但是,如果您有其他想要处理的 webhook 事件,您可以通过监听 Cashier 分发的以下事件来实现:
Laravel\Cashier\事件\Webhook已接收Laravel\Cashier\Events\WebhookHandled这两个事件都包含 Stripe webhook 的完整有效负载。例如,如果你希望处理 invoice.payment_succeeded webhook,你可以注册一个 监听器 来处理该事件:
<?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') {
// Handle the incoming event...
}
}
}为了保护您的 Webhook,您可以使用 Stripe 的 Webhook 签名。为方便起见,Cashier 自动包含一个中间件,用于验证传入的 Stripe Webhook 请求是否有效。
要启用 webhook 验证,请确保 STRIPE_WEBHOOK_SECRET 环境变量已设置在您的应用程序的 .env 文件中。此 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 文档 中找到:
$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 方法可供您使用,但建议您使用带有预定义价格的 invoicePrice 和 tabPrice 方法。通过这样做,您将能够在 Stripe 控制面板中获取关于每个产品销售情况的更好分析和数据。
`
您可以通过在一个可计费模型实例上调用 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 文档。
当使用 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]
pay和payWith方法接受以您的应用程序所使用的货币的最小单位表示的支付金额。例如,如果客户以美元支付,金额应以美分指定。
如果您需要退还 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文件之前,你应该使用 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 实现,它利用 dompdf PHP 库来生成 Cashier 的发票。但是,你可以使用任何你想要的渲染器通过实现 Laravel\Cashier\Contracts\InvoiceRenderer 接口。例如,你可能希望通过调用第三方 PDF 渲染服务的 API 来渲染发票 PDF:
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 配置值。此配置值应该设置为您的自定义渲染器实现的类名。
Cashier Stripe 也支持 Stripe Checkout。Stripe Checkout 消除了实现自定义收款页面的麻烦,通过提供一个预构建的、托管的支付页面。
以下文档包含有关如何开始将 Stripe Checkout 与 Cashier 配合使用的信息。要了解有关 Stripe Checkout 的更多信息,您还应考虑查阅 Stripe 自己的 Checkout 文档。
您可以在可计费模型上使用 checkout 方法,为已在您的 Stripe 控制面板中创建的现有产品执行结账。checkout 方法将会发起一个新的 Stripe 结账会话。默认情况下,您需要传递一个 Stripe 价格 ID:
use Illuminate\Http\Request;
Route::get('/product-checkout', function (Request $request) {
return $request->user()->checkout('price_tshirt');
});如果需要,您还可以指定产品数量:
use Illuminate\Http\Request;
Route::get('/product-checkout', function (Request $request) {
return $request->user()->checkout(['price_tshirt' => 15]);
});当客户访问此路由时,他们将被重定向到 Stripe 的结账页面。默认情况下,当用户成功完成或取消购买时,他们将被重定向到您的 home 路由位置,但您可以使用 success_url 和 cancel_url 选项指定自定义回调 URL:
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 替换此占位符:
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 不允许[用户可兑换的促销代码](https://stripe.com/docs/billing/subscriptions/discounts/codes)。幸运的是,有一种简单的方法可以为您的Checkout页面启用这些功能。为此,您可以调用 allowPromotionCodes 方法:
use Illuminate\Http\Request;
Route::get('/product-checkout', function (Request $request) {
return $request->user()
->allowPromotionCodes()
->checkout('price_tshirt');
});您还可以针对一个尚未在您的 Stripe 控制面板中创建的临时产品执行一次简单的收费。为此,您可以在一个可计费模型上使用 checkoutCharge 方法,并向其传递一个可计费金额、一个产品名称和一个可选的数量。当客户访问此路由时,他们将被重定向到 Stripe 的结账页面:
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.createdWeb 钩子。此 Web 钩子将在您的数据库中创建订阅记录,并存储所有相关的订阅项。
您也可以使用 Stripe Checkout 来发起订阅。在通过 Cashier 的订阅构建器方法定义您的订阅后,您可以调用 checkout 方法。当客户访问此路由时,他们将被重定向到 Stripe 的结账页面:
use Illuminate\Http\Request;
Route::get('/subscription-checkout', function (Request $request) {
return $request->user()
->newSubscription('default', 'price_monthly')
->checkout();
});与产品结账一样,您可以自定义成功和取消 URL:
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'),
]);
});当然,你也可以为订阅结账启用促销码:
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 会话 API 文档 以查看哪些参数可用。
当然,你可以在构建将通过 Stripe Checkout 完成的订阅时定义一个试用期:
$checkout = Auth::user()->newSubscription('default', 'price_monthly')
->trialDays(3)
->checkout();然而,试用期必须为至少 48 小时,这是 Stripe Checkout 支持的最短试用时间。
请记住,Stripe 和 Cashier 通过 Webhook 更新订阅状态,因此客户在输入付款信息并返回应用程序时,订阅可能尚未激活. 为了处理这种情况,您可能希望显示一条消息,告知用户其付款或订阅正在等待处理.
结账也支持收集客户的税号。要在结账会话中启用此功能,请在创建会话时调用 collectTaxIds 方法:
$checkout = $user->collectTaxIds()->checkout('price_tshirt');当调用此方法时,客户将可以使用一个新的复选框,允许他们表明是否以公司身份购买。如果是这样,他们将有机会提供他们的税号。
[!WARNING]
如果您已配置自动税务代收在您应用的 服务提供者 中 则此功能将自动启用并且无需调用collectTaxIds方法。
使用 Checkout::guest 方法,您可以为您的应用程序中没有“帐户”的访客启动结账会话:
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 实例上可用的其他方法来自定义访客结账会话:
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,以便将此事件实际发送到你的应用程序。一旦 webhook 在 Stripe 控制台中启用,你就可以 使用 Cashier 处理该 webhook。webhook 负载中包含的对象将是一个 checkout 对象,你可以检查该对象以履行客户的订单。
有时,订阅或单次支付可能会失败。发生这种情况时,Cashier 会抛出一个 Laravel\Cashier\Exceptions\IncompletePayment 异常,通知你此事发生。捕获此异常后,你有两个选项来处理。
首先,您可以将客户重定向到 Cashier 中包含的专用支付确认页面。此页面已经有一个关联的命名路由,该路由通过 Cashier 的服务提供者注册。因此,您可以捕获 IncompletePayment 异常并将用户重定向到支付确认页面:
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安全”确认。确认支付后,用户将被重定向到上面指定的 redirect 参数提供的 URL。重定向后,message(字符串)和 success(整数)查询字符串变量将被添加到 URL。支付页面当前支持以下支付方式类型:
或者,您可以让 Stripe 为您处理付款确认。在这种情况下,您可以不重定向到付款确认页面,而是在您的 Stripe 管理平台中设置 Stripe 的自动账单邮件。但是,如果捕获到 IncompletePayment 异常,您仍应通知用户他们将收到一封包含进一步付款确认说明的电子邮件。
以下方法可能会抛出支付异常:charge, invoiceFor, 和 invoice 在使用了 Billable trait 的模型上。在与订阅交互时,SubscriptionBuilder 上的 create 方法, 以及 Subscription 和 SubscriptionItem 模型上的 incrementAndInvoice 和 swapAndInvoice 方法可能会抛出不完整的支付异常。
确定现有订阅是否具有未完成的支付,可以通过在可计费模型或订阅实例上使用 hasIncompletePayment 方法来实现:
if ($user->hasIncompletePayment('default')) {
// ...
}
if ($user->subscription('default')->hasIncompletePayment()) {
// ...
}您可以通过检查异常实例上的 payment 属性来推导出未完成支付的具体状态:
use Laravel\Cashier\Exceptions\IncompletePayment;
try {
$user->charge(1000, 'pm_card_threeDSecure2Required');
} catch (IncompletePayment $exception) {
// Get the payment intent status...
$exception->payment->status;
// Check specific conditions...
if ($exception->payment->requiresPaymentMethod()) {
// ...
} elseif ($exception->payment->requiresConfirmation()) {
// ...
}
}某些支付方式需要额外数据以确认支付。例如,SEPA支付方式在支付过程中需要额外的“授权”数据。您可以使用 withPaymentConfirmationOptions 方法将此数据提供给 Cashier:
$subscription->withPaymentConfirmationOptions([
'mandate_data' => '...',
])->swap('price_xxx');您可以查阅 Stripe API 文档 以查看确认付款时接受的所有选项。
如果您的企业或您的客户之一位于欧洲,您将需要遵守欧盟的强客户认证(SCA)规定。这些规定由欧盟于2019年9月实施,以防止支付欺诈。幸运的是,Stripe 和 Cashier 已准备好构建符合SCA要求的应用程序。
[!警告]
在开始之前,请查阅 Stripe 关于 PSD2 和 SCA 的指南 以及他们的 关于新的 SCA API 的文档。
SCA 规定通常需要额外的验证以确认和处理付款。当这种情况发生时,Cashier 将抛出一个 Laravel\Cashier\Exceptions\IncompletePayment 异常,通知您需要额外的验证。有关如何处理这些异常的更多信息,请参阅 处理失败付款 的文档。
Stripe 或 Cashier 提供的支付确认屏幕可能会根据特定银行或发卡机构的支付流程进行定制,并可能包括额外的银行卡确认、临时小额扣款、单独的设备认证或其他形式的验证。
当支付需要额外确认时,订阅将保持在其 stripe_status 数据库列指示的 incomplete 或 past_due 状态。Cashier 将自动激活客户的订阅一旦支付确认完成并且您的应用程序通过 webhook 收到 Stripe 的完成通知。
有关 不完整 和 逾期 状态的更多信息,请参阅 我们关于这些状态的补充文档。
由于SCA法规要求客户即使在订阅处于活动状态时也需偶尔验证其支付详情,Cashier可以在需要离线支付确认时向客户发送通知。例如,这可能发生在订阅续订时。Cashier的支付通知可以通过将 CASHIER_PAYMENT_NOTIFICATION 环境变量设置为通知类来启用。默认情况下,此通知是禁用的。当然,Cashier包含一个可用于此目的的通知类,但如果需要,您可以自由提供自己的通知类:
CASHIER_PAYMENT_NOTIFICATION=Laravel\Cashier\Notifications\ConfirmPayment为确保会话外支付确认通知得以发送,验证 Stripe Webhook 已为您的应用程序配置 并且 invoice.payment_action_required Webhook 已在您的 Stripe 控制面板中启用。此外,您的 Billable 模型也应该使用 Laravel 的 Illuminate\Notifications\Notifiable trait。
[!WARNING]
即使客户手动进行需要额外确认的支付,也会发送通知。遗憾的是,Stripe 无法知道该支付是手动完成还是“离线会话”完成的。但是,如果客户在已确认支付后访问支付页面,他们只会看到“支付成功”消息。客户将不会被允许意外地两次确认同一笔支付并产生第二次意外扣款。
Cashier 的许多对象都是 Stripe SDK 对象的封装器。如果您希望直接与 Stripe 对象交互,可以使用 asStripe 方法方便地检索它们:
$stripeSubscription = $subscription->asStripeSubscription();
$stripeSubscription->application_fee_percent = 5;
$stripeSubscription->save();您也可以使用 updateStripeSubscription 方法直接更新 Stripe 订阅:
$subscription->updateStripeSubscription(['application_fee_percent' => 5]);您可以调用 Cashier 类上的 stripe 方法,如果您想直接使用 Stripe\StripeClient 客户端。例如,您可以使用此方法访问 StripeClient 实例,并从您的 Stripe 账户中获取价格列表:
use Laravel\Cashier\Cashier;
$prices = Cashier::stripe()->prices->all();在测试使用 Cashier 的应用程序时,你可以模拟对 Stripe API 的实际 HTTP 请求;然而,这要求你部分地重新实现 Cashier 自身的行为。因此,我们建议允许你的测试访问实际的 Stripe API。尽管这会比较慢,但它能提供更多信心,确保你的应用程序按预期工作,并且任何慢速测试都可以被放置在它们各自的 Pest / PHPUnit 测试组中。
在测试时,请记住 Cashier 本身已经拥有一个出色的测试套件,因此,您应该只专注于测试您的应用程序的订阅和支付流程,而不是 Cashier 的所有底层行为。
要开始使用,将你的 Stripe 密钥的测试版本添加到你的 phpunit.xml 文件中:
<env name="STRIPE_SECRET" value="sk_test_<your-key>"/>现在,每当你测试时与 Cashier 交互,它都会发送实际的 API 请求到你的 Stripe 测试环境。为方便起见,你应该预先在你的 Stripe 测试账户中填充你可能在测试期间使用的订阅 / 价格。
[!注意]
为了测试各种计费场景,例如信用卡拒绝和失败,您可以使用 Stripe 提供的大量测试卡号和令牌。