Laravel Cashiers (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
接下来,您应该使用 Artisan 命令发布 Cashier 迁移文件vendor:publish
:
1php artisan vendor:publish --tag="cashier-migrations"
然后,您应该运行应用程序的数据库迁移。Cashier 迁移将创建一个新customers
表。此外,还将创建新subscriptions
表subscription_items
来存储所有客户的订阅。最后,transactions
将创建一个新表来存储与客户相关的所有 PaddlePaddle 交易:
1php artisan migrate
为了确保 Cashier 正确处理所有 Paddle 事件,请记住设置 Cashier 的 webhook 处理。
Paddles沙盒
在本地开发阶段,您需要注册一个 Paddle Sandbox 账户。该账户将为您提供一个沙盒环境,用于测试和开发您的应用程序,而无需进行实际支付。您可以使用 Paddle 的测试卡号来模拟各种支付场景。
使用 Paddle Sandbox 环境时,您应该在应用程序的文件中设置PADDLE_SANDBOX
环境变量:true
.env
1PADDLE_SANDBOX=true
应用程序开发完成后,您可以申请一个 PaddlePaddle 供应商账户。在应用程序投入生产之前,PaddlePaddle 需要批准您应用程序的域名。
配置
计费模型
在使用 Cashier 之前,你必须先将Billable
trait 添加到你的用户模型定义中。此 trait 提供了多种方法,允许你执行常见的计费任务,例如创建订阅和更新支付方式信息:
1use Laravel\Paddle\Billable;2 3class User extends Authenticatable4{5 use Billable;6}
如果您有非用户的可计费实体,您也可以将特征添加到这些类中:
1use Illuminate\Database\Eloquent\Model;2use Laravel\Paddle\Billable;3 4class Team extends Model5{6 use Billable;7}
API 密钥
接下来,您应该在应用程序文件中配置 PaddlePaddle 密钥.env
。您可以从 PaddlePaddle 控制面板检索 PaddlePaddle 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
变量应设置为。如果您将应用程序部署到生产环境并使用 Paddle 的在线供应商环境,则应将环境变量设置为。true
PADDLE_SANDBOX
false
这PADDLE_RETAIN_KEY
是可选的,并且仅当您使用带有Retain 的Paddle 时才需要设置。
PaddleJS
Paddle 依靠其自身的 JavaScript 库来启动 Paddle 结账小部件。您可以通过在@paddleJS
应用程序布局的结束</head>
标记之前放置 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}
定义好模型后,你可以通过类来指示 Cashier 使用你的自定义模型。通常,你应该在应用类的方法Laravel\Paddle\Cashier
中告知 Cashier 你的自定义模型:boot
App\Providers\AppServiceProvider
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 Checkout 之前,您需要在 Paddle 仪表盘中定义固定价格的产品。此外,您还需要配置 Paddle 的 webhook 处理。
通过您的应用程序提供产品和订阅计费功能可能会令人望而生畏。但是,借助 Cashier 和Paddle 的 Checkout Overlay,您可以轻松构建现代、强大的支付集成。
为了向客户收取非循环、单次付费产品的款项,我们将利用 Cashier 通过 Paddle 的 Checkout Overlay 向客户收费,客户需提供付款详情并确认购买。通过 Checkout Overlay 付款后,客户将被重定向到您在应用程序中指定的成功 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 Checkout 界面。在使用 Paddle 时,“价格”指的是特定产品的指定价格。
如有必要,该checkout
方法将自动在 Paddle 中创建客户,并将该 Paddle 客户记录连接到应用程序数据库中的相应用户。完成结账会话后,客户将被重定向到专用的成功页面,您可以在该页面向客户显示一条信息消息。
在buy
视图中,我们将添加一个按钮来显示结账叠加层。Bladepaddle-button
组件已包含在 Cashier Paddle 中;不过,您也可以手动渲染一个叠加结账层:
1<x-paddle-button :checkout="$checkout" class="px-8 py-4">2 Buy Product3</x-paddle-button>
向 Paddle Checkout 提供元数据
Cart
在销售产品时,通常会通过您自己应用定义的模型来跟踪已完成的订单和已购买的产品Order
。当将客户重定向到 Paddle 的 Checkout Overlay 完成购买时,您可能需要提供一个现有的订单标识符,以便在客户重定向回您的应用时,将已完成的购买与相应的订单关联起来。
为此,您可以向该checkout
方法提供一个自定义数据数组。假设Order
当用户开始结账Processes时,我们的应用中会创建一个 pending 对象。请注意,本例中的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');
如上例所示,当用户开始结账Processes时,我们会将所有与购物车/订单相关的 PaddlePaddle 价格标识符提供给该checkout
方法。当然,您的应用负责在客户添加这些商品时,将这些商品与“购物车”或订单关联起来。我们还会通过该方法将订单 ID 提供给 PaddlePaddle 结账覆盖层customData
。
当然,您可能希望在客户完成结账Processes后将订单标记为“完成”。为此,您可以监听由 PaddlePaddle 调度并由 Cashier 触发的 webhook,将订单信息存储在数据库中。
首先,监听Cashier 派发的事件。通常,你应该在应用程序的以下方法TransactionCompleted
中注册事件监听器:boot
AppServiceProvider
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 Checkout 之前,您需要在 Paddle 仪表盘中定义固定价格的产品。此外,您还需要配置 Paddle 的 webhook 处理。
通过您的应用程序提供产品和订阅计费功能可能会令人望而生畏。但是,借助 Cashier 和Paddle 的 Checkout Overlay,您可以轻松构建现代、强大的支付集成。
要了解如何使用 Cashier 和 Paddle 的 Checkout Overlay 销售订阅产品,我们先来设想一个简单的订阅服务场景,该服务包含一个基本的月费 ( price_basic_monthly
) 和年费 ( price_basic_yearly
) Packages。这两个价格可以在 Paddle 控制面板中归类到“基本”产品 ( pro_basic
) 下。此外,我们的订阅服务还可以提供专家Packages,具体价格如下pro_expert
。
首先,让我们了解一下客户如何订阅我们的服务。当然,您可以想象客户可能会点击我们应用程序定价页面上的“订阅”按钮,选择“基础版”。此按钮将根据所选的Packages调用 Paddle Checkout Overlay。首先,让我们通过以下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
视图中,我们将添加一个按钮来显示结账叠加层。Bladepaddle-button
组件已包含在 Cashier Paddle 中;不过,您也可以手动渲染一个叠加结账层:
1<x-paddle-button :checkout="$checkout" class="px-8 py-4">2 Subscribe3</x-paddle-button>
现在,当点击“订阅”按钮时,客户将能够输入他们的付款详情并启动订阅。为了知道他们的订阅何时真正开始(因为某些付款方式需要几秒钟才能处理),您还应该配置 Cashier 的 webhook 处理。
subscribed
现在用户可以开始订阅了,我们需要限制应用程序的某些部分,以便只有订阅的用户才能访问它们。当然,我们可以通过Cashier trait 提供的方法来确定用户当前的订阅状态Billable
:
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 就会通过检查来自 PaddlePaddle 的传入 webhook,自动同步您应用中与 Cashier 相关的数据库表。例如,当您通过 PaddlePaddle 的仪表盘取消客户的订阅时,Cashier 会收到相应的 webhook,并在您应用的数据库中将该订阅标记为“已取消”。
结帐会话
大多数向客户收费的操作都是通过 Paddle 的Checkout Overlay 小部件使用“结账”或利用内联结账来完成的。
在使用 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 组件。你可以将结账 session 作为 prop 传递给此组件。这样,当点击此按钮时,就会显示 PaddlePaddle 的结账小部件:
1<x-paddle-button :checkout="$checkout" class="px-8 py-4">2 Subscribe3</x-paddle-button>
默认情况下,这将使用 PaddlePaddle 的默认样式显示窗口小部件。您可以通过向组件添加PaddlePaddle 支持的属性(例如以下 属性)来自定义窗口小部件: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 组件。
价格预览
PaddlePaddle 允许您自定义每种货币的价格,本质上就是允许您为不同国家/地区配置不同的价格。CashierPaddle 允许您使用以下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
方法时,您可以通过以下选项提供折扣 ID discount_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 允许您在创建结账会话时为客户定义一些有用的默认值。设置这些默认值允许您预填客户的电子邮件地址和姓名,以便他们可以立即转到结账小部件的付款部分。您可以通过在 Billable 模型中重写以下方法来设置这些默认值:
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 中生成结帐会话的每个操作。
检索客户
您可以使用该方法通过 PaddlePaddle 客户 ID 检索客户Cashier::findBillable
。此方法将返回一个可计费模型的实例:
1use Laravel\Paddle\Cashier;2 3$user = Cashier::findBillable($customerId);
创建客户
有时,您可能希望创建 Paddle 客户而不开始订阅。您可以使用以下createAsCustomer
方法实现此目的:
1$customer = $user->createAsCustomer();
返回一个 实例Laravel\Paddle\Customer
。在 PaddlePaddle 中创建客户后,您可以稍后开始订阅。您可以提供一个可选数组,用于传入 PaddlePaddle API 支持的$options
任何其他客户创建参数:
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
是用户订阅的具体价格。此值应与 PaddlePaddle 中的价格标识符相对应。该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>
用户完成结账后,subscription_created
PaddlePaddle 会发送一个 webhook。Cashiers会收到此 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 判断用户是否订阅了指定Packages。在本例中,我们将判断用户的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
中调用:register
AppServiceProvider
1use Laravel\Paddle\Cashier;2 3/**4 * Register any application services.5 */6public function register(): void7{8 Cashier::keepPastDueSubscriptionsActive();9}
当订阅处于某种past_due
状态时,除非付款信息已更新,否则无法更改该状态。因此,当订阅处于某种状态时,swap
和方法将抛出异常。updateQuantity
past_due
订阅范围
大多数订阅状态也可作为查询范围使用,以便您可以轻松地在数据库中查询处于给定状态的订阅:
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');
更新付款信息
redirectToUpdatePaymentMethod
PaddlePaddle 始终为每个订阅保存一个付款方式。如果您要更新订阅的默认付款方式,则应使用订阅模型中的方法将客户重定向到 PaddlePaddle 托管的付款方式更新页面:
1use Illuminate\Http\Request;2 3Route::get('/update-payment-method', function (Request $request) {4 $user = $request->user();5 6 return $user->subscription()->redirectToUpdatePaymentMethod();7});
当用户完成更新其信息时,subscription_updated
Paddle 将发送一个 webhook,并且订阅详细信息将更新到应用程序的数据库中。
改变计划
用户订阅您的应用后,可能偶尔会想要更换新的订阅方案。要更新用户的订阅方案,您应该将 PaddlePaddle 价格的标识符传递给订阅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');
按比例分配
默认情况下,PaddlePaddle 在切换Packages时会按比例收费。您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 允许您的客户同时订阅多个Packages。例如,您可能经营一家健身房,提供游泳Packages和举重Packages,每个Packages的价格可能不同。当然,客户应该可以同时订阅其中一个Packages或两个Packages。
当你的应用创建订阅时,你可以将订阅的类型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
方法将继续返回false
到 3 月 5 日。这是因为用户通常被允许继续使用应用程序直到其结算周期结束。
默认情况下,暂停发生在下一个计费间隔,以便客户可以使用其已支付的剩余订阅期。如果您想立即暂停订阅,可以使用以下pauseNow
方法:
1$user->subscription()->pauseNow();
使用该pauseUntil
方法,您可以暂停订阅直到特定的时间点:
1$user->subscription()->pauseUntil(now()->addMonth());
或者,您可以使用该pauseNowUntil
方法立即暂停订阅,直到给定的时间点:
1$user->subscription()->pauseNowUntil(now()->addMonth());
您可以使用该方法确定用户是否已暂停订阅但仍处于“宽限期” onPausedGracePeriod
:
1if ($user->subscription()->onPausedGracePeriod()) {2 // ...3}
要恢复暂停的订阅,您可以调用resume
订阅上的方法:
1$user->subscription()->resume();
订阅暂停期间无法修改。如果您想更换Packages或更新订阅数量,必须先恢复订阅。
取消订阅
要取消订阅,请调用cancel
用户订阅上的方法:
1$user->subscription()->cancel();
当订阅被取消时,Cashier 会自动设置ends_at
数据库中的相应列。此列用于确定该subscribed
方法何时开始返回false
。例如,如果客户在 3 月 1 日取消了订阅,但订阅预计在 3 月 5 日才结束,则该subscribed
方法将继续返回true
到 3 月 5 日。这样做是因为用户通常被允许继续使用应用程序直到其计费周期结束。
您可以使用该方法确定用户是否已取消订阅但仍处于“宽限期” onGracePeriod
:
1if ($user->subscription()->onGracePeriod()) {2 // ...3}
如果您希望立即取消订阅,您可以调用cancelNow
订阅上的方法:
1$user->subscription()->cancelNow();
要阻止宽限期内的订阅取消,您可以调用该stopCancelation
方法:
1$user->subscription()->stopCancelation();
Paddle 的订阅一旦取消便无法恢复。如果您的客户希望恢复订阅,则必须创建新的订阅。
订阅试用
预付付款方式
如果您想为客户提供试用期,同时仍预先收集付款方式信息,您应该在 Paddle 控制面板中根据客户订阅的价格设置试用时间。然后,照常启动结帐Processes:
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()->addDays(10)9]);
Cashier 将这种试用类型称为“通用试用”,因为它不附加到任何现有订阅。如果当前日期不晚于 的值,则实例onTrial
上的方法User
将返回:true
trial_ends_at
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()->addDays(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 的完整列表如下:
- 客户更新
- 交易完成
- 交易已更新
- 订阅已创建
- 订阅已更新
- 订阅已暂停
- 订阅已取消
确保使用 Cashier 包含的webhook 签名验证中间件保护传入的请求。
Webhook 和 CSRF 保护
由于 Paddle webhook 需要绕过 Laravel 的CSRF 保护,因此你应该确保 Laravel 不会尝试验证传入 Paddle webhook 的 CSRF 令牌。为此,你应该paddle/*
在应用程序的bootstrap/app.php
文件中将以下内容排除在 CSRF 保护之外:
1->withMiddleware(function (Middleware $middleware) {2 $middleware->validateCsrfTokens(except: [3 'paddle/*',4 ]);5})
Webhook 和本地开发
为了使 PaddlePaddle 能够在本地开发期间发送你的应用程序 webhook,你需要通过站点共享服务(例如Ngrok或Expose )公开你的应用程序。如果你使用Laravel Sail本地开发应用程序,则可以使用 Sail 的站点共享命令。
定义 Webhook 事件处理程序
Cashier 会自动处理因扣款失败和其他常见的 Paddle webhook 事件而导致的订阅取消。如果您还有其他 webhook 事件需要处理,可以通过监听 Cashier 派发的以下事件来实现:
Laravel\Paddle\Events\WebhookReceived
Laravel\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\CustomerUpdated
Laravel\Paddle\Events\TransactionCompleted
Laravel\Paddle\Events\TransactionUpdated
Laravel\Paddle\Events\SubscriptionCreated
Laravel\Paddle\Events\SubscriptionUpdated
Laravel\Paddle\Events\SubscriptionPaused
Laravel\Paddle\Events\SubscriptionCanceled
CASHIER_WEBHOOK
您还可以通过在应用程序文件中定义环境变量来覆盖默认的内置 webhook 路由.env
。此值应为 webhook 路由的完整 URL,并且需要与 Paddle 控制面板中设置的 URL 匹配:
1CASHIER_WEBHOOK=https://example.com/my-paddle-webhook-url
验证 Webhook 签名
为了保护你的 webhook,你可以使用Paddle 的 webhook 签名。为了方便起见,Cashier 自动包含一个中间件,用于验证传入的 Paddle webhook 请求是否有效。
要启用 webhook 验证,请确保PADDLE_WEBHOOK_SECRET
在应用程序.env
文件中定义了环境变量。您可以从 Paddle 帐户仪表板中检索 webhook 密钥。
单次收费
产品收费
如果您想为客户发起产品购买,您可以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 ]);
退款交易
退款交易会将退款金额退回到客户购买时使用的付款方式。如果您需要对 PaddlePaddle 购买进行退款,您可以refund
在模型上使用该方法Cashier\Paddle\Transaction
。该方法接受退款原因作为第一个参数,以及一个或多个要退款的价格 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 的信用文档。
积分仅适用于手动收款的交易。自动收款的交易将由 PaddlePaddle 自行记入。
交易
您可以通过以下属性轻松检索可计费模型的交易数组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') }}
测试
在测试时,您应该手动测试您的计费Processes,以确保您的集成按预期工作。
对于自动化测试(包括在 CI 环境中执行的测试),您可以使用Laravel 的 HTTP 客户端模拟对 PaddlePaddle 的 HTTP 调用。虽然这不会测试 PaddlePaddle 的实际响应,但它提供了一种无需实际调用 PaddlePaddle API 即可测试应用程序的方法。