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 包:
composer require laravel/cashier-paddle
接下来,您应该使用 vendor:publish
Artisan 命令发布 Cashier 迁移文件:
php artisan vendor:publish --tag="cashier-migrations"
然后,您应该运行应用程序的数据库迁移。Cashier 迁移将创建一个新的 customers
表。此外,还将创建新的 subscriptions
和 subscription_items
表来存储所有客户的订阅。最后,将创建一个新的 transactions
表来存储与客户相关的所有 Paddle 交易:
php artisan migrate
为确保 Cashier 正确处理所有 Paddle 事件,请记得 设置 Cashier 的 webhook 处理。
Paddle 沙盒
在本地和暂存开发期间,您应该 注册一个 Paddle 沙盒账户。此账户将为您提供一个沙盒环境,以便在不进行实际支付的情况下测试和开发应用程序。您可以使用 Paddle 的 测试卡号 来模拟各种支付场景。
使用 Paddle 沙盒环境时,您应该在应用程序的 .env
文件中将 PADDLE_SANDBOX
环境变量设置为 true
:
PADDLE_SANDBOX=true
在完成应用程序开发后,您可以 申请一个 Paddle 供应商账户。在将应用程序投入生产之前,Paddle 需要批准您的应用程序域。
配置
可计费模型
在使用 Cashier 之前,您必须将 Billable
trait 添加到用户模型定义中。此 trait 提供了各种方法,允许您执行常见的计费任务,例如创建订阅和更新支付方式信息:
use Laravel\Paddle\Billable;
class User extends Authenticatable
{
use Billable;
}
如果您有可计费的实体不是用户,您也可以将 trait 添加到这些类中:
use Illuminate\Database\Eloquent\Model;
use Laravel\Paddle\Billable;
class Team extends Model
{
use Billable;
}
API 密钥
接下来,您应该在应用程序的 .env
文件中配置您的 Paddle 密钥。您可以从 Paddle 控制面板中检索您的 Paddle API 密钥:
PADDLE_CLIENT_SIDE_TOKEN=your-paddle-client-side-token
PADDLE_API_KEY=your-paddle-api-key
PADDLE_RETAIN_KEY=your-paddle-retain-key
PADDLE_WEBHOOK_SECRET="your-paddle-webhook-secret"
PADDLE_SANDBOX=true
当您使用 Paddle 的沙盒环境 时,PADDLE_SANDBOX
环境变量应设置为 true
。如果您将应用程序部署到生产环境并使用 Paddle 的实时供应商环境,则应将 PADDLE_SANDBOX
变量设置为 false
。
PADDLE_RETAIN_KEY
是可选的,仅在您使用 Paddle 和 Retain 时设置。
Paddle JS
Paddle 依赖于其自己的 JavaScript 库来启动 Paddle 结账小部件。您可以通过在应用程序布局的关闭 </head>
标签之前放置 @paddleJS
Blade 指令来加载 JavaScript 库:
<head>
...
@paddleJS
</head>
货币配置
您可以指定一个区域设置,用于在发票上显示货币值的格式化。内部,Cashier 使用 PHP 的 NumberFormatter
类 类来设置货币区域设置:
CASHIER_CURRENCY_LOCALE=nl_BE
为了使用 en
以外的区域设置,请确保在服务器上安装并配置 ext-intl
PHP 扩展。
覆盖默认模型
您可以通过定义自己的模型并扩展相应的 Cashier 模型来自由扩展 Cashier 内部使用的模型:
use Laravel\Paddle\Subscription as CashierSubscription;
class Subscription extends CashierSubscription
{
// ...
}
定义模型后,您可以通过 Laravel\Paddle\Cashier
类指示 Cashier 使用您的自定义模型。通常,您应该在应用程序的 App\Providers\AppServiceProvider
类的 boot
方法中通知 Cashier 您的自定义模型:
use App\Models\Cashier\Subscription;
use App\Models\Cashier\Transaction;
/**
* 启动任何应用程序服务。
*/
public function boot(): void
{
Cashier::useSubscriptionModel(Subscription::class);
Cashier::useTransactionModel(Transaction::class);
}
快速入门
销售产品
在使用 Paddle Checkout 之前,您应该在 Paddle 仪表板中定义具有固定价格的产品。此外,您应该 配置 Paddle 的 webhook 处理。
通过您的应用程序提供产品和订阅计费可能会让人望而生畏。然而,感谢 Cashier 和 Paddle 的 Checkout Overlay,您可以轻松构建现代、强大的支付集成。
要向客户收费非经常性、单次收费的产品,我们将利用 Cashier 向客户收费,使用 Paddle 的 Checkout Overlay,在那里他们将提供支付详细信息并确认购买。一旦通过 Checkout Overlay 完成支付,客户将被重定向到您应用程序中您选择的成功 URL:
use Illuminate\Http\Request;
Route::get('/buy', function (Request $request) {
$checkout = $request->user()->checkout('pri_deluxe_album')
->returnTo(route('dashboard'));
return view('buy', ['checkout' => $checkout]);
})->name('checkout');
如上例所示,我们将利用 Cashier 提供的 checkout
方法为给定的“价格标识符”创建一个结账对象,以向客户展示 Paddle Checkout Overlay。使用 Paddle 时,“价格”指的是 特定产品的定义价格。
如果需要,checkout
方法将自动在 Paddle 中创建一个客户,并将该 Paddle 客户记录连接到应用程序数据库中相应的用户。完成结账会话后,客户将被重定向到一个专用的成功页面,您可以在该页面上向客户显示信息性消息。
在 buy
视图中,我们将包括一个按钮来显示 Checkout Overlay。paddle-button
Blade 组件包含在 Cashier Paddle 中;不过,您也可以 手动渲染覆盖结账:
<x-paddle-button :checkout="$checkout" class="px-8 py-4">
购买产品
</x-paddle-button>
向 Paddle Checkout 提供元数据
销售产品时,通常会通过您自己的应用程序定义的 Cart
和 Order
模型来跟踪已完成的订单和已购买的产品。当将客户重定向到 Paddle 的 Checkout Overlay 以完成购买时,您可能需要提供现有的订单标识符,以便在客户被重定向回应用程序时将已完成的购买与相应的订单关联。
为此,您可以向 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',
]);
$checkout = $request->user()->checkout($order->price_ids)
->customData(['order_id' => $order->id]);
return view('billing', ['checkout' => $checkout]);
})->name('checkout');
如上例所示,当用户开始结账过程时,我们将提供所有购物车/订单的关联 Paddle 价格标识符给 checkout
方法。当然,您的应用程序负责在客户添加这些项目时将其与“购物车”或订单关联。我们还通过 customData
方法将订单的 ID 提供给 Paddle Checkout Overlay。
当然,您可能希望在客户完成结账过程后将订单标记为“已完成”。为此,您可以监听 Paddle 发送的 webhooks,并通过 Cashier 触发的事件将订单信息存储在数据库中。
要开始,请监听 Cashier 发送的 TransactionCompleted
事件。通常,您应该在应用程序服务提供者的 boot
方法中注册事件监听器:
use App\Listeners\CompleteOrder;
use Illuminate\Support\Facades\Event;
use Laravel\Paddle\Events\TransactionCompleted;
/**
* 启动任何应用程序服务。
*/
public function boot(): void
{
Event::listen(TransactionCompleted::class, CompleteOrder::class);
}
在此示例中,CompleteOrder
监听器可能如下所示:
namespace App\Listeners;
use App\Models\Order;
use Laravel\Cashier\Cashier;
use Laravel\Cashier\Events\TransactionCompleted;
class CompleteOrder
{
/**
* 处理传入的 Cashier webhook 事件。
*/
public function handle(TransactionCompleted $event): void
{
$orderId = $event->payload['data']['custom_data']['order_id'] ?? null;
$order = Order::findOrFail($orderId);
$order->update(['status' => 'completed']);
}
}
请参考 Paddle 的文档以获取有关 transaction.completed
事件中包含的数据的更多信息。
销售订阅
在使用 Paddle Checkout 之前,您应该在 Paddle 仪表板中定义具有固定价格的产品。此外,您应该 配置 Paddle 的 webhook 处理。
通过您的应用程序提供产品和订阅计费可能会让人望而生畏。然而,感谢 Cashier 和 Paddle 的 Checkout Overlay,您可以轻松构建现代、强大的支付集成。
要了解如何使用 Cashier 和 Paddle 的 Checkout Overlay 销售订阅,让我们考虑一个简单的订阅服务场景,其中包含一个基本的每月(price_basic_monthly
)和每年(price_basic_yearly
)计划。这两个价格可以在我们的 Paddle 仪表板中分组为“Basic”产品(pro_basic
)。此外,我们的订阅服务可能会提供一个 Expert 计划作为 pro_expert
。
首先,让我们了解客户如何订阅我们的服务。当然,您可以想象客户可能会在我们应用程序的定价页面上点击“订阅”按钮。此按钮将为他们选择的计划调用 Paddle Checkout Overlay。要开始,请通过 checkout
方法启动结账会话:
use Illuminate\Http\Request;
Route::get('/subscribe', function (Request $request) {
$checkout = $request->user()->checkout('price_basic_monthly')
->returnTo(route('dashboard'));
return view('subscribe', ['checkout' => $checkout]);
})->name('subscribe');
在 subscribe
视图中,我们将包括一个按钮来显示 Checkout Overlay。paddle-button
Blade 组件包含在 Cashier Paddle 中;不过,您也可以 手动渲染覆盖结账:
<x-paddle-button :checkout="$checkout" class="px-8 py-4">
订阅
</x-paddle-button>
现在,当点击订阅按钮时,客户将能够输入他们的支付详细信息并启动他们的订阅。要知道他们的订阅何时实际开始(因为某些支付方式需要几秒钟来处理),您还应该 配置 Cashier 的 webhook 处理。
现在客户可以开始订阅了,我们需要限制应用程序的某些部分,以便只有订阅用户可以访问它们。当然,我们可以始终通过 Cashier 的 Billable
trait 提供的 subscribed
方法来确定用户的当前订阅状态:
@if ($user->subscribed())
<p>您已订阅。</p>
@endif
我们甚至可以轻松确定用户是否订阅了特定产品或价格:
@if ($user->subscribedToProduct('pro_basic'))
<p>您已订阅我们的 Basic 产品。</p>
@endif
@if ($user->subscribedToPrice('price_basic_monthly'))
<p>您已订阅我们的每月 Basic 计划。</p>
@endif
构建订阅中间件
为了方便起见,您可能希望创建一个 中间件,用于确定传入请求是否来自订阅用户。定义此中间件后,您可以轻松地将其分配给路由,以防止未订阅的用户访问该路由:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class Subscribed
{
/**
* 处理传入请求。
*/
public function handle(Request $request, Closure $next): Response
{
if (! $request->user()?->subscribed()) {
// 重定向用户到计费页面并要求他们订阅...
return redirect('/subscribe');
}
return $next($request);
}
}
定义中间件后,您可以将其分配给路由:
use App\Http\Middleware\Subscribed;
Route::get('/dashboard', function () {
// ...
})->middleware([Subscribed::class]);
允许客户管理他们的计费计划
当然,客户可能希望将他们的订阅计划更改为其他产品或“层级”。在我们上面的示例中,我们希望允许客户将他们的计划从每月订阅更改为年度订阅。为此,您需要实现类似于一个按钮的功能,该按钮指向以下路由:
use Illuminate\Http\Request;
Route::put('/subscription/{price}/swap', function (Request $request, $price) {
$user->subscription()->swap($price); // 在此示例中,"$price" 为 "price_basic_yearly"。
return redirect()->route('dashboard');
})->name('subscription.swap');
除了交换计划,您还需要允许客户取消他们的订阅。与交换计划一样,提供一个按钮,该按钮指向以下路由:
use Illuminate\Http\Request;
Route::put('/subscription/cancel', function (Request $request, $price) {
$user->subscription()->cancel();
return redirect()->route('dashboard');
})->name('subscription.cancel');
现在您的订阅将在其计费周期结束时取消。
只要您已配置 Cashier 的 webhook 处理,Cashier 将通过检查来自 Paddle 的传入 webhooks 自动保持应用程序的 Cashier 相关数据库表同步。因此,例如,当您通过 Paddle 的仪表板取消客户的订阅时,Cashier 将接收相应的 webhook 并在应用程序的数据库中将订阅标记为“已取消”。
结账会话
大多数向客户收费的操作都是通过 Paddle 的 Checkout Overlay 小部件 或通过利用 内联结账 执行的。
在使用 Paddle 处理结账付款之前,您应该在 Paddle 结账设置仪表板中定义应用程序的 默认付款链接。
覆盖结账
在显示 Checkout Overlay 小部件之前,您必须使用 Cashier 生成一个结账会话。结账会话将通知结账小部件应执行的计费操作:
use Illuminate\Http\Request;
Route::get('/buy', function (Request $request) {
$checkout = $user->checkout('pri_34567')
->returnTo(route('dashboard'));
return view('billing', ['checkout' => $checkout]);
});
Cashier 包含一个 paddle-button
Blade 组件。您可以将结账会话作为“prop”传递给此组件。然后,当此按钮被点击时,Paddle 的结账小部件将被显示:
<x-paddle-button :checkout="$checkout" class="px-8 py-4">
订阅
</x-paddle-button>
默认情况下,这将使用 Paddle 的默认样式显示小部件。您可以通过添加 Paddle 支持的属性 来自定义小部件,例如 data-theme='light'
属性到组件:
<x-paddle-button :url="$payLink" class="px-8 py-4" data-theme="light">
订阅
</x-paddle-button>
Paddle 结账小部件是异步的。一旦用户在小部件中创建了订阅,Paddle 将向您的应用程序发送一个 webhook,以便您可以正确更新应用程序数据库中的订阅状态。因此,重要的是您正确 设置 webhooks 以适应来自 Paddle 的状态更改。
在订阅状态更改后,接收相应 webhook 的延迟通常是最小的,但您应该在应用程序中考虑到这一点,因为用户的订阅可能在完成结账后不会立即可用。
手动渲染覆盖结账
您也可以在不使用 Laravel 内置 Blade 组件的情况下手动渲染覆盖结账。要开始,请 生成结账会话:
use Illuminate\Http\Request;
Route::get('/buy', function (Request $request) {
$checkout = $user->checkout('pri_34567')
->returnTo(route('dashboard'));
return view('billing', ['checkout' => $checkout]);
});
接下来,您可以使用 Paddle.js 初始化结账。在此示例中,我们将创建一个分配了 paddle_button
类的链接。Paddle.js 将检测到此类,并在点击链接时显示覆盖结账:
<?php
$items = $checkout->getItems();
$customer = $checkout->getCustomer();
$custom = $checkout->getCustomData();
?>
<a
href='#!'
class='paddle_button'
data-items='{!! json_encode($items) !!}'
@if ($customer) data-customer-id='{{ $customer->paddle_id }}' @endif
@if ($custom) data-custom-data='{{ json_encode($custom) }}' @endif
@if ($returnUrl = $checkout->getReturnUrl()) data-success-url='{{ $returnUrl }}' @endif
>
购买产品
</a>
内联结账
如果您不想使用 Paddle 的“覆盖”样式结账小部件,Paddle 还提供了在应用程序中内嵌显示小部件的选项。虽然这种方法不允许您调整结账的任何 HTML 字段,但它允许您将小部件嵌入到应用程序中。
为了让您轻松开始使用内联结账,Cashier 包含一个 paddle-checkout
Blade 组件。要开始,您应该 生成结账会话:
use Illuminate\Http\Request;
Route::get('/buy', function (Request $request) {
$checkout = $user->checkout('pri_34567')
->returnTo(route('dashboard'));
return view('billing', ['checkout' => $checkout]);
});
然后,您可以将结账会话传递给组件的 checkout
属性:
<x-paddle-checkout :checkout="$checkout" class="w-full" />
要调整内联结账组件的高度,您可以将 height
属性传递给 Blade 组件:
<x-paddle-checkout :checkout="$checkout" class="w-full" height="500" />
请查阅 Paddle 的 内联结账指南 和 可用结账设置 以获取有关内联结账自定义选项的更多详细信息。
手动渲染内联结账
您也可以在不使用 Laravel 内置 Blade 组件的情况下手动渲染内联结账。要开始,请 生成结账会话:
use Illuminate\Http\Request;
Route::get('/buy', function (Request $request) {
$checkout = $user->checkout('pri_34567')
->returnTo(route('dashboard'));
return view('billing', ['checkout' => $checkout]);
});
接下来,您可以使用 Paddle.js 初始化结账。在此示例中,我们将使用 Alpine.js 演示;不过,您可以根据自己的前端堆栈自由修改此示例:
<?php
$options = $checkout->options();
$options['settings']['frameTarget'] = 'paddle-checkout';
$options['settings']['frameInitialHeight'] = 366;
?>
<div class="paddle-checkout" x-data="{}" x-init="
Paddle.Checkout.open(@json($options));
">
</div>
访客结账
有时,您可能需要为不需要在应用程序中创建账户的用户创建结账会话。为此,您可以使用 guest
方法:
use Illuminate\Http\Request;
use Laravel\Paddle\Checkout;
Route::get('/buy', function (Request $request) {
$checkout = Checkout::guest('pri_34567')
->returnTo(route('home'));
return view('billing', ['checkout' => $checkout]);
});
然后,您可以将结账会话提供给 Paddle 按钮 或 内联结账 Blade 组件。
价格预览
Paddle 允许您根据货币自定义价格,基本上允许您为不同国家/地区配置不同的价格。Cashier Paddle 允许您使用 previewPrices
方法检索所有这些价格。此方法接受您希望检索价格的价格 ID:
use Laravel\Paddle\Cashier;
$prices = Cashier::previewPrices(['pri_123', 'pri_456']);
货币将根据请求的 IP 地址确定;不过,您可以选择提供特定国家/地区以检索价格:
use Laravel\Paddle\Cashier;
$prices = Cashier::productPrices(['pri_123', 'pri_456'], ['address' => [
'country_code' => 'BE',
'postal_code' => '1234',
]]);
检索价格后,您可以根据需要显示它们:
<ul>
@foreach ($prices as $price)
<li>{{ $price->product['name'] }} - {{ $price->total() }}</li>
@endforeach
</ul>
您还可以分别显示小计价格和税额:
<ul>
@foreach ($prices as $price)
<li>{{ $price->product_title }} - {{ $price->subtotal() }} (+ {{ $price->tax() }} 税)</li>
@endforeach
</ul>
有关更多信息,请查看 Paddle 的 价格预览 API 文档。
客户价格预览
如果用户已经是客户,并且您希望显示适用于该客户的价格,您可以通过直接从客户实例中检索价格来实现:
use App\Models\User;
$prices = User::find(1)->previewPrices(['pri_123', 'pri_456']);
在内部,Cashier 将使用用户的客户 ID 来检索其货币的价格。因此,例如,居住在美国的用户将看到美元价格,而比利时的用户将看到欧元价格。如果找不到匹配的货币,将使用产品的默认货币。您可以在 Paddle 控制面板中自定义产品或订阅计划的所有价格。
折扣
您还可以选择在折扣后显示价格。调用 previewPrices
方法时,您可以通过 discount_id
选项提供折扣 ID:
use Laravel\Paddle\Cashier;
$prices = Cashier::previewPrices(['pri_123', 'pri_456'], [
'discount_id' => 'dsc_123'
]);
然后,显示计算后的价格:
<ul>
@foreach ($prices as $price)
<li>{{ $price->product['name'] }} - {{ $price->total() }}</li>
@endforeach
</ul>
客户
客户默认值
Cashier 允许您在创建结账会话时为客户定义一些有用的默认值。设置这些默认值可以让您预先填写客户的电子邮件地址和姓名,以便他们可以立即进入结账小部件的支付部分。您可以通过覆盖可计费模型上的以下方法来设置这些默认值:
/**
* 获取要与 Paddle 关联的客户姓名。
*/
public function paddleName(): string|null
{
return $this->name;
}
/**
* 获取要与 Paddle 关联的客户电子邮件地址。
*/
public function paddleEmail(): string|null
{
return $this->email;
}
这些默认值将用于 Cashier 中生成 结账会话 的每个操作。
检索客户
您可以使用 Cashier::findBillable
方法通过 Paddle 客户 ID 检索客户。此方法将返回可计费模型的实例:
use Laravel\Cashier\Cashier;
$user = Cashier::findBillable($customerId);
创建客户
有时,您可能希望在不开始订阅的情况下创建一个 Paddle 客户。您可以使用 createAsCustomer
方法实现此目的:
$customer = $user->createAsCustomer();
返回一个 Laravel\Paddle\Customer
实例。一旦客户在 Paddle 中创建,您可以在稍后开始订阅。您可以提供一个可选的 $options
数组,以传递 Paddle API 支持的任何其他 客户创建参数:
$customer = $user->createAsCustomer($options);
订阅
创建订阅
要创建订阅,首先从数据库中检索可计费模型的实例,通常是 App\Models\User
的实例。一旦检索到模型实例,您可以使用 subscribe
方法创建模型的结账会话:
use Illuminate\Http\Request;
Route::get('/user/subscribe', function (Request $request) {
$checkout = $request->user()->subscribe($premium = 12345, 'default')
->returnTo(route('home'));
return view('billing', ['checkout' => $checkout]);
});
传递给 subscribe
方法的第一个参数是用户订阅的特定价格。此值应对应于 Paddle 中价格的标识符。returnTo
方法接受一个 URL,用户在成功完成结账后将被重定向到该 URL。传递给 subscribe
方法的第二个参数应为订阅的内部“类型”。如果您的应用程序仅提供单一订阅,您可以将其称为 default
或 primary
。此订阅类型仅供内部应用程序使用,不应显示给用户。此外,它不应包含空格,并且在创建订阅后不应更改。
您还可以使用 customData
方法提供有关订阅的自定义元数据数组:
$checkout = $request->user()->subscribe($premium = 12345, 'default')
->customData(['key' => 'value'])
->returnTo(route('home'));
一旦创建了订阅结账会话,结账会话可以提供给 Cashier Paddle 附带的 paddle-button
Blade 组件:
<x-paddle-button :checkout="$checkout" class="px-8 py-4">
订阅
</x-paddle-button>
用户完成结账后,Paddle 将发送一个 subscription_created
webhook。Cashier 将接收此 webhook 并为您的客户设置订阅。为了确保所有 webhooks 都能被您的应用程序正确接收和处理,请确保您已正确 设置 webhook 处理。
检查订阅状态
一旦用户订阅了您的应用程序,您可以使用各种方便的方法检查他们的订阅状态。首先,subscribed
方法返回 true
,如果用户有一个有效的订阅,即使订阅当前处于试用期:
if ($user->subscribed()) {
// ...
}
如果您的应用程序提供多个订阅,您可以在调用 subscribed
方法时指定订阅:
if ($user->subscribed('default')) {
// ...
}
subscribed
方法也是一个很好的 路由中间件 候选者,允许您根据用户的订阅状态过滤对路由和控制器的访问:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureUserIsSubscribed
{
/**
* 处理传入请求。
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if ($request->user() && ! $request->user()->subscribed()) {
// 此用户不是付费客户...
return redirect('billing');
}
return $next($request);
}
}
如果您想确定用户是否仍在试用期内,可以使用 onTrial
方法。此方法可用于确定是否应向用户显示警告,告知他们仍在试用期内:
if ($user->subscription()->onTrial()) {
// ...
}
subscribedToPrice
方法可用于确定用户是否订阅了给定计划,基于给定的 Paddle 价格 ID。在此示例中,我们将确定用户的 default
订阅是否积极订阅了每月价格:
if ($user->subscribedToPrice($monthly = 'pri_123', 'default')) {
// ...
}
recurring
方法可用于确定用户当前是否处于活动订阅状态,并且不再处于试用期或宽限期:
if ($user->subscription()->recurring()) {
// ...
}
已取消订阅状态
要确定用户是否曾经是活跃订阅者但已取消订阅,您可以使用 canceled
方法:
if ($user->subscription()->canceled()) {
// ...
}
您还可以确定用户是否已取消订阅,但仍处于“宽限期”内,直到订阅完全过期。例如,如果用户在 3 月 5 日取消订阅,而原计划在 3 月 10 日到期,则用户在 3 月 10 日之前处于“宽限期”。此外,在此期间,subscribed
方法仍将返回 true
:
if ($user->subscription()->onGracePeriod()) {
// ...
}
逾期状态
如果订阅的付款失败,它将被标记为 past_due
。当您的订阅处于此状态时,它将不会激活,直到客户更新其支付信息。您可以使用订阅实例上的 pastDue
方法确定订阅是否逾期:
if ($user->subscription()->pastDue()) {
// ...
}
当订阅逾期时,您应该指示用户 更新其支付信息。
如果您希望订阅在 past_due
状态时仍被视为有效,您可以使用 Cashier 提供的 keepPastDueSubscriptionsActive
方法。通常,此方法应在 AppServiceProvider
的 register
方法中调用:
use Laravel\Paddle\Cashier;
/**
* 注册任何应用程序服务。
*/
public function register(): void
{
Cashier::keepPastDueSubscriptionsActive();
}
当订阅处于 past_due
状态时,无法更改。因此,当订阅处于 past_due
状态时,swap
和 updateQuantity
方法将抛出异常。
订阅范围
大多数订阅状态也可作为查询范围,以便您可以轻松查询数据库中处于给定状态的订阅:
// 获取所有有效订阅...
$subscriptions = Subscription::query()->valid()->get();
// 获取用户的所有已取消订阅...
$subscriptions = $user->subscriptions()->canceled()->get();
以下是可用范围的完整列表:
Subscription::query()->valid();
Subscription::query()->onTrial();
Subscription::query()->expiredTrial();
Subscription::query()->notOnTrial();
Subscription::query()->active();
Subscription::query()->recurring();
Subscription::query()->pastDue();
Subscription::query()->paused();
Subscription::query()->notPaused();
Subscription::query()->onPausedGracePeriod();
Subscription::query()->notOnPausedGracePeriod();
Subscription::query()->canceled();
Subscription::query()->notCanceled();
Subscription::query()->onGracePeriod();
Subscription::query()->notOnGracePeriod();
订阅单次收费
订阅单次收费允许您向订阅者收取一次性费用。调用 charge
方法时,您必须提供一个或多个价格 ID:
// 收取单个价格...
$response = $user->subscription()->charge('pri_123');
// 一次性收取多个价格...
$response = $user->subscription()->charge(['pri_123', 'pri_456']);
charge
方法不会立即向客户收费,直到其订阅的下一个计费间隔。如果您希望立即向客户收费,可以使用 chargeAndInvoice
方法:
$response = $user->subscription()->chargeAndInvoice('pri_123');
更新支付信息
Paddle 始终为每个订阅保存一个支付方式。如果您想更新订阅的默认支付方式,您应该使用订阅模型上的 redirectToUpdatePaymentMethod
方法将客户重定向到 Paddle 托管的支付方式更新页面:
use Illuminate\Http\Request;
Route::get('/update-payment-method', function (Request $request) {
$user = $request->user();
return $user->subscription()->redirectToUpdatePaymentMethod();
});
当用户完成信息更新后,Paddle 将发送一个 subscription_updated
webhook,并在应用程序的数据库中更新订阅详细信息。
更改计划
用户订阅您的应用程序后,他们可能偶尔想要更改为新的订阅计划。要更新用户的订阅计划,您应该将 Paddle 价格的标识符传递给订阅的 swap
方法:
use App\Models\User;
$user = User::find(1);
$user->subscription()->swap($premium = 'pri_456');
如果您希望交换计划并立即向用户开具发票,而不是等待他们的下一个计费周期,可以使用 swapAndInvoice
方法:
$user = User::find(1);
$user->subscription()->swapAndInvoice($premium = 'pri_456');
按比例分配
默认情况下,Paddle 在计划之间交换时按比例分配费用。noProrate
方法可用于在不按比例分配费用的情况下更新订阅:
$user->subscription('default')->noProrate()->swap($premium = 'pri_456');
如果您希望禁用按比例分配并立即向客户开具发票,可以将 swapAndInvoice
方法与 noProrate
结合使用:
$user->subscription('default')->noProrate()->swapAndInvoice($premium = 'pri_456');
或者,为了不向客户收取订阅更改的费用,您可以使用 doNotBill
方法:
$user->subscription('default')->doNotBill()->swap($premium = 'pri_456');
有关 Paddle 的按比例分配政策的更多信息,请查阅 Paddle 的 按比例分配文档。
订阅数量
有时订阅会受到“数量”的影响。例如,项目管理应用程序可能每月每个项目收费 10 美元。要轻松增加或减少订阅的数量,请使用 incrementQuantity
和 decrementQuantity
方法:
$user = User::find(1);
$user->subscription()->incrementQuantity();
// 在订阅的当前数量上增加五个...
$user->subscription()->incrementQuantity(5);
$user->subscription()->decrementQuantity();
// 在订阅的当前数量上减少五个...
$user->subscription()->decrementQuantity(5);
或者,您可以使用 updateQuantity
方法设置特定数量:
$user->subscription()->updateQuantity(10);
noProrate
方法可用于在不按比例分配费用的情况下更新订阅的数量:
$user->subscription()->noProrate()->updateQuantity(10);
多产品订阅的数量
如果您的订阅是 多产品订阅,您应该将要增加或减少数量的价格 ID 作为第二个参数传递给增量/减量方法:
$user->subscription()->incrementQuantity(1, 'price_chat');
多产品订阅
多产品订阅 允许您将多个计费产品分配给单个订阅。例如,假设您正在构建一个客户服务“帮助台”应用程序,该应用程序的基本订阅价格为每月 10 美元,但提供一个实时聊天附加产品,每月额外收费 15 美元。
在创建订阅结账会话时,您可以通过将价格数组作为 subscribe
方法的第一个参数来为给定订阅指定多个产品:
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$checkout = $request->user()->subscribe([
'price_monthly',
'price_chat',
]);
return view('billing', ['checkout' => $checkout]);
});
在上面的示例中,客户将有两个价格附加到他们的 default
订阅。两个价格都将在各自的计费间隔内收费。如果需要,您可以传递一个键/值对的关联数组,以指示每个价格的特定数量:
$user = User::find(1);
$checkout = $user->subscribe('default', ['price_monthly', 'price_chat' => 5]);
如果您希望将另一个价格添加到现有订阅中,您必须使用订阅的 swap
方法。调用 swap
方法时,您还应包括订阅的当前价格和数量:
$user = User::find(1);
$user->subscription()->swap(['price_chat', 'price_original' => 2]);
上面的示例将添加新价格,但客户将在下一个计费周期之前不会被收取费用。如果您希望立即向客户收费,可以使用 swapAndInvoice
方法:
$user->subscription()->swapAndInvoice(['price_chat', 'price_original' => 2]);
您可以使用 swap
方法删除订阅中的价格,并省略要删除的价格:
$user->subscription()->swap(['price_original' => 2]);
您不能删除订阅中的最后一个价格。相反,您应该简单地取消订阅。
多重订阅
Paddle 允许您的客户同时拥有多个订阅。例如,您可能经营一家提供游泳订阅和举重订阅的健身房,每个订阅可能有不同的定价。当然,客户应该能够订阅任一或两个计划。
当您的应用程序创建订阅时,您可以将订阅的类型作为第二个参数提供给 subscribe
方法。类型可以是表示用户正在启动的订阅类型的任何字符串:
use Illuminate\Http\Request;
Route::post('/swimming/subscribe', function (Request $request) {
$checkout = $request->user()->subscribe($swimmingMonthly = 'pri_123', 'swimming');
return view('billing', ['checkout' => $checkout]);
});
在此示例中,我们为客户启动了每月游泳订阅。然而,他们可能希望在稍后时间交换为年度订阅。在调整客户的订阅时,我们可以简单地交换 swimming
订阅上的价格:
$user->subscription('swimming')->swap($swimmingYearly = 'pri_456');
当然,您也可以完全取消订阅:
$user->subscription('swimming')->cancel();
暂停订阅
要暂停订阅,请调用用户订阅上的 pause
方法:
$user->subscription()->pause();
当订阅暂停时,Cashier 将自动在数据库中设置 paused_at
列。此列用于确定 paused
方法何时应开始返回 true
。例如,如果客户在 3 月 1 日暂停订阅,但订阅原计划在 3 月 5 日续订,则 paused
方法将继续返回 false
,直到 3 月 5 日。这是因为用户通常被允许在其计费周期结束之前继续使用应用程序。
默认情况下,暂停发生在下一个计费间隔,因此客户可以使用他们支付的剩余时间。如果您希望立即暂停订阅,可以使用 pauseNow
方法:
$user->subscription()->pauseNow();
使用 pauseUntil
方法,您可以将订阅暂停到特定时间点:
$user->subscription()->pauseUntil(now()->addMonth());
或者,您可以使用 pauseNowUntil
方法立即暂停订阅,直到给定时间点:
$user->subscription()->pauseNowUntil(now()->addMonth());
您可以使用 onPausedGracePeriod
方法确定用户是否已暂停其订阅,但仍处于“宽限期”内:
if ($user->subscription()->onPausedGracePeriod()) {
// ...
}
要恢复暂停的订阅,您可以在订阅上调用 resume
方法:
$user->subscription()->resume();
暂停的订阅无法修改。如果您想交换到不同的计划或更新数量,您必须先恢复订阅。
取消订阅
要取消订阅,请调用用户订阅上的 cancel
方法:
$user->subscription()->cancel();
当订阅被取消时,Cashier 将自动在数据库中设置 ends_at
列。此列用于确定 subscribed
方法何时应开始返回 false
。例如,如果客户在 3 月 1 日取消订阅,但订阅原计划在 3 月 5 日结束,则 subscribed
方法将继续返回 true
,直到 3 月 5 日。这是因为用户通常被允许在其计费周期结束之前继续使用应用程序。
您可以使用 onGracePeriod
方法确定用户是否已取消订阅,但仍处于“宽限期”内:
if ($user->subscription()->onGracePeriod()) {
// ...
}
如果您希望立即取消订阅,可以在订阅上调用 cancelNow
方法:
$user->subscription()->cancelNow();
要阻止订阅在其宽限期内取消,您可以调用 stopCancelation
方法:
$user->subscription()->stopCancelation();
Paddle 的订阅在取消后无法恢复。如果您的客户希望恢复其订阅,他们将不得不创建一个新订阅。
订阅试用
提前支付方式
如果您希望在仍然收集支付方式信息的情况下向客户提供试用期,您应该在客户订阅的价格上在 Paddle 仪表板中设置试用时间。然后,正常启动结账会话:
use Illuminate\Http\Request;
Route::get('/user/subscribe', function (Request $request) {
$checkout = $request->user()->subscribe('pri_monthly')
->returnTo(route('home'));
return view('billing', ['checkout' => $checkout]);
});
当您的应用程序收到 subscription_created
事件时,Cashier 将在应用程序数据库中的订阅记录上设置试用期结束日期,并指示 Paddle 在此日期之后才开始向客户收费。
如果客户的订阅在试用期结束日期之前未被取消,他们将在试用期到期后立即被收费,因此您应该确保通知用户其试用期结束日期。
您可以使用用户实例的 onTrial
方法或订阅实例的 onTrial
方法确定用户是否在其试用期内。以下两个示例是等效的:
if ($user->onTrial()) {
// ...
}
if ($user->subscription()->onTrial()) {
// ...
}
要确定现有试用期是否已过期,您可以使用 hasExpiredTrial
方法:
if ($user->hasExpiredTrial()) {
// ...
}
if ($user->subscription()->hasExpiredTrial()) {
// ...
}
要确定用户是否在特定订阅类型的试用期内,您可以将类型传递给 onTrial
或 hasExpiredTrial
方法:
if ($user->onTrial('default')) {
// ...
}
if ($user->hasExpiredTrial('default')) {
// ...
}
无提前支付方式
如果您希望在不收集用户支付方式信息的情况下提供试用期,您可以在用户附加的客户记录上设置 trial_ends_at
列为您期望的试用期结束日期。这通常在用户注册期间完成:
use App\Models\User;
$user = User::create([
// ...
]);
$user->createAsCustomer([
'trial_ends_at' => now()->addDays(10)
]);
Cashier 将此类型的试用称为“通用试用”,因为它不附加到任何现有订阅。User
实例上的 onTrial
方法将返回 true
,如果当前日期未超过 trial_ends_at
的值:
if ($user->onTrial()) {
// 用户在其试用期内...
}
一旦您准备好为用户创建实际订阅,可以像往常一样使用 subscribe
方法:
use Illuminate\Http\Request;
Route::get('/user/subscribe', function (Request $request) {
$checkout = $user->subscribe('pri_monthly')
->returnTo(route('home'));
return view('billing', ['checkout' => $checkout]);
});
要检索用户的试用期结束日期,您可以使用 trialEndsAt
方法。如果用户在试用期内,此方法将返回一个 Carbon 日期实例;如果他们不在试用期内,则返回 null
。如果您希望获取特定订阅而不是默认订阅的试用期结束日期,还可以传递一个可选的订阅类型参数:
if ($user->onTrial('default')) {
$trialEndsAt = $user->trialEndsAt();
}
如果您希望知道用户是否在其“通用”试用期内,并且尚未创建实际订阅,可以使用 onGenericTrial
方法:
if ($user->onGenericTrial()) {
// 用户在其“通用”试用期内...
}
延长或激活试用
您可以通过调用 extendTrial
方法并指定试用期应结束的时间点来延长现有订阅的试用期:
$user->subsription()->extendTrial(now()->addDays(5));
或者,您可以通过在订阅上调用 activate
方法立即激活订阅,结束其试用期:
$user->subscription()->activate();
处理 Paddle Webhooks
Paddle 可以通过 webhooks 通知您的应用程序各种事件。默认情况下,Cashier 服务提供者注册了一个指向 Cashier 的 webhook 控制器的路由。此控制器将处理所有传入的 webhook 请求。
默认情况下,此控制器将自动处理取消订阅的失败收费、订阅更新和支付方式更改;然而,正如我们将很快发现的,您可以扩展此控制器以处理您喜欢的任何 Paddle webhook 事件。
为了确保您的应用程序可以处理 Paddle webhooks,请确保在 Paddle 控制面板中 配置 webhook URL。默认情况下,Cashier 的 webhook 控制器响应 /paddle/webhook
URL 路径。您应该在 Paddle 控制面板中启用的所有 webhooks 的完整列表如下:
- 客户更新
- 交易完成
- 交易更新
- 订阅创建
- 订阅更新
- 订阅暂停
- 订阅取消
确保使用 Cashier 附带的 webhook 签名验证 中间件保护传入请求。
Webhooks 和 CSRF 保护
由于 Paddle webhooks 需要绕过 Laravel 的 CSRF 保护,请确保在 App\Http\Middleware\VerifyCsrfToken
中间件中将 URI 列为例外,或将路由列在 web
中间件组之外:
protected $except = [
'paddle/*',
];
Webhooks 和本地开发
为了让 Paddle 能够在本地开发期间向您的应用程序发送 webhooks,您需要通过站点共享服务(如 Ngrok 或 Expose)公开您的应用程序。如果您正在使用 Laravel Sail 本地开发应用程序,可以使用 Sail 的 站点共享命令。
定义 Webhook 事件处理程序
Cashier 自动处理失败收费的订阅取消和其他常见的 Paddle webhooks。然而,如果您有其他 webhook 事件需要处理,可以通过监听 Cashier 发送的以下事件来实现:
Laravel\Paddle\Events\WebhookReceived
Laravel\Paddle\Events\WebhookHandled
这两个事件都包含 Paddle webhook 的完整负载。例如,如果您希望处理 transaction_billed
webhook,可以注册一个 监听器 来处理事件:
<?php
namespace App\Listeners;
use Laravel\Paddle\Events\WebhookReceived;
class PaddleEventListener
{
/**
* 处理接收到的 Paddle webhooks。
*/
public function handle(WebhookReceived $event): void
{
if ($event->payload['alert_name'] === 'transaction_billed') {
// 处理传入事件...
}
}
}
定义监听器后,您可以在应用程序的 EventServiceProvider
中注册它:
<?php
namespace App\Providers;
use App\Listeners\PaddleEventListener;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Laravel\Paddle\Events\WebhookReceived;
class EventServiceProvider extends ServiceProvider
{
protected $listen = [
WebhookReceived::class => [
PaddleEventListener::class,
],
];
}
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
您还可以通过在应用程序的 .env
文件中定义 CASHIER_WEBHOOK
环境变量来覆盖默认的内置 webhook 路由。此值应为您的 webhook 路由的完整 URL,并需要与 Paddle 控制面板中设置的 URL 匹配:
CASHIER_WEBHOOK=https://example.com/my-paddle-webhook-url
验证 Webhook 签名
为了保护您的 webhooks,您可以使用 Paddle 的 webhook 签名。为了方便起见,Cashier 自动包含一个中间件,用于验证传入的 Paddle webhook 请求是否有效。
要启用 webhook 验证,请确保在应用程序的 .env
文件中定义 PADDLE_WEBHOOK_SECRET
环境变量。webhook 密钥可以从您的 Paddle 账户仪表板中检索。
单次收费
产品收费
如果您希望为客户发起产品购买,可以在可计费模型实例上使用 checkout
方法为购买生成结账会话。checkout
方法接受一个或多个价格 ID。如果需要,可以使用关联数组提供购买产品的数量:
use Illuminate\Http\Request;
Route::get('/buy', function (Request $request) {
$checkout = $request->user()->checkout(['pri_tshirt', 'pri_socks' => 5]);
return view('buy', ['checkout' => $checkout]);
});
生成结账会话后,您可以使用 Cashier 提供的 paddle-button
Blade 组件 允许用户查看 Paddle 结账小部件并完成购买:
<x-paddle-button :checkout="$checkout" class="px-8 py-4">
购买
</x-paddle-button>
结账会话有一个 customData
方法,允许您将任何自定义数据传递给底层交易创建。请查阅 Paddle 文档 以了解传递自定义数据时可用的选项:
$checkout = $user->checkout('pri_tshirt')
->customData([
'custom_option' => $value,
]);
退款交易
退款交易将退款金额返回到客户在购买时使用的支付方式。如果您需要退款 Paddle 购买,可以在 Cashier\Paddle\Transaction
模型上使用 refund
方法。此方法接受一个原因作为第一个参数,一个或多个价格 ID 以退款,并可选地作为关联数组提供金额。您可以使用 transactions
方法检索给定可计费模型的交易。
例如,假设我们想为价格 pri_123
和 pri_456
的特定交易退款。我们想完全退款 pri_123
,但只为 pri_456
退款两美元:
use App\Models\User;
$user = User::find(1);
$transaction = $user->transactions()->first();
$response = $transaction->refund('Accidental charge', [
'pri_123', // 完全退款此价格...
'pri_456' => 200, // 仅部分退款此价格...
]);
上面的示例为交易中的特定行项目退款。如果您想退款整个交易,只需提供一个原因:
$response = $transaction->refund('Accidental charge');
有关退款的更多信息,请查阅 Paddle 的退款文档。
退款必须始终由 Paddle 批准后才能完全处理。
交易记账
就像退款一样,您也可以为交易记账。交易记账将资金添加到客户的余额中,以便用于未来的购买。交易记账只能用于手动收集的交易,而不能用于自动收集的交易(如订阅),因为 Paddle 会自动处理订阅记账:
$transaction = $user->transactions()->first();
// 完全记账特定行项目...
$response = $transaction->credit('Compensation', 'pri_123');
有关更多信息,请查阅 Paddle 的记账文档。
记账只能应用于手动收集的交易。自动收集的交易由 Paddle 自行记账。
交易
您可以通过 transactions
属性轻松检索可计费模型的交易数组:
use App\Models\User;
$user = User::find(1);
$transactions = $user->transactions;
交易代表您的产品和购买的付款,并附有发票。只有已完成的交易存储在应用程序的数据库中。
列出客户的交易时,您可以使用交易实例的方法显示相关的支付信息。例如,您可能希望在表格中列出每笔交易,允许用户轻松下载任何发票:
<table>
@foreach ($transactions as $transaction)
<tr>
<td>{{ $transaction->billed_at->toFormattedDateString() }}</td>
<td>{{ $transaction->total() }}</td>
<td>{{ $transaction->tax() }}</td>
<td>
<a
href="{{ route('download-invoice', $transaction->id) }}"
target="_blank"
>下载</a
>
</td>
</tr>
@endforeach
</table>
download-invoice
路由可能如下所示:
use Illuminate\Http\Request;
use Laravel\Cashier\Transaction;
Route::get('/download-invoice/{transaction}', function (Request $request, Transaction $transaction) {
return $transaction->redirectToInvoicePdf();
})->name('download-invoice');
过去和即将到来的付款
您可以使用 lastPayment
和 nextPayment
方法检索和显示客户的过去或即将到来的定期订阅付款:
use App\Models\User;
$user = User::find(1);
$subscription = $user->subscription();
$lastPayment = $subscription->lastPayment();
$nextPayment = $subscription->nextPayment();
这两个方法都将返回一个 Laravel\Paddle\Payment
实例;然而,当交易尚未通过 webhooks 同步时,lastPayment
将返回 null
,而当计费周期结束时(例如订阅已取消时),nextPayment
将返回 null
:
下次付款:{{ $nextPayment->amount() }} 到期日期为 {{ $nextPayment->date()->format('d/m/Y') }}
测试
在测试时,您应该手动测试您的计费流程,以确保您的集成按预期工作。
对于自动化测试,包括在 CI 环境中执行的测试,您可以使用 Laravel 的 HTTP 客户端 来伪造对 Paddle 的 HTTP 调用。虽然这不会测试来自 Paddle 的实际响应,但它提供了一种在不实际调用 Paddle API 的情况下测试应用程序的方法。