Laravel Cashier (Paddle)
简介
本文档适用于 Cashier Paddle 2.x 与 Paddle Billing 的集成。如果您仍在使用 Paddle Classic,请使用 Cashier Paddle 1.x。
Laravel Cashier Paddle 为 Paddle 的订阅计费服务提供了一个富有表现力的、流畅的接口。它处理了几乎所有令人生厌的样板化订阅计费代码。除了基本的订阅管理外,Cashier 还可以处理:切换订阅、订阅“数量”、订阅暂停、取消宽限期等。
在深入了解 Cashier Paddle 之前,我们建议您同时查阅 Paddle 的 概念指南 和 API 文档。
升级 Cashier
升级到新版本的 Cashier 时,请务必仔细查阅 升级指南。
安装
首先,使用 Composer 包管理器安装适用于 Paddle 的 Cashier 包
1composer require laravel/cashier-paddle
接下来,您应该使用 vendor:publish Artisan 命令发布 Cashier 迁移文件
1php artisan vendor:publish --tag="cashier-migrations"
然后,运行应用程序的数据库迁移。Cashier 迁移将创建一个新的 customers 表。此外,还会创建新的 subscriptions 和 subscription_items 表来存储客户的所有订阅信息。最后,会创建一个新的 transactions 表来存储与客户关联的所有 Paddle 交易记录。
1php artisan migrate
为确保 Cashier 正确处理所有 Paddle 事件,请记得 设置 Cashier 的 Webhook 处理程序。
Paddle 沙盒
在本地和测试开发期间,您应该 注册一个 Paddle 沙盒账户。此账户将为您提供一个沙盒环境,以便在不进行实际支付的情况下测试和开发应用程序。您可以使用 Paddle 的 测试卡号 来模拟各种支付场景。
使用 Paddle 沙盒环境时,应在应用程序的 .env 文件中将 PADDLE_SANDBOX 环境变量设置为 true
1PADDLE_SANDBOX=true
完成应用程序开发后,您可以 申请一个 Paddle 供应商账户。在应用程序投入生产之前,Paddle 需要审核您应用程序的域名。
配置
计费模型 (Billable Model)
在使用 Cashier 之前,必须在用户模型定义中添加 Billable trait。该 trait 提供了各种方法,允许您执行常见的计费任务,例如创建订阅和更新支付方式信息。
1use Laravel\Paddle\Billable;2 3class User extends Authenticatable4{5 use Billable;6}
如果您有非用户的计费实体,也可以将该 trait 添加到这些类中。
1use Illuminate\Database\Eloquent\Model;2use Laravel\Paddle\Billable;3 4class Team extends Model5{6 use Billable;7}
API 密钥
接下来,在应用程序的 .env 文件中配置您的 Paddle 密钥。您可以从 Paddle 控制面板获取您的 Paddle API 密钥。
1PADDLE_CLIENT_SIDE_TOKEN=your-paddle-client-side-token2PADDLE_API_KEY=your-paddle-api-key3PADDLE_RETAIN_KEY=your-paddle-retain-key4PADDLE_WEBHOOK_SECRET="your-paddle-webhook-secret"5PADDLE_SANDBOX=true
使用 Paddle 沙盒环境时,应将 PADDLE_SANDBOX 环境变量设置为 true。如果您将应用程序部署到生产环境并使用 Paddle 的正式供应商环境,则应将 PADDLE_SANDBOX 变量设置为 false。
PADDLE_RETAIN_KEY 是可选的,仅当您将 Paddle 与 Retain 配合使用时才需要设置。
Paddle JS
Paddle 依赖其自身的 JavaScript 库来启动 Paddle 结账小部件。您可以通过在应用程序布局的结束 </head> 标签之前放置 @paddleJS Blade 指令来加载该 JavaScript 库。
1<head>2 ...3 4 @paddleJS5</head>
货币配置
您可以指定在发票上显示货币数值时使用的语言环境。在内部,Cashier 使用 PHP 的 NumberFormatter 类 来设置货币语言环境。
1CASHIER_CURRENCY_LOCALE=nl_BE
为了使用除 en 以外的语言环境,请确保您的服务器上已安装并配置了 ext-intl PHP 扩展。
覆盖默认模型
您可以通过定义自己的模型并扩展相应的 Cashier 模型,自由地扩展 Cashier 在内部使用的模型。
1use Laravel\Paddle\Subscription as CashierSubscription;2 3class Subscription extends CashierSubscription4{5 // ...6}
定义模型后,您可以通过 Laravel\Paddle\Cashier 类指示 Cashier 使用您的自定义模型。通常,您应该在应用程序 App\Providers\AppServiceProvider 类的 boot 方法中告知 Cashier 有关您的自定义模型。
1use App\Models\Cashier\Subscription; 2use App\Models\Cashier\Transaction; 3 4/** 5 * Bootstrap any application services. 6 */ 7public function boot(): void 8{ 9 Cashier::useSubscriptionModel(Subscription::class);10 Cashier::useTransactionModel(Transaction::class);11}
快速入门
销售产品
在使用 Paddle 结账之前,您应该在 Paddle 仪表板中定义具有固定价格的产品。此外,您还应该 配置 Paddle 的 Webhook 处理程序。
通过应用程序提供产品和订阅计费可能会让人望而生畏。然而,得益于 Cashier 和 Paddle 结账浮层 (Checkout Overlay),您可以轻松构建现代、稳健的支付集成。
要对非经常性的单次购买产品进行扣费,我们将利用 Cashier 通过 Paddle 的结账浮层向客户收费,客户将在其中提供支付详情并确认购买。一旦通过结账浮层完成付款,客户将被重定向到您在应用程序内选择的成功 URL。
1use Illuminate\Http\Request;2 3Route::get('/buy', function (Request $request) {4 $checkout = $request->user()->checkout('pri_deluxe_album')5 ->returnTo(route('dashboard'));6 7 return view('buy', ['checkout' => $checkout]);8})->name('checkout');
如上述示例所示,我们将利用 Cashier 提供的 checkout 方法创建一个结账对象,为客户展示给定“价格标识符”的 Paddle 结账浮层。使用 Paddle 时,“价格”是指 为特定产品定义的价格。
如有必要,checkout 方法会自动在 Paddle 中创建客户,并将该 Paddle 客户记录连接到您应用程序数据库中的相应用户。完成结账会话后,客户将被重定向到一个专门的成功页面,您可以在该页面向客户显示信息性消息。
在 buy 视图中,我们将包含一个按钮来显示结账浮层。paddle-button Blade 组件随 Cashier Paddle 一起提供;但是,您也可以 手动渲染浮层结账。
1<x-paddle-button :checkout="$checkout" class="px-8 py-4">2 Buy Product3</x-paddle-button>
向 Paddle 结账提供元数据
销售产品时,通常需要通过您自己应用程序定义的 Cart 和 Order 模型来跟踪已完成的订单和已购买的产品。将客户重定向到 Paddle 结账浮层以完成购买时,您可能需要提供现有的订单标识符,以便在客户重定向回您的应用程序时,将已完成的购买与相应的订单关联起来。
为实现这一点,您可以向 checkout 方法提供一个自定义数据数组。假设当用户开始结账流程时,在我们的应用程序中创建了一个待处理的 Order。请记住,此示例中的 Cart 和 Order 模型仅为说明之用,并非由 Cashier 提供。您可以根据自己应用程序的需求自由实现这些概念。
1use App\Models\Cart; 2use App\Models\Order; 3use Illuminate\Http\Request; 4 5Route::get('/cart/{cart}/checkout', function (Request $request, Cart $cart) { 6 $order = Order::create([ 7 'cart_id' => $cart->id, 8 'price_ids' => $cart->price_ids, 9 'status' => 'incomplete',10 ]);11 12 $checkout = $request->user()->checkout($order->price_ids)13 ->customData(['order_id' => $order->id]);14 15 return view('billing', ['checkout' => $checkout]);16})->name('checkout');
如上述示例所示,当用户开始结账流程时,我们将向 checkout 方法提供所有与购物车/订单相关的 Paddle 价格标识符。当然,您的应用程序有责任在客户添加商品时将这些项目与“购物车”或订单关联起来。我们还通过 customData 方法将订单 ID 提供给 Paddle 结账浮层。
当然,您可能希望在客户完成结账流程后将订单标记为“完成”。为此,您可以监听 Paddle 分派并由 Cashier 引发的 Webhook 事件,将订单信息存储在您的数据库中。
首先,监听由 Cashier 分派的 TransactionCompleted 事件。通常,您应该在应用程序 AppServiceProvider 的 boot 方法中注册事件监听器。
1use App\Listeners\CompleteOrder; 2use Illuminate\Support\Facades\Event; 3use Laravel\Paddle\Events\TransactionCompleted; 4 5/** 6 * Bootstrap any application services. 7 */ 8public function boot(): void 9{10 Event::listen(TransactionCompleted::class, CompleteOrder::class);11}
在此示例中,CompleteOrder 监听器可能如下所示
1namespace App\Listeners; 2 3use App\Models\Order; 4use Laravel\Paddle\Cashier; 5use Laravel\Paddle\Events\TransactionCompleted; 6 7class CompleteOrder 8{ 9 /**10 * Handle the incoming Cashier webhook event.11 */12 public function handle(TransactionCompleted $event): void13 {14 $orderId = $event->payload['data']['custom_data']['order_id'] ?? null;15 16 $order = Order::findOrFail($orderId);17 18 $order->update(['status' => 'completed']);19 }20}
有关 transaction.completed 事件所包含数据的更多信息,请参阅 Paddle 的文档 。
销售订阅
在使用 Paddle 结账之前,您应该在 Paddle 仪表板中定义具有固定价格的产品。此外,您还应该 配置 Paddle 的 Webhook 处理程序。
通过应用程序提供产品和订阅计费可能会让人望而生畏。然而,得益于 Cashier 和 Paddle 结账浮层 (Checkout Overlay),您可以轻松构建现代、稳健的支付集成。
为了了解如何使用 Cashier 和 Paddle 的结账浮层销售订阅,让我们考虑一个简单的场景:一个包含基础月度(price_basic_monthly)和年度(price_basic_yearly)计划的订阅服务。这两个价格可以在我们的 Paddle 仪表板中归入一个“Basic”产品(pro_basic)下。此外,我们的订阅服务可能还提供一个名为 pro_expert 的“Expert”计划。
首先,让我们了解客户如何订阅我们的服务。当然,您可以想象客户可能会在应用程序的定价页面上点击 Basic 计划的“订阅”按钮。此按钮将为他们选择的计划调用 Paddle 结账浮层。首先,让我们通过 checkout 方法启动一个结账会话。
1use Illuminate\Http\Request;2 3Route::get('/subscribe', function (Request $request) {4 $checkout = $request->user()->checkout('price_basic_monthly')5 ->returnTo(route('dashboard'));6 7 return view('subscribe', ['checkout' => $checkout]);8})->name('subscribe');
在 subscribe 视图中,我们将包含一个按钮来显示结账浮层。paddle-button Blade 组件随 Cashier Paddle 一起提供;但是,您也可以 手动渲染浮层结账。
1<x-paddle-button :checkout="$checkout" class="px-8 py-4">2 Subscribe3</x-paddle-button>
现在,当点击订阅按钮时,客户将能够输入他们的支付详情并启动订阅。要了解订阅何时真正开始(因为某些支付方式需要几秒钟来处理),您还应该 配置 Cashier 的 Webhook 处理程序。
既然客户可以开始订阅,我们需要限制应用程序的某些部分,以便只有已订阅的用户才能访问它们。当然,我们始终可以通过 Cashier 的 Billable trait 提供的 subscribed 方法确定用户的当前订阅状态。
1@if ($user->subscribed())2 <p>You are subscribed.</p>3@endif
我们甚至可以轻松确定用户是否订阅了特定产品或价格。
1@if ($user->subscribedToProduct('pro_basic'))2 <p>You are subscribed to our Basic product.</p>3@endif4 5@if ($user->subscribedToPrice('price_basic_monthly'))6 <p>You are subscribed to our monthly Basic plan.</p>7@endif
构建订阅中间件
为方便起见,您可能希望创建一个 中间件,用于确定传入请求是否来自已订阅的用户。定义此中间件后,您可以轻松将其分配给路由,以防止未订阅的用户访问该路由。
1<?php 2 3namespace App\Http\Middleware; 4 5use Closure; 6use Illuminate\Http\Request; 7use Symfony\Component\HttpFoundation\Response; 8 9class Subscribed10{11 /**12 * Handle an incoming request.13 */14 public function handle(Request $request, Closure $next): Response15 {16 if (! $request->user()?->subscribed()) {17 // Redirect user to billing page and ask them to subscribe...18 return redirect('/subscribe');19 }20 21 return $next($request);22 }23}
定义中间件后,您可以将其分配给路由。
1use App\Http\Middleware\Subscribed;2 3Route::get('/dashboard', function () {4 // ...5})->middleware([Subscribed::class]);
允许客户管理其计费套餐
当然,客户可能希望将其订阅套餐更改为其他产品或“级别”。在我们上面的示例中,我们希望允许客户将其套餐从月度订阅更改为年度订阅。为此,您需要实现一个类似跳转到以下路由的按钮。
1use Illuminate\Http\Request;2 3Route::put('/subscription/{price}/swap', function (Request $request, $price) {4 $user->subscription()->swap($price); // With "$price" being "price_basic_yearly" for this example.5 6 return redirect()->route('dashboard');7})->name('subscription.swap');
除了交换套餐外,您还需要允许您的客户取消订阅。像交换套餐一样,提供一个跳转到以下路由的按钮。
1use Illuminate\Http\Request;2 3Route::put('/subscription/cancel', function (Request $request, $price) {4 $user->subscription()->cancel();5 6 return redirect()->route('dashboard');7})->name('subscription.cancel');
现在,您的订阅将在计费周期结束时取消。
只要您配置了 Cashier 的 Webhook 处理程序,Cashier 就会通过检查来自 Paddle 的传入 Webhook,自动保持应用程序中与 Cashier 相关的数据库表同步。因此,例如,当您通过 Paddle 的仪表板取消客户的订阅时,Cashier 将接收到相应的 Webhook 并将应用程序数据库中的订阅标记为“已取消”。
结账会话
大多数向客户计费的操作都是通过 Paddle 的 结账浮层小部件 (Checkout Overlay widget) 或利用 内嵌结账 (inline checkout) 进行的。
在使用 Paddle 处理结账付款之前,您应该在 Paddle 结账设置仪表板中定义应用程序的 默认付款链接。
浮层结账
在显示结账浮层小部件之前,您必须使用 Cashier 生成结账会话。结账会话将通知结账小部件应执行的计费操作。
1use Illuminate\Http\Request;2 3Route::get('/buy', function (Request $request) {4 $checkout = $user->checkout('pri_34567')5 ->returnTo(route('dashboard'));6 7 return view('billing', ['checkout' => $checkout]);8});
Cashier 包含一个 paddle-button Blade 组件。您可以将结账会话作为“prop”传递给此组件。然后,当点击此按钮时,将显示 Paddle 的结账小部件。
1<x-paddle-button :checkout="$checkout" class="px-8 py-4">2 Subscribe3</x-paddle-button>
默认情况下,这将使用 Paddle 的默认样式显示小部件。您可以通过向组件添加 Paddle 支持的属性(例如 data-theme='light' 属性)来自定义小部件。
1<x-paddle-button :checkout="$checkout" class="px-8 py-4" data-theme="light">2 Subscribe3</x-paddle-button>
Paddle 结账小部件是异步的。一旦用户在小部件中创建了订阅,Paddle 就会向您的应用程序发送一个 Webhook,以便您可以正确更新应用程序数据库中的订阅状态。因此,正确 设置 Webhook 以适应来自 Paddle 的状态更改非常重要。
订阅状态更改后,接收相应 Webhook 的延迟通常很小,但您应该在应用程序中考虑到这一点,因为您的用户在完成结账后可能无法立即使用订阅。
手动渲染浮层结账
您也可以在不使用 Laravel 内置 Blade 组件的情况下手动渲染浮层结账。首先,按照前面的示例 生成结账会话。
1use Illuminate\Http\Request;2 3Route::get('/buy', function (Request $request) {4 $checkout = $user->checkout('pri_34567')5 ->returnTo(route('dashboard'));6 7 return view('billing', ['checkout' => $checkout]);8});
接下来,您可以使用 Paddle.js 初始化结账。在此示例中,我们将创建一个分配了 paddle_button 类的链接。Paddle.js 将检测此类并在点击链接时显示浮层结账。
1<?php 2$items = $checkout->getItems(); 3$customer = $checkout->getCustomer(); 4$custom = $checkout->getCustomData(); 5?> 6 7<a 8 href='#!' 9 class='paddle_button'10 data-items='{!! json_encode($items) !!}'11 @if ($customer) data-customer-id='{{ $customer->paddle_id }}' @endif12 @if ($custom) data-custom-data='{{ json_encode($custom) }}' @endif13 @if ($returnUrl = $checkout->getReturnUrl()) data-success-url='{{ $returnUrl }}' @endif14>15 Buy Product16</a>
内嵌结账
如果您不想使用 Paddle 的“浮层”式结账小部件,Paddle 还提供了内嵌显示小部件的选项。虽然此方法不允许您调整结账的任何 HTML 字段,但它允许您将小部件嵌入到您的应用程序中。
为了让您轻松上手内嵌结账,Cashier 包含一个 paddle-checkout Blade 组件。首先,您应该 生成一个结账会话。
1use Illuminate\Http\Request;2 3Route::get('/buy', function (Request $request) {4 $checkout = $user->checkout('pri_34567')5 ->returnTo(route('dashboard'));6 7 return view('billing', ['checkout' => $checkout]);8});
然后,您可以将结账会话传递给组件的 checkout 属性。
1<x-paddle-checkout :checkout="$checkout" class="w-full" />
要调整内嵌结账组件的高度,您可以将 height 属性传递给 Blade 组件。
1<x-paddle-checkout :checkout="$checkout" class="w-full" height="500" />
有关内嵌结账自定义选项的更多详细信息,请查阅 Paddle 关于 内嵌结账 和 可用结账设置 的指南。
手动渲染内嵌结账
您也可以在不使用 Laravel 内置 Blade 组件的情况下手动渲染内嵌结账。首先,按照前面的示例 生成结账会话。
1use Illuminate\Http\Request;2 3Route::get('/buy', function (Request $request) {4 $checkout = $user->checkout('pri_34567')5 ->returnTo(route('dashboard'));6 7 return view('billing', ['checkout' => $checkout]);8});
接下来,您可以使用 Paddle.js 初始化结账。在此示例中,我们将演示如何使用 Alpine.js;但是,您可以根据自己的前端技术栈自由修改此示例。
1<?php 2$options = $checkout->options(); 3 4$options['settings']['frameTarget'] = 'paddle-checkout'; 5$options['settings']['frameInitialHeight'] = 366; 6?> 7 8<div class="paddle-checkout" x-data="{}" x-init=" 9 Paddle.Checkout.open(@json($options));10">11</div>
游客结账
有时,您可能需要为不需要在您的应用程序中创建账户的用户创建结账会话。为此,您可以使用 guest 方法。
1use Illuminate\Http\Request;2use Laravel\Paddle\Checkout;3 4Route::get('/buy', function (Request $request) {5 $checkout = Checkout::guest(['pri_34567'])6 ->returnTo(route('home'));7 8 return view('billing', ['checkout' => $checkout]);9});
然后,您可以将结账会话提供给 Paddle 按钮 或 内嵌结账 Blade 组件。
价格预览
Paddle 允许您按货币自定义价格,实际上允许您为不同的国家配置不同的价格。Cashier Paddle 允许您使用 previewPrices 方法检索所有这些价格。此方法接受您希望检索价格的价格 ID。
1use Laravel\Paddle\Cashier;2 3$prices = Cashier::previewPrices(['pri_123', 'pri_456']);
货币将根据请求的 IP 地址确定;但是,您可以选择提供特定国家/地区来检索价格。
1use Laravel\Paddle\Cashier;2 3$prices = Cashier::previewPrices(['pri_123', 'pri_456'], ['address' => [4 'country_code' => 'BE',5 'postal_code' => '1234',6]]);
检索价格后,您可以按照自己的意愿显示它们。
1<ul>2 @foreach ($prices as $price)3 <li>{{ $price->product['name'] }} - {{ $price->total() }}</li>4 @endforeach5</ul>
您还可以分别显示小计价格和税额。
1<ul>2 @foreach ($prices as $price)3 <li>{{ $price->product['name'] }} - {{ $price->subtotal() }} (+ {{ $price->tax() }} tax)</li>4 @endforeach5</ul>
有关更多信息,请 查看 Paddle 关于价格预览的 API 文档。
客户价格预览
如果用户已经是客户,并且您想显示适用于该客户的价格,可以通过直接从客户实例中检索价格来实现。
1use App\Models\User;2 3$prices = User::find(1)->previewPrices(['pri_123', 'pri_456']);
在内部,Cashier 将使用用户的客户 ID 来检索其货币对应的价格。因此,例如,居住在美国的用户将看到以美元显示的价格,而居住在比利时的用户将看到以欧元显示的价格。如果找不到匹配的货币,将使用产品的默认货币。您可以在 Paddle 控制面板中自定义产品或订阅计划的所有价格。
折扣
您还可以选择在折扣后显示价格。调用 previewPrices 方法时,可以通过 discount_id 选项提供折扣 ID。
1use Laravel\Paddle\Cashier;2 3$prices = Cashier::previewPrices(['pri_123', 'pri_456'], [4 'discount_id' => 'dsc_123'5]);
然后,显示计算出的价格。
1<ul>2 @foreach ($prices as $price)3 <li>{{ $price->product['name'] }} - {{ $price->total() }}</li>4 @endforeach5</ul>
客户
客户默认设置
Cashier 允许您在创建结账会话时为客户定义一些有用的默认值。设置这些默认值允许您预填客户的电子邮件地址和姓名,以便他们可以立即进入结账小部件的支付部分。您可以通过覆盖计费模型上的以下方法来设置这些默认值。
1/** 2 * Get the customer's name to associate with Paddle. 3 */ 4public function paddleName(): string|null 5{ 6 return $this->name; 7} 8 9/**10 * Get the customer's email address to associate with Paddle.11 */12public function paddleEmail(): string|null13{14 return $this->email;15}
这些默认值将用于 Cashier 中生成 结账会话 的每一个操作。
检索客户
您可以使用 Cashier::findBillable 方法通过 Paddle 客户 ID 检索客户。此方法将返回计费模型的一个实例。
1use Laravel\Paddle\Cashier;2 3$user = Cashier::findBillable($customerId);
创建客户
有时,您可能希望在不开始订阅的情况下创建 Paddle 客户。您可以使用 createAsCustomer 方法实现这一点。
1$customer = $user->createAsCustomer();
将返回 Laravel\Paddle\Customer 的一个实例。一旦在 Paddle 中创建了客户,您就可以在以后的日期开始订阅。您可以提供一个可选的 $options 数组,以传递 Paddle API 支持的任何其他客户创建参数。
1$customer = $user->createAsCustomer($options);
订阅
创建订阅
要创建订阅,首先从数据库中检索计费模型的实例,这通常是 App\Models\User 的一个实例。检索到模型实例后,您可以使用 subscribe 方法来创建模型的结账会话。
1use Illuminate\Http\Request;2 3Route::get('/user/subscribe', function (Request $request) {4 $checkout = $request->user()->subscribe($premium = 'pri_123', 'default')5 ->returnTo(route('home'));6 7 return view('billing', ['checkout' => $checkout]);8});
传递给 subscribe 方法的第一个参数是用户要订阅的具体价格。此值应对应于 Paddle 中的价格标识符。returnTo 方法接受一个 URL,用户在成功完成结账后将被重定向到该 URL。传递给 subscribe 方法的第二个参数应该是订阅的内部“类型”。如果您的应用程序只提供单一订阅,您可以将其称为 default 或 primary。此订阅类型仅用于内部应用程序使用,不打算显示给用户。此外,它不应包含空格,并且在创建订阅后绝不应更改。
您还可以使用 customData 方法提供有关订阅的自定义元数据数组。
1$checkout = $request->user()->subscribe($premium = 'pri_123', 'default')2 ->customData(['key' => 'value'])3 ->returnTo(route('home'));
创建订阅结账会话后,可以将结账会话提供给随 Cashier Paddle 一起提供的 paddle-button Blade 组件。
1<x-paddle-button :checkout="$checkout" class="px-8 py-4">2 Subscribe3</x-paddle-button>
用户完成结账后,Paddle 将分派一个 subscription_created Webhook。Cashier 将接收此 Webhook 并为您的客户设置订阅。为了确保应用程序正确接收和处理所有 Webhook,请确保您已正确 设置 Webhook 处理程序。
检查订阅状态
一旦用户订阅了您的应用程序,您就可以使用各种便捷的方法检查他们的订阅状态。首先,如果用户有有效的订阅,即使订阅目前处于试用期,subscribed 方法也会返回 true。
1if ($user->subscribed()) {2 // ...3}
如果您的应用程序提供多种订阅,您可以在调用 subscribed 方法时指定订阅类型。
1if ($user->subscribed('default')) {2 // ...3}
subscribed 方法也非常适合用作 路由中间件,允许您根据用户的订阅状态过滤对路由和控制器的访问。
1<?php 2 3namespace App\Http\Middleware; 4 5use Closure; 6use Illuminate\Http\Request; 7use Symfony\Component\HttpFoundation\Response; 8 9class EnsureUserIsSubscribed10{11 /**12 * Handle an incoming request.13 *14 * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next15 */16 public function handle(Request $request, Closure $next): Response17 {18 if ($request->user() && ! $request->user()->subscribed()) {19 // This user is not a paying customer...20 return redirect('/billing');21 }22 23 return $next($request);24 }25}
如果您想确定用户是否仍处于试用期,可以使用 onTrial 方法。此方法对于确定是否应向用户显示他们仍处于试用期的警告非常有用。
1if ($user->subscription()->onTrial()) {2 // ...3}
subscribedToPrice 方法可用于根据给定的 Paddle 价格 ID 确定用户是否订阅了给定的套餐。在此示例中,我们将确定用户的 default 订阅是否已积极订阅了月度价格。
1if ($user->subscribedToPrice($monthly = 'pri_123', 'default')) {2 // ...3}
recurring 方法可用于确定用户目前是否处于活跃订阅状态,并且不再处于试用期或宽限期内。
1if ($user->subscription()->recurring()) {2 // ...3}
已取消的订阅状态
要确定用户是否曾是活跃订阅者但已取消订阅,可以使用 canceled 方法。
1if ($user->subscription()->canceled()) {2 // ...3}
您还可以确定用户是否已取消订阅,但仍处于直到订阅完全到期之前的“宽限期”内。例如,如果用户在 3 月 5 日取消了原定于 3 月 10 日到期的订阅,则用户处于直到 3 月 10 日的“宽限期”。此外,在此期间 subscribed 方法仍将返回 true。
1if ($user->subscription()->onGracePeriod()) {2 // ...3}
逾期状态
如果订阅付款失败,它将被标记为 past_due。当您的订阅处于此状态时,它将不会处于活动状态,直到客户更新其支付信息。您可以使用订阅实例上的 pastDue 方法确定订阅是否逾期。
1if ($user->subscription()->pastDue()) {2 // ...3}
当订阅逾期时,您应该指示用户 更新其支付信息。
如果您希望订阅在 past_due 时仍被视为有效,可以使用 Cashier 提供的 keepPastDueSubscriptionsActive 方法。通常,此方法应在您的 AppServiceProvider 的 register 方法中调用。
1use Laravel\Paddle\Cashier;2 3/**4 * Register any application services.5 */6public function register(): void7{8 Cashier::keepPastDueSubscriptionsActive();9}
当订阅处于 past_due 状态时,在更新支付信息之前,它无法更改。因此,当订阅处于 past_due 状态时,swap 和 updateQuantity 方法将抛出异常。
订阅作用域
大多数订阅状态也可作为查询作用域使用,以便您可以轻松地查询数据库中处于给定状态的订阅。
1// Get all valid subscriptions...2$subscriptions = Subscription::query()->valid()->get();3 4// Get all of the canceled subscriptions for a user...5$subscriptions = $user->subscriptions()->canceled()->get();
可用作用域的完整列表如下所示。
1Subscription::query()->valid(); 2Subscription::query()->onTrial(); 3Subscription::query()->expiredTrial(); 4Subscription::query()->notOnTrial(); 5Subscription::query()->active(); 6Subscription::query()->recurring(); 7Subscription::query()->pastDue(); 8Subscription::query()->paused(); 9Subscription::query()->notPaused();10Subscription::query()->onPausedGracePeriod();11Subscription::query()->notOnPausedGracePeriod();12Subscription::query()->canceled();13Subscription::query()->notCanceled();14Subscription::query()->onGracePeriod();15Subscription::query()->notOnGracePeriod();
订阅单次扣费
订阅单次扣费允许您在订阅的基础上向订阅者收取一次性费用。在调用 charge 方法时,您必须提供一个或多个价格 ID。
1// Charge a single price...2$response = $user->subscription()->charge('pri_123');3 4// Charge multiple prices at once...5$response = $user->subscription()->charge(['pri_123', 'pri_456']);
charge 方法实际上不会在下一个订阅计费周期之前向客户收费。如果您想立即向客户开具发票,可以使用 chargeAndInvoice 方法。
1$response = $user->subscription()->chargeAndInvoice('pri_123');
更新支付信息
Paddle 始终为每个订阅保存一种支付方式。如果您想更新订阅的默认支付方式,应使用订阅模型上的 redirectToUpdatePaymentMethod 方法将客户重定向到 Paddle 托管的支付方式更新页面。
1use Illuminate\Http\Request;2 3Route::get('/update-payment-method', function (Request $request) {4 $user = $request->user();5 6 return $user->subscription()->redirectToUpdatePaymentMethod();7});
当用户更新完信息后,Paddle 将分派一个 subscription_updated Webhook,订阅详情将在您的应用程序数据库中更新。
更改套餐
用户订阅您的应用程序后,有时可能希望更改为新的订阅套餐。要更新用户的订阅套餐,您应该将 Paddle 价格标识符传递给订阅的 swap 方法。
1use App\Models\User;2 3$user = User::find(1);4 5$user->subscription()->swap($premium = 'pri_456');
如果您想更换套餐并立即向用户开具发票,而不是等待下一个计费周期,可以使用 swapAndInvoice 方法。
1$user = User::find(1);2 3$user->subscription()->swapAndInvoice($premium = 'pri_456');
按比例计算 (Prorations)
默认情况下,Paddle 在更换套餐时会对费用进行按比例计算。noProrate 方法可用于更新订阅而不按比例计算费用。
1$user->subscription('default')->noProrate()->swap($premium = 'pri_456');
如果您想禁用按比例计算并立即向客户开具发票,可以结合使用 swapAndInvoice 方法和 noProrate。
1$user->subscription('default')->noProrate()->swapAndInvoice($premium = 'pri_456');
或者,为了不对订阅更改向客户收费,您可以利用 doNotBill 方法。
1$user->subscription('default')->doNotBill()->swap($premium = 'pri_456');
有关 Paddle 按比例计算政策的更多信息,请查阅 Paddle 的 按比例计算文档。
订阅数量
有时订阅会受“数量”影响。例如,项目管理应用程序可能会按每个项目每月 10 美元收费。要轻松增加或减少订阅数量,请使用 incrementQuantity 和 decrementQuantity 方法。
1$user = User::find(1); 2 3$user->subscription()->incrementQuantity(); 4 5// Add five to the subscription's current quantity... 6$user->subscription()->incrementQuantity(5); 7 8$user->subscription()->decrementQuantity(); 9 10// Subtract five from the subscription's current quantity...11$user->subscription()->decrementQuantity(5);
或者,您可以使用 updateQuantity 方法设置特定数量。
1$user->subscription()->updateQuantity(10);
noProrate 方法可用于更新订阅数量而不按比例计算费用。
1$user->subscription()->noProrate()->updateQuantity(10);
多产品订阅的数量
如果您的订阅是 多产品订阅,则应将您希望增加或减少数量的价格 ID 作为第二个参数传递给增加/减少方法。
1$user->subscription()->incrementQuantity(1, 'price_chat');
多产品订阅
多产品订阅 允许您将多个计费产品分配给单个订阅。例如,想象一下您正在构建一个客户服务“帮助台”应用程序,其基础订阅价格为每月 10 美元,但提供每月额外 15 美元的实时聊天附加产品。
创建订阅结账会话时,您可以通过将价格数组作为 subscribe 方法的第一个参数传递,为特定订阅指定多个产品。
1use Illuminate\Http\Request; 2 3Route::post('/user/subscribe', function (Request $request) { 4 $checkout = $request->user()->subscribe([ 5 'price_monthly', 6 'price_chat', 7 ]); 8 9 return view('billing', ['checkout' => $checkout]);10});
在上面的示例中,客户的 default 订阅将附加两个价格。这两个价格都将在各自的计费周期内收取费用。如有必要,您可以传递键/值对的关联数组来指示每个价格的具体数量。
1$user = User::find(1);2 3$checkout = $user->subscribe('default', ['price_monthly', 'price_chat' => 5]);
如果您想向现有订阅添加另一个价格,必须使用订阅的 swap 方法。调用 swap 方法时,还应包括订阅当前的价格和数量。
1$user = User::find(1);2 3$user->subscription()->swap(['price_chat', 'price_original' => 2]);
上面的示例将添加新价格,但客户在下一个计费周期之前不会被收取费用。如果您想立即向客户收费,可以使用 swapAndInvoice 方法。
1$user->subscription()->swapAndInvoice(['price_chat', 'price_original' => 2]);
您可以使用 swap 方法并省略要删除的价格来从订阅中删除价格。
1$user->subscription()->swap(['price_original' => 2]);
您不能删除订阅上的最后一个价格。相反,您应该直接取消订阅。
多重订阅
Paddle 允许您的客户同时拥有多个订阅。例如,您可以经营一家提供游泳订阅和举重订阅的健身房,每种订阅可能有不同的定价。当然,客户应该能够订阅其中一项或两项计划。
当您的应用程序创建订阅时,您可以将订阅类型作为第二个参数提供给 subscribe 方法。类型可以是表示用户正在启动的订阅类型的任何字符串。
1use Illuminate\Http\Request;2 3Route::post('/swimming/subscribe', function (Request $request) {4 $checkout = $request->user()->subscribe($swimmingMonthly = 'pri_123', 'swimming');5 6 return view('billing', ['checkout' => $checkout]);7});
在此示例中,我们为客户启动了月度游泳订阅。但是,他们以后可能希望更换为年度订阅。调整客户的订阅时,我们可以简单地交换 swimming 订阅的价格。
1$user->subscription('swimming')->swap($swimmingYearly = 'pri_456');
当然,您也可以完全取消订阅。
1$user->subscription('swimming')->cancel();
暂停订阅
要暂停订阅,请调用用户订阅上的 pause 方法。
1$user->subscription()->pause();
当订阅暂停时,Cashier 会自动设置数据库中的 paused_at 列。此列用于确定 paused 方法何时开始返回 true。例如,如果客户在 3 月 1 日暂停了订阅,但订阅原定于 3 月 5 日到期扣费,则 paused 方法在 3 月 5 日之前将继续返回 false。这是因为用户通常被允许继续使用应用程序直到其计费周期结束。
默认情况下,暂停发生在下一个计费周期,因此客户可以使用他们已付款周期的剩余时间。如果您想立即暂停订阅,可以使用 pauseNow 方法。
1$user->subscription()->pauseNow();
使用 pauseUntil 方法,您可以将订阅暂停到特定的时间点。
1$user->subscription()->pauseUntil(now()->plus(months: 1));
或者,您可以使用 pauseNowUntil 方法立即将订阅暂停到给定的时间点。
1$user->subscription()->pauseNowUntil(now()->plus(months: 1));
您可以使用 onPausedGracePeriod 方法确定用户是否已暂停订阅但仍处于“宽限期”内。
1if ($user->subscription()->onPausedGracePeriod()) {2 // ...3}
要恢复已暂停的订阅,您可以调用订阅上的 resume 方法。
1$user->subscription()->resume();
订阅暂停期间无法修改。如果您想更换套餐或更新数量,必须先恢复订阅。
取消订阅
要取消订阅,请调用用户订阅上的 cancel 方法。
1$user->subscription()->cancel();
当订阅取消时,Cashier 会自动设置数据库中的 ends_at 列。此列用于确定 subscribed 方法何时开始返回 false。例如,如果客户在 3 月 1 日取消了订阅,但订阅原定于 3 月 5 日到期,则 subscribed 方法在 3 月 5 日之前将继续返回 true。这样做是因为用户通常被允许继续使用应用程序直到其计费周期结束。
您可以使用 onGracePeriod 方法确定用户是否已取消订阅但仍处于“宽限期”内。
1if ($user->subscription()->onGracePeriod()) {2 // ...3}
如果您希望立即取消订阅,可以在订阅上调用 cancelNow 方法。
1$user->subscription()->cancelNow();
要阻止处于宽限期的订阅被取消,您可以调用 stopCancelation 方法。
1$user->subscription()->stopCancelation();
Paddle 的订阅在取消后无法恢复。如果您的客户希望恢复订阅,他们将必须创建一个新订阅。
订阅试用
预先提供支付方式
如果您想在预先收集支付方式信息的同时为客户提供试用期,您应该在客户订阅的价格所在的 Paddle 仪表板上设置试用时间。然后,按常规启动结账会话。
1use Illuminate\Http\Request;2 3Route::get('/user/subscribe', function (Request $request) {4 $checkout = $request->user()5 ->subscribe('pri_monthly')6 ->returnTo(route('home'));7 8 return view('billing', ['checkout' => $checkout]);9});
当您的应用程序收到 subscription_created 事件时,Cashier 将在应用程序数据库中的订阅记录上设置试用期结束日期,并指示 Paddle 在此日期之前不要开始向客户计费。
如果客户的订阅在试用期结束日期之前没有取消,他们将在试用期结束后立即被收费,因此请务必通知您的用户试用期结束日期。
您可以使用用户实例上的 onTrial 方法确定用户是否处于试用期。
1if ($user->onTrial()) {2 // ...3}
要确定现有试用期是否已过期,可以使用 hasExpiredTrial 方法。
1if ($user->hasExpiredTrial()) {2 // ...3}
要确定用户是否针对特定订阅类型处于试用期,您可以将类型传递给 onTrial 或 hasExpiredTrial 方法。
1if ($user->onTrial('default')) {2 // ...3}4 5if ($user->hasExpiredTrial('default')) {6 // ...7}
无需预先提供支付方式
如果您想在不预先收集用户支付方式信息的情况下提供试用期,您可以将附加到用户的客户记录上的 trial_ends_at 列设置为您期望的试用结束日期。这通常是在用户注册期间完成的。
1use App\Models\User;2 3$user = User::create([4 // ...5]);6 7$user->createAsCustomer([8 'trial_ends_at' => now()->plus(days: 10)9]);
Cashier 将这种类型的试用称为“通用试用”,因为它未附加到任何现有订阅。如果当前日期未超过 trial_ends_at 的值,User 实例上的 onTrial 方法将返回 true。
1if ($user->onTrial()) {2 // User is within their trial period...3}
一旦准备好为用户创建实际订阅,就可以像往常一样使用 subscribe 方法。
1use Illuminate\Http\Request;2 3Route::get('/user/subscribe', function (Request $request) {4 $checkout = $request->user()5 ->subscribe('pri_monthly')6 ->returnTo(route('home'));7 8 return view('billing', ['checkout' => $checkout]);9});
要检索用户的试用结束日期,可以使用 trialEndsAt 方法。如果用户处于试用期,此方法将返回 Carbon 日期实例,如果不在试用期,则返回 null。如果您想获取除默认订阅之外的特定订阅的试用结束日期,还可以传递一个可选的订阅类型参数。
1if ($user->onTrial('default')) {2 $trialEndsAt = $user->trialEndsAt();3}
如果您想明确知道用户处于其“通用”试用期内且尚未创建实际订阅,可以使用 onGenericTrial 方法。
1if ($user->onGenericTrial()) {2 // User is within their "generic" trial period...3}
延长或激活试用
您可以通过调用 extendTrial 方法并指定试用期应结束的时间点来延长订阅上的现有试用期。
1$user->subscription()->extendTrial(now()->plus(days: 5));
或者,您可以调用订阅上的 activate 方法来结束试用期,从而立即激活订阅。
1$user->subscription()->activate();
处理 Paddle Webhook
Paddle 可以通过 Webhook 向您的应用程序通知各种事件。默认情况下,Cashier 服务提供程序会注册一个指向 Cashier Webhook 控制器的路由。此控制器将处理所有传入的 Webhook 请求。
默认情况下,此控制器会自动处理取消失败次数过多的订阅、订阅更新和支付方式更改;但是,正如我们很快会发现的那样,您可以扩展此控制器以处理您喜欢的任何 Paddle Webhook 事件。
为确保您的应用程序能够处理 Paddle Webhook,请务必 在 Paddle 控制面板中配置 Webhook URL。默认情况下,Cashier 的 Webhook 控制器响应 /paddle/webhook URL 路径。您应该在 Paddle 控制面板中启用的所有 Webhook 的完整列表是:
- 客户更新 (Customer Updated)
- 交易完成 (Transaction Completed)
- 交易更新 (Transaction Updated)
- 订阅创建 (Subscription Created)
- 订阅更新 (Subscription Updated)
- 订阅暂停 (Subscription Paused)
- 订阅取消 (Subscription Canceled)
请确保使用 Cashier 包含的 Webhook 签名验证 中间件来保护传入的请求。
Webhook 与 CSRF 保护
由于 Paddle Webhook 需要绕过 Laravel 的 CSRF 保护,因此您应确保 Laravel 不会尝试验证传入的 Paddle Webhook 的 CSRF 令牌。为此,您应该在应用程序的 bootstrap/app.php 文件中从 CSRF 保护中排除 paddle/*。
1->withMiddleware(function (Middleware $middleware): void {2 $middleware->preventRequestForgery(except: [3 'paddle/*',4 ]);5})
Webhook 与本地开发
为了让 Paddle 能够在本地开发期间向您的应用程序发送 Webhook,您需要通过站点共享服务(例如 Ngrok 或 Expose)公开您的应用程序。如果您使用 Laravel Sail 本地开发应用程序,则可以使用 Sail 的 站点共享命令。
定义 Webhook 事件处理器
Cashier 会自动处理扣费失败时的订阅取消以及其他常见的 Paddle Webhook。但是,如果您还有其他想要处理的 Webhook 事件,可以通过监听由 Cashier 分派的以下事件来完成:
Laravel\Paddle\Events\WebhookReceivedLaravel\Paddle\Events\WebhookHandled
这两个事件都包含 Paddle Webhook 的完整负载。例如,如果您希望处理 transaction.billed Webhook,可以注册一个 监听器 来处理该事件。
1<?php 2 3namespace App\Listeners; 4 5use Laravel\Paddle\Events\WebhookReceived; 6 7class PaddleEventListener 8{ 9 /**10 * Handle received Paddle webhooks.11 */12 public function handle(WebhookReceived $event): void13 {14 if ($event->payload['event_type'] === 'transaction.billed') {15 // Handle the incoming event...16 }17 }18}
Cashier 还发出专用于所接收 Webhook 类型的事件。除了来自 Paddle 的完整负载外,它们还包含用于处理 Webhook 的相关模型,例如计费模型、订阅或收据。
Laravel\Paddle\Events\CustomerUpdatedLaravel\Paddle\Events\TransactionCompletedLaravel\Paddle\Events\TransactionUpdatedLaravel\Paddle\Events\SubscriptionCreatedLaravel\Paddle\Events\SubscriptionUpdatedLaravel\Paddle\Events\SubscriptionPausedLaravel\Paddle\Events\SubscriptionCanceled
您还可以通过在应用程序的 .env 文件中定义 CASHIER_WEBHOOK 环境变量来覆盖默认的内置 Webhook 路由。此值应为指向 Webhook 路由的完整 URL,并且必须与在 Paddle 控制面板中设置的 URL 相匹配。
1CASHIER_WEBHOOK=https://example.com/my-paddle-webhook-url
验证 Webhook 签名
要保护您的 Webhook,您可以使用 Paddle 的 Webhook 签名。为方便起见,Cashier 自动包含了一个中间件,用于验证传入的 Paddle Webhook 请求是否有效。
要启用 Webhook 验证,请确保应用程序的 .env 文件中定义了 PADDLE_WEBHOOK_SECRET 环境变量。Webhook 密钥可以从您的 Paddle 账户仪表板中获取。
单次扣费
产品扣费
如果您想为客户发起产品购买,可以使用计费模型实例上的 checkout 方法为该购买生成结账会话。checkout 方法接受一个或多个价格 ID。如有必要,可以使用关联数组来提供所购买产品的数量。
1use Illuminate\Http\Request;2 3Route::get('/buy', function (Request $request) {4 $checkout = $request->user()->checkout(['pri_tshirt', 'pri_socks' => 5]);5 6 return view('buy', ['checkout' => $checkout]);7});
生成结账会话后,您可以使用 Cashier 提供的 paddle-button Blade 组件,让用户查看 Paddle 结账小部件并完成购买。
1<x-paddle-button :checkout="$checkout" class="px-8 py-4">2 Buy3</x-paddle-button>
结账会话具有 customData 方法,允许您将任何您希望的自定义数据传递给底层的交易创建过程。请查阅 Paddle 文档 以了解有关传递自定义数据时可用选项的更多信息。
1$checkout = $user->checkout('pri_tshirt')2 ->customData([3 'custom_option' => $value,4 ]);
退款交易
退款交易将把退款金额退还到购买时客户使用的支付方式。如果您需要退款 Paddle 购买,可以在 Cashier\Paddle\Transaction 模型上使用 refund 方法。此方法接受原因作为第一个参数,以及一个或多个要退款的价格 ID(带有可选的金额,作为关联数组)。您可以使用 transactions 方法检索给定计费模型的交易。
例如,假设我们要为价格 pri_123 和 pri_456 退还特定交易。我们想全额退还 pri_123,但只为 pri_456 退款两美元。
1use App\Models\User; 2 3$user = User::find(1); 4 5$transaction = $user->transactions()->first(); 6 7$response = $transaction->refund('Accidental charge', [ 8 'pri_123', // Fully refund this price... 9 'pri_456' => 200, // Only partially refund this price...10]);
上面的示例退还了交易中的特定行项目。如果您想退还整笔交易,只需提供原因即可。
1$response = $transaction->refund('Accidental charge');
有关退款的更多信息,请查阅 Paddle 的退款文档。
退款必须始终在完全处理前由 Paddle 批准。
贷记交易
就像退款一样,您也可以对交易进行贷记。贷记交易会将资金添加到客户的余额中,以便可用于以后的购买。贷记交易只能针对手动收集的交易进行,不能针对自动收集的交易(如订阅)进行,因为 Paddle 会自动处理订阅贷记。
1$transaction = $user->transactions()->first();2 3// Credit a specific line item fully...4$response = $transaction->credit('Compensation', 'pri_123');
更多信息,请 参阅 Paddle 关于贷记的文档。
贷记只能应用于手动收集的交易。自动收集的交易由 Paddle 自己进行贷记。
交易记录
您可以通过 transactions 属性轻松检索计费模型交易的数组。
1use App\Models\User;2 3$user = User::find(1);4 5$transactions = $user->transactions;
交易代表您的产品和购买的付款,并附有发票。只有已完成的交易才会存储在您的应用程序数据库中。
列出客户的交易时,您可以使用交易实例的方法来显示相关的支付信息。例如,您可能希望在一个表格中列出每笔交易,以便用户轻松下载任何发票。
1<table> 2 @foreach ($transactions as $transaction) 3 <tr> 4 <td>{{ $transaction->billed_at->toFormattedDateString() }}</td> 5 <td>{{ $transaction->total() }}</td> 6 <td>{{ $transaction->tax() }}</td> 7 <td><a href="{{ route('download-invoice', $transaction->id) }}" target="_blank">Download</a></td> 8 </tr> 9 @endforeach10</table>
download-invoice 路由可能如下所示:
1use Illuminate\Http\Request;2use Laravel\Paddle\Transaction;3 4Route::get('/download-invoice/{transaction}', function (Request $request, Transaction $transaction) {5 return $transaction->redirectToInvoicePdf();6})->name('download-invoice');
往期及即将进行的付款
您可以使用 lastPayment 和 nextPayment 方法来检索和显示客户往期或即将进行的定期订阅付款。
1use App\Models\User;2 3$user = User::find(1);4 5$subscription = $user->subscription();6 7$lastPayment = $subscription->lastPayment();8$nextPayment = $subscription->nextPayment();
这两个方法都将返回 Laravel\Paddle\Payment 的一个实例;但是,当交易尚未通过 Webhook 同步时,lastPayment 将返回 null,而当计费周期结束(例如订阅已取消)时,nextPayment 将返回 null。
1Next payment: {{ $nextPayment->amount() }} due on {{ $nextPayment->date()->format('d/m/Y') }}
测试
测试时,您应该手动测试您的计费流程,以确保您的集成按预期工作。
对于自动化测试(包括在 CI 环境中执行的测试),您可以使用 Laravel 的 HTTP 客户端 来伪造对 Paddle 发出的 HTTP 调用。虽然这不会测试来自 Paddle 的实际响应,但它提供了一种在不实际调用 Paddle API 的情况下测试应用程序的方法。