Appearance
Laravel Cashier (Paddle)
简介
WARNING
本文档适用于 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 包:
shell
composer require laravel/cashier-paddle接下来,你应该使用 vendor:publish Artisan 命令发布 Cashier 的迁移文件:
shell
php artisan vendor:publish --tag="cashier-migrations"然后,你应该运行应用程序的数据库迁移。Cashier 迁移将创建一个新的 customers 表。此外,还将创建新的 subscriptions 和 subscription_items 表来存储所有客户的订阅信息。最后,将创建一个新的 transactions 表来存储与客户相关的所有 Paddle 交易:
shell
php artisan migrateWARNING
为确保 Cashier 能正确处理所有 Paddle 事件,请记得设置 Cashier 的 webhook 处理。
Paddle 沙盒
在本地和测试环境开发期间,你应该注册一个 Paddle 沙盒账户。这个账户将为你提供一个沙盒环境,用于测试和开发你的应用程序,而无需进行实际支付。你可以使用 Paddle 的测试卡号来模拟各种支付场景。
使用 Paddle 沙盒环境时,你应该在应用程序的 .env 文件中将 PADDLE_SANDBOX 环境变量设置为 true:
ini
PADDLE_SANDBOX=true完成应用程序开发后,你可以申请一个 Paddle 供应商账户。在你的应用程序投入生产之前,Paddle 需要批准你的应用程序域名。
配置
Billable 模型
在使用 Cashier 之前,你必须将 Billable trait 添加到你的用户模型定义中。这个 trait 提供了各种方法,允许你执行常见的计费任务,如创建订阅和更新支付方式信息:
php
use Laravel\Paddle\Billable;
class User extends Authenticatable
{
use Billable;
}如果你有不是用户的可计费实体,你也可以将该 trait 添加到这些类中:
php
use Illuminate\Database\Eloquent\Model;
use Laravel\Paddle\Billable;
class Team extends Model
{
use Billable;
}API 密钥
接下来,你应该在应用程序的 .env 文件中配置你的 Paddle 密钥。你可以从 Paddle 控制面板检索 Paddle API 密钥:
ini
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 库:
blade
<head>
...
@paddleJS
</head>货币配置
你可以指定一个区域设置,用于格式化发票上显示的货币值。在内部,Cashier 使用 PHP 的 NumberFormatter 类来设置货币区域设置:
ini
CASHIER_CURRENCY_LOCALE=nl_BEWARNING
为了使用 en 以外的区域设置,请确保在服务器上安装并配置了 ext-intl PHP 扩展。
覆盖默认模型
你可以自由扩展 Cashier 内部使用的模型,方法是定义自己的模型并扩展相应的 Cashier 模型:
php
use Laravel\Paddle\Subscription as CashierSubscription;
class Subscription extends CashierSubscription
{
// ...
}定义模型后,你可以通过 Laravel\Paddle\Cashier 类指示 Cashier 使用你的自定义模型。通常,你应该在应用程序的 App\Providers\AppServiceProvider 类的 boot 方法中通知 Cashier 有关你的自定义模型:
php
use App\Models\Cashier\Subscription;
use App\Models\Cashier\Transaction;
/**
* 引导任何应用程序服务。
*/
public function boot(): void
{
Cashier::useSubscriptionModel(Subscription::class);
Cashier::useTransactionModel(Transaction::class);
}快速入门
销售产品
NOTE
在使用 Paddle 结账之前,你应该在 Paddle 仪表板中定义具有固定价格的产品。此外,你应该配置 Paddle 的 webhook 处理。
通过你的应用程序提供产品和订阅计费可能会让人感到害怕。然而,多亏了 Cashier 和 Paddle 的结账覆盖层,你可以轻松构建现代、强大的支付集成。
要为非重复性的单次收费产品向客户收费,我们将使用 Cashier 通过 Paddle 的结账覆盖层向客户收费,客户将在其中提供他们的支付详细信息并确认购买。一旦通过结账覆盖层完成支付,客户将被重定向到你在应用程序中选择的成功 URL:
php
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 结账覆盖层。在使用 Paddle 时,"价格"指的是特定产品的定义价格。
如果需要,checkout 方法将自动在 Paddle 中创建一个客户,并将该 Paddle 客户记录连接到你的应用程序数据库中相应的用户。完成结账会话后,客户将被重定向到一个专用的成功页面,你可以在那里向客户显示信息性消息。
在 buy 视图中,我们将包含一个按钮来显示结账覆盖层。paddle-button Blade 组件包含在 Cashier Paddle 中;但是,你也可以手动渲染覆盖结账:
html
<x-paddle-button :checkout="$checkout" class="px-8 py-4">
购买产品
</x-paddle-button>向 Paddle 结账提供元数据
在销售产品时,通常会通过你自己的应用程序定义的 Cart 和 Order 模型来跟踪已完成的订单和购买的产品。当将客户重定向到 Paddle 的结账覆盖层以完成购买时,你可能需要提供现有的订单标识符,以便在客户重定向回你的应用程序时,可以将完成的购买与相应的订单关联起来。
为了实现这一点,你可以向 checkout 方法提供一个自定义数据数组。让我们假设当用户开始结账过程时,在我们的应用程序中创建了一个待处理的 Order。请记住,这个示例中的 Cart 和 Order 模型是说明性的,并非由 Cashier 提供。你可以根据自己应用程序的需求自由实现这些概念:
php
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');如你在上面的示例中所见,当用户开始结账过程时,我们将向 checkout 方法提供所有与购物车/订单相关的 Paddle 价格标识符。当然,你的应用程序负责在客户添加这些项目时将它们与"购物车"或订单关联起来。我们还通过 customData 方法将订单的 ID 提供给 Paddle 结账覆盖层。
当然,你可能希望在客户完成结账过程后将订单标记为"完成"。为了实现这一点,你可以监听 Paddle 发送的 webhooks 和 Cashier 引发的事件,以在你的数据库中存储订单信息。
首先,监听 Cashier 发送的 TransactionCompleted 事件。通常,你应该在应用程序的 AppServiceProvider 的 boot 方法中注册事件监听器:
php
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 监听器可能如下所示:
php
namespace App\Listeners;
use App\Models\Order;
use Laravel\Paddle\Cashier;
use Laravel\Paddle\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 事件 包含的数据的更多信息。
销售订阅
NOTE
在使用 Paddle 结账之前,你应该在 Paddle 仪表板中定义具有固定价格的产品。此外,你应该配置 Paddle 的 webhook 处理。
通过你的应用程序提供产品和订阅计费可能会让人感到害怕。然而,多亏了 Cashier 和 Paddle 的结账覆盖层,你可以轻松构建现代、强大的支付集成。
为了学习如何使用 Cashier 和 Paddle 的结账覆盖层销售订阅,让我们考虑一个简单的订阅服务场景,该服务有一个基本的月度(price_basic_monthly)和年度(price_basic_yearly)计划。这两个价格可以在我们的 Paddle 仪表板中归为"Basic"产品(pro_basic)。此外,我们的订阅服务可能还提供一个专家计划 pro_expert。
首先,让我们了解客户如何订阅我们的服务。当然,你可以想象客户可能会在我们应用程序的定价页面上点击"Basic"计划的"订阅"按钮。这个按钮将为他们选择的计划调用 Paddle 结账覆盖层。首先,让我们通过 checkout 方法启动一个结账会话:
php
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 视图中,我们将包含一个按钮来显示结账覆盖层。paddle-button Blade 组件包含在 Cashier Paddle 中;但是,你也可以手动渲染覆盖结账:
html
<x-paddle-button :checkout="$checkout" class="px-8 py-4">
订阅
</x-paddle-button>现在,当点击订阅按钮时,客户将能够输入他们的支付详细信息并开始他们的订阅。要知道他们的订阅何时实际开始(因为某些支付方式需要几秒钟来处理),你还应该配置 Cashier 的 webhook 处理。
现在客户可以开始订阅,我们需要限制应用程序的某些部分,以便只有订阅用户才能访问它们。当然,我们始终可以通过 Cashier 的 Billable trait 提供的 subscribed 方法来确定用户的当前订阅状态:
blade
@if ($user->subscribed())
<p>你已订阅。</p>
@endif我们甚至可以轻松确定用户是否订阅了特定产品或价格:
blade
@if ($user->subscribedToProduct('pro_basic'))
<p>你已订阅我们的基本产品。</p>
@endif
@if ($user->subscribedToPrice('price_basic_monthly'))
<p>你已订阅我们的月度基本计划。</p>
@endif构建订阅中间件
为了方便起见,你可能希望创建一个中间件,用于确定传入请求是否来自已订阅的用户。一旦定义了这个中间件,你就可以轻松地将其分配给路由,以防止未订阅的用户访问该路由:
php
<?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);
}
}一旦定义了中间件,你就可以将其分配给路由:
php
use App\Http\Middleware\Subscribed;
Route::get('/dashboard', function () {
// ...
})->middleware([Subscribed::class]);允许客户管理他们的计费计划
当然,客户可能希望将他们的订阅计划更改为另一个产品或"等级"。在我们上面的例子中,我们希望允许客户将他们的计划从月度订阅更改为年度订阅。为此,你需要实现类似于下面路由的按钮:
php
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');除了切换计划,你还需要允许你的客户取消他们的订阅。像切换计划一样,提供一个按钮,引导到以下路由:
php
use Illuminate\Http\Request;
Route::put('/subscription/cancel', function (Request $request, $price) {
$user->subscription()->cancel();
return redirect()->route('dashboard');
})->name('subscription.cancel');现在你的订阅将在其计费期结束时被取消。
NOTE
只要你已配置 Cashier 的 webhook 处理,Cashier 将通过检查来自 Paddle 的传入 webhooks 自动保持应用程序的 Cashier 相关数据库表同步。因此,例如,当你通过 Paddle 的仪表板取消客户的订阅时,Cashier 将接收相应的 webhook 并在你的应用程序数据库中将订阅标记为"已取消"。
结账会话
大多数向客户收费的操作都是通过 Paddle 的结账覆盖小部件或使用内联结账执行的。
在使用 Paddle 处理结账支付之前,你应该在 Paddle 结账设置仪表板中定义应用程序的默认支付链接。
覆盖结账
在显示结账覆盖小部件之前,你必须使用 Cashier 生成一个结账会话。结账会话将告知结账小部件应执行的计费操作:
php
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 的结账小部件:
html
<x-paddle-button :checkout="$checkout" class="px-8 py-4">
订阅
</x-paddle-button>默认情况下,这将使用 Paddle 的默认样式显示小部件。你可以通过添加 Paddle 支持的属性(如 data-theme='light' 属性)来自定义小部件:
html
<x-paddle-button :url="$payLink" class="px-8 py-4" data-theme="light">
订阅
</x-paddle-button>Paddle 结账小部件是异步的。一旦用户在小部件中创建订阅,Paddle 将向你的应用程序发送一个 webhook,以便你可以适当更新应用程序数据库中的订阅状态。因此,重要的是你要正确设置 webhooks以适应来自 Paddle 的状态变化。
WARNING
订阅状态变化后,接收相应 webhook 的延迟通常很小,但你应该在应用程序中考虑到这一点,即用户的订阅可能在完成结账后不会立即可用。
手动渲染覆盖结账
你也可以在不使用 Laravel 内置 Blade 组件的情况下手动渲染覆盖结账。首先,如前面的示例所示生成结账会话:
php
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 将检测这个类,并在点击链接时显示覆盖结账:
blade
<?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 组件。首先,你应该生成一个结账会话:
php
use Illuminate\Http\Request;
Route::get('/buy', function (Request $request) {
$checkout = $user->checkout('pri_34567')
->returnTo(route('dashboard'));
return view('billing', ['checkout' => $checkout]);
});然后,你可以将结账会话传递给组件的 checkout 属性:
blade
<x-paddle-checkout :checkout="$checkout" class="w-full" />要调整内联结帐组件的高度,您可以将 height 属性传递给 Blade 组件:
blade
<x-paddle-checkout :checkout="$checkout" class="w-full" height="500" />有关内联结账自定义选项的更多详细信息,请参阅 Paddle 的内联结账指南和可用的结账设置。
手动渲染内联签出
你也可以手动渲染内联签出,而无需使用 Laravel 的内置 Blade 组件。要开始使用,请生成 checkout 会话,如前面的示例所示:
php
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 进行演示;但是,您可以自由地为自己的 frontend 堆栈修改此示例:
blade
<?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 方法:
php
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:
php
use Laravel\Paddle\Cashier;
$prices = Cashier::previewPrices(['pri_123', 'pri_456']);货币将根据请求的 IP 地址确定;但是,您可以选择提供特定国家/地区来检索以下商品的价格:
php
use Laravel\Paddle\Cashier;
$prices = Cashier::previewPrices(['pri_123', 'pri_456'], ['address' => [
'country_code' => 'BE',
'postal_code' => '1234',
]]);检索价格后,您可以按照自己的意愿显示它们:
blade
<ul>
@foreach ($prices as $price)
<li>{{ $price->product['name'] }} - {{ $price->total() }}</li>
@endforeach
</ul>您也可以单独显示小计价格和税额:
blade
<ul>
@foreach ($prices as $price)
<li>{{ $price->product['name'] }} - {{ $price->subtotal() }} (+ {{ $price->tax() }} tax)</li>
@endforeach
</ul>有关更多信息,请查看 Paddle 的 API 文档 关于价格预览。
买家价格预览
如果用户已经是客户,并且您希望显示适用于该客户的价格,则可以直接从 Customer 实例中检索价格:
php
use App\Models\User;
$prices = User::find(1)->previewPrices(['pri_123', 'pri_456']);在内部,Cashier 将使用用户的客户 ID 来检索其货币的价格。因此,例如,居住在美国的用户将看到以美元为单位的价格,而居住在比利时的用户将看到以欧元为单位的价格。如果找不到匹配的货币,则将使用产品的默认货币。您可以在 Paddle 控制面板中自定义产品或订阅计划的所有价格。
折扣
您也可以选择在折扣后显示价格。调用 previewPrices 方法时,您可以通过 discount_id 选项提供折扣 ID:
php
use Laravel\Paddle\Cashier;
$prices = Cashier::previewPrices(['pri_123', 'pri_456'], [
'discount_id' => 'dsc_123'
]);然后,显示计算出的价格:
blade
<ul>
@foreach ($prices as $price)
<li>{{ $price->product['name'] }} - {{ $price->total() }}</li>
@endforeach
</ul>客户
客户默认值
Cashier 允许您在创建结账会话时为客户定义一些有用的默认值。设置这些默认值允许您预先填写客户的电子邮件地址和姓名,以便他们可以立即进入结账小部件的付款部分。您可以通过在计费模型上覆盖以下方法来设置这些默认值:
php
/**
* Get the customer's name to associate with Paddle.
*/
public function paddleName(): string|null
{
return $this->name;
}
/**
* Get the customer's email address to associate with Paddle.
*/
public function paddleEmail(): string|null
{
return $this->email;
}这些默认值将用于 Cashier 中生成结账会话的每个操作。
检索客户
您可以通过 Cashier::findBillable 方法通过 Paddle 客户 ID 检索客户。该方法将返回计费模型的实例:
php
use Laravel\Paddle\Cashier;
$user = Cashier::findBillable($customerId);创建客户
有时,您可能希望在不开始订阅的情况下创建 Paddle 客户。您可以使用 createAsCustomer 方法完成此操作:
php
$customer = $user->createAsCustomer();返回 Laravel\Paddle\Customer 的实例。在 Paddle 中创建客户后,您可以在以后开始订阅。您可以提供可选的 $options 数组来传入 Paddle API 支持的任何其他客户创建参数:
php
$customer = $user->createAsCustomer($options);订阅
创建订阅
要创建订阅,请首先从数据库中检索计费模型的实例,该实例通常是 App\Models\User 的实例。检索到模型实例后,你可以使用 subscribe 方法创建模型的 checkout 会话:
php
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 方法的第二个参数应该是订阅的内部 “type”。如果您的应用程序仅提供单个订阅,则可以将其称为 default 或 primary。此订阅类型仅用于内部应用程序使用,不打算向用户显示。此外,它不应包含空格,并且在创建订阅后不应更改。
您还可以使用 customData 方法提供有关订阅的自定义元数据数组:
php
$checkout = $request->user()->subscribe($premium = 12345, 'default')
->customData(['key' => 'value'])
->returnTo(route('home'));创建订阅结帐会话后,可以将结帐会话提供给 Cashier Paddle 附带的桨式按钮Blade 组件:
blade
<x-paddle-button :checkout="$checkout" class="px-8 py-4">
Subscribe
</x-paddle-button>用户完成结帐后,Paddle 将调度一个 subscription_created webhook。收银员将收到此 Webhook 并为您的客户设置订阅。为了确保您的应用程序正确接收和处理所有 Webhook,请确保您已正确设置 Webhook 处理。
检查订阅状态
用户订阅您的应用程序后,您可以使用各种便捷的方法检查他们的订阅状态。首先,如果用户具有有效的订阅,则 subscribed 方法返回 true,即使订阅当前在其试用期内也是如此:
php
if ($user->subscribed()) {
// ...
}如果您的应用程序提供多个订阅,您可以在调用 subscribed 方法时指定订阅:
php
if ($user->subscribed('default')) {
// ...
}subscribed 方法也是路由中间件的绝佳候选者,它允许你根据用户的订阅状态过滤对路由和控制器的访问:
php
<?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()) {
// This user is not a paying customer...
return redirect('/billing');
}
return $next($request);
}
}如果您想确定用户是否仍在试用期内,您可以使用 onTrial 方法。此方法可用于确定是否应向用户显示警告,表明他们仍在试用期内:
php
if ($user->subscription()->onTrial()) {
// ...
}subscribedToPrice 方法可用于根据给定的 Paddle 价格 ID 确定用户是否订阅了给定的计划。在此示例中,我们将确定用户的默认订阅是否主动订阅了每月价格:
php
if ($user->subscribedToPrice($monthly = 'pri_123', 'default')) {
// ...
}recurring 方法可用于确定用户当前是否处于有效订阅中,并且不再处于试用期或宽限期内:
php
if ($user->subscription()->recurring()) {
// ...
}已取消订阅状态
要确定用户是否曾经是活跃订阅者但已取消订阅,您可以使用 canceled 方法:
php
if ($user->subscription()->canceled()) {
// ...
}您还可以确定用户是否已取消订阅,但在订阅完全到期之前仍处于“宽限期”中。例如,如果用户在 3 月 5 日取消订阅,而该订阅原定于 3 月 10 日到期,则该用户将处于 3 月 10 日之前的“宽限期”。此外,在此期间,subscribed 方法仍将返回 true:
php
if ($user->subscription()->onGracePeriod()) {
// ...
}逾期状态
如果订阅付款失败,则会被标记为 past_due。当您的订阅处于此状态时,在客户更新其付款信息之前,它不会处于活动状态。您可以使用订阅实例上的 pastDue 方法确定订阅是否逾期:
php
if ($user->subscription()->pastDue()) {
// ...
}当订阅逾期时,您应该指示用户更新其付款信息。
如果您希望订阅在past_due时仍被视为有效,您可以使用 Cashier 提供 keepPastDueSubscriptionsActive 的方法。通常,应在 AppServiceProvider 的 register 方法中调用此方法:
php
use Laravel\Paddle\Cashier;
/**
* Register any application services.
*/
public function register(): void
{
Cashier::keepPastDueSubscriptionsActive();
}WARNING
当订阅处于past_due状态时,在更新付款信息之前无法更改。因此,当订阅处于 past_due 状态时,swap 和 updateQuantity 方法将引发异常。
订阅范围
大多数订阅状态也可用作查询范围,以便您可以轻松地在数据库中查询处于给定状态的订阅:
php
//获取所有有效订阅...
$subscriptions = Subscription::query()->valid()->get();
//获取用户的所有已取消订阅...
$subscriptions = $user->subscriptions()->canceled()->get();可用范围的完整列表如下:
php
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:
php
//收取单一价格...
$response = $user->subscription()->charge('pri_123');
//一次收取多个价格...
$response = $user->subscription()->charge(['pri_123', 'pri_456']);在客户订阅的下一个计费周期之前,该收费方法实际上不会向客户收费。如果您想立即向客户收费,可以改用 chargeAndInvoice 方法:
php
$response = $user->subscription()->chargeAndInvoice('pri_123');更新付款信息
Paddle 始终为每个订阅保存一种付款方式。如果您想更新订阅的默认支付方式,您应该使用订阅模式下的 redirectToUpdatePaymentMethod 方法将您的客户重定向到 Paddle 的托管支付方式更新页面:
php
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 方法:
php
use App\Models\User;
$user = User::find(1);
$user->subscription()->swap($premium = 'pri_456');如果您想交换计划并立即向用户开具发票,而不是等待他们的下一个计费周期,则可以使用 swapAndInvoice 方法:
php
$user = User::find(1);
$user->subscription()->swapAndInvoice($premium = 'pri_456');按比例分配
默认情况下,Paddle 在计划之间切换时按比例分配费用。noProrate 方法可用于更新订阅,而无需按比例分配费用:
php
$user->subscription('default')->noProrate()->swap($premium = 'pri_456');如果您想禁用按比例分配并立即向客户开具发票,则可以将 swapAndInvoice 方法与 noProrate 结合使用:
php
$user->subscription('default')->noProrate()->swapAndInvoice($premium = 'pri_456');或者,要不向客户收取订阅更改费用,您可以使用 doNotBill 方法:
php
$user->subscription('default')->doNotBill()->swap($premium = 'pri_456');有关 Paddle 的按比例计费策略的更多信息,请参阅 Paddle 的按比例计费文档。
认购数量
有时订阅会受到 “数量” 的影响。例如,项目管理应用程序可能每个项目每月收费 10 USD。若要轻松增加或减少订阅的数量,请使用 incrementQuantity 和 decrementQuantity 方法:
php
$user = User::find(1);
$user->subscription()->incrementQuantity();
//将订阅的当前数量增加 5...
$user->subscription()->incrementQuantity(5);
$user->subscription()->decrementQuantity();
//从订阅的当前数量中减去 5...
$user->subscription()->decrementQuantity(5);或者,您可以使用 updateQuantity 方法设置特定数量:
php
$user->subscription()->updateQuantity(10);noProrate 方法可用于更新订阅的数量,而无需按比例分配费用:
php
$user->subscription()->noProrate()->updateQuantity(10);具有多个产品的订阅数量
如果您的订阅是包含多个产品的订阅,则应将要递增或递减数量的价格的 ID 作为第二个参数传递给 increment / decrement 方法:
php
$user->subscription()->incrementQuantity(1, 'price_chat');具有多个产品的订阅
具有多个产品的订阅允许您将多个计费产品分配给单个订阅。例如,假设您正在构建一个客户服务“帮助台”应用程序,该应用程序的基本订阅价格为每月 10 美元,但提供实时聊天附加产品,每月额外支付 15 美元。
在创建订阅结帐会话时,您可以通过将 prices 数组作为第一个参数传递给 subscribe 方法,为给定订阅指定多个产品:
php
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$checkout = $request->user()->subscribe([
'price_monthly',
'price_chat',
]);
return view('billing', ['checkout' => $checkout]);
});在上面的示例中,客户将为其默认订阅附加两个价格。这两个价格都将按各自的计费间隔收费。如有必要,您可以传递键/值对的关联数组来指示每个价格的特定数量:
php
$user = User::find(1);
$checkout = $user->subscribe('default', ['price_monthly', 'price_chat' => 5]);如果您想为现有订阅添加其他价格,则必须使用订阅的 swap method。调用 swap 方法时,您还应该包括订阅的当前价格和数量:
php
$user = User::find(1);
$user->subscription()->swap(['price_chat', 'price_original' => 2]);上面的示例将添加新价格,但客户在下一个计费周期之前不会被收取费用。如果您想立即向客户收费,可以使用 swapAndInvoice 方法:
php
$user->subscription()->swapAndInvoice(['price_chat', 'price_original' => 2]);您可以使用 swap 方法从订阅中删除价格,并省略要删除的价格:
php
$user->subscription()->swap(['price_original' => 2]);WARNING
您不能删除订阅的最后价格。相反,您应该简单地取消订阅。
多个订阅
Paddle 允许您的客户同时拥有多个订阅。例如,您可以经营一家提供游泳订阅和举重订阅的健身房,并且每个订阅的定价可能不同。当然,客户应该能够订阅其中一个或两个计划。
当您的应用程序创建订阅时,您可以将订阅的类型作为第二个参数提供给 subscribe 方法。type 可以是表示用户正在启动的订阅类型的任何字符串:
php
use Illuminate\Http\Request;
Route::post('/swimming/subscribe', function (Request $request) {
$checkout = $request->user()->subscribe($swimmingMonthly = 'pri_123', 'swimming');
return view('billing', ['checkout' => $checkout]);
});在此示例中,我们为客户启动了每月游泳订阅。但是,他们可能希望稍后切换到年度订阅。在调整客户的订阅时,我们可以简单地交换游泳订阅的价格:
php
$user->subscription('swimming')->swap($swimmingYearly = 'pri_456');当然,您也可以完全取消订阅:
php
$user->subscription('swimming')->cancel();暂停订阅
要暂停订阅,请对用户的订阅调用 pause 方法:
php
$user->subscription()->pause();当订阅暂停时,收银员将自动在您的数据库中设置 paused_at 列。此列用于确定 paused 方法何时应开始返回 true。例如,如果客户在 3 月 1 日暂停订阅,但订阅未计划在 3 月 5 日之前重复,则暂停的方法将继续返回 false,直到 3 月 5 日。这是因为通常允许用户继续使用应用程序,直到其计费周期结束。
默认情况下,暂停发生在下一个计费间隔,以便客户可以使用他们支付的剩余时间段。如果您想立即暂停订阅,您可以使用 pauseNow 方法:
php
$user->subscription()->pauseNow();使用 pauseUntil 方法,你可以暂停订阅,直到某个特定时刻:
php
$user->subscription()->pauseUntil(now()->addMonth());或者,您可以使用 pauseNowUntil 方法立即暂停订阅,直到给定的时间点:
php
$user->subscription()->pauseNowUntil(now()->addMonth());您可以使用 onPausedGracePeriod 方法确定用户是否已暂停订阅,但仍处于其“宽限期”中:
php
if ($user->subscription()->onPausedGracePeriod()) {
// ...
}要恢复已暂停的订阅,您可以在订阅上调用 resume 方法:
php
$user->subscription()->resume();WARNING
订阅在暂停时无法修改。如果要切换到其他计划或更新数量,则必须先恢复订阅。
取消订阅
要取消订阅,请对用户的订阅调用 cancel 方法:
php
$user->subscription()->cancel();取消订阅后,收银员将自动在您的数据库中设置 ends_at 列。此列用于确定 subscribed 方法何时应开始返回 false。例如,如果客户在 3 月 1 日取消订阅,但订阅未计划在 3 月 5 日之前结束,则 subscribed 方法将继续返回 true,直到 3 月 5 日。这样做是因为通常允许用户继续使用应用程序,直到其计费周期结束。
您可以使用 onGracePeriod 方法确定用户是否已取消订阅,但仍处于其“宽限期”中:
php
if ($user->subscription()->onGracePeriod()) {
// ...
}如果您想立即取消订阅,您可以在订阅上调用 cancelNow 方法:
php
$user->subscription()->cancelNow();要停止宽限期内的订阅取消,您可以调用 stopCancelation 方法:
php
$user->subscription()->stopCancelation();WARNING
Paddle 的订阅在取消订阅后无法恢复。如果您的客户希望恢复其订阅,则必须创建一个新订阅。
订阅试用
预先提供付款方式
如果您想为客户提供试用期,同时仍预先收集付款方式信息,则应在 Paddle 控制面板中针对客户订阅的价格设置试用时间。然后,照常启动结帐会话:
php
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 在此日期之后才开始向客户收费。
WARNING
如果客户的订阅未在试用结束日期之前取消,则将在试用期满后立即向他们收费,因此您应确保通知用户其试用结束日期。
您可以使用用户实例的 onTrial 方法或订阅实例的 onTrial 方法来确定用户是否在其试用期内。下面的两个示例是等效的:
php
if ($user->onTrial()) {
// ...
}
if ($user->subscription()->onTrial()) {
// ...
}要确定现有试用版是否已过期,您可以使用 hasExpiredTrial 方法:
php
if ($user->hasExpiredTrial()) {
// ...
}
if ($user->subscription()->hasExpiredTrial()) {
// ...
}要确定用户是否正在试用特定的订阅类型,您可以将类型提供给 onTrial 或 hasExpiredTrial 方法:
php
if ($user->onTrial('default')) {
// ...
}
if ($user->hasExpiredTrial('default')) {
// ...
}无需预先提供付款方式
如果您想在不预先收集用户的付款方式信息的情况下提供试用期,则可以将附加到用户的客户记录上的trial_ends_at列设置为所需的试用结束日期。这通常在用户注册期间完成:
php
use App\Models\User;
$user = User::create([
// ...
]);
$user->createAsCustomer([
'trial_ends_at' => now()->addDays(10)
]);Cashier 将这种类型的试用称为“通用试用”,因为它未附加到任何现有订阅。如果当前日期未超过 trial_ends_at的值,则 User 实例上的 onTrial 方法将返回 true:
php
if ($user->onTrial()) {
// User is within their trial period...
}准备好为用户创建实际订阅后,您可以像往常一样使用 subscribe 方法:
php
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 date 实例,如果用户未试用,则返回 null。如果您想获取除默认订阅以外的特定订阅的试用结束日期,您还可以传递可选的订阅类型参数:
php
if ($user->onTrial('default')) {
$trialEndsAt = $user->trialEndsAt();
}如果你希望明确知道用户在其 “通用” 试用期内,并且尚未创建实际订阅,则可以使用 onGenericTrial 方法:
php
if ($user->onGenericTrial()) {
// User is within their "generic" trial period...
}延长或激活试用
您可以通过调用 extendTrial 方法并指定试用应结束的时刻来延长订阅的现有试用期:
php
$user->subscription()->extendTrial(now()->addDays(5));或者,您可以通过对订阅调用 activate 方法结束试用来立即激活订阅:
php
$user->subscription()->activate();处理 Paddle Webhook
Paddle 可以通过 webhook 通知您的应用程序各种事件。默认情况下,指向 Cashier 的 webhook 控制器的路由由 Cashier 服务提供商注册。此控制器将处理所有传入的 webhook 请求。
默认情况下,此控制器将自动处理取消收费失败次数过多的订阅、订阅更新和付款方式更改;但是,我们很快就会发现,您可以扩展此控制器以处理您喜欢的任何 Paddle webhook 事件。
为确保您的应用程序可以处理 Paddle Webhook,请务必在 Paddle 控制面板中配置 Webhook URL。默认情况下,收银员的 webhook 控制器响应 /paddle/webhook URL 路径。您应该在 Paddle 控制面板中启用的所有 webhook 的完整列表是:
- Customer Updated 客户更新
- Transaction Completed 交易已完成
- Transaction Updated 事务更新
- Subscription Created 已创建订阅
- Subscription Updated 订阅已更新
- Subscription Paused 订阅已暂停
- Subscription Canceled 订阅已取消
WARNING
确保使用 Cashier 附带的 webhook 签名验证中间件保护传入的请求。
Webhook 和 CSRF 保护
由于 Paddle webhook 需要绕过 Laravel 的 CSRF 保护,因此您应该确保 Laravel 不会尝试验证传入的 Paddle webhook 的 CSRF 令牌。为此,您应该在应用程序的 bootstrap/app.php 文件中从 CSRF 保护中排除 paddle/* :
php
->withMiddleware(function (Middleware $middleware) {
$middleware->validateCsrfTokens(except: [
'paddle/*',
]);
})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,您可以注册一个将处理该事件的侦听器:
php
<?php
namespace App\Listeners;
use Laravel\Paddle\Events\WebhookReceived;
class PaddleEventListener
{
/**
* Handle received Paddle webhooks.
*/
public function handle(WebhookReceived $event): void
{
if ($event->payload['event_type'] === 'transaction.billed') {
// Handle the incoming event...
}
}
}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 匹配:
ini
CASHIER_WEBHOOK=https://example.com/my-paddle-webhook-url验证 Webhook 签名
为了保护您的 webhook,您可以使用 Paddle 的 webhook 签名。为方便起见,Cashier 会自动包含一个中间件,用于验证传入的 Paddle webhook 请求是否有效。
要启用 Webhook 验证,请确保在应用程序的 .env 文件中定义 PADDLE_WEBHOOK_SECRET 环境变量。可以从您的 Paddle 帐户仪表板中检索 webhook 密钥。
单次收费
产品充电
如果您想为客户发起产品购买,您可以在可计费模型实例上使用 checkout 方法为购买生成结帐会话。结帐方法接受一个或多个价格 ID。如有必要,可以使用关联数组来提供所购买产品的数量:
php
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 提供的桨式按钮Blade 组件,允许用户查看 Paddle 结帐小部件并完成购买:
blade
<x-paddle-button :checkout="$checkout" class="px-8 py-4">
Buy
</x-paddle-button>checkout 会话具有 customData 方法,允许您将希望的任何自定义数据传递给底层事务创建。请查阅 Paddle 文档,以了解有关传递自定义数据时可用的选项的更多信息:
php
$checkout = $user->checkout('pri_tshirt')
->customData([
'custom_option' => $value,
]);交易退款
退款交易会将退款金额退还至客户在购买时使用的付款方式。如果您需要对 Paddle 购买进行退款,您可以使用 Cashier\Paddle\Transaction 模式的退款方式。此方法接受 reason 作为第一个参数,一个或多个要退款的价格 ID 以及可选金额作为关联数组。您可以使用 transactions 方法检索给定计费模型的事务。
例如,假设我们想要针对 pri_123 和 pri_456 的价格对特定交易进行退款。我们想全额退款pri_123,但只退还 2 美元的pri_456:
php
use App\Models\User;
$user = User::find(1);
$transaction = $user->transactions()->first();
$response = $transaction->refund('Accidental charge', [
'pri_123', // Fully refund this price...
'pri_456' => 200, // Only partially refund this price...
]);上面的示例对交易中的特定订单项进行退款。如果您想退还整个交易,只需提供原因:
php
$response = $transaction->refund('Accidental charge');有关退款的更多信息,请查阅 Paddle 的退款文档。
WARNING
退款必须始终得到 Paddle 的批准才能完全处理。
贷记交易
就像退款一样,您也可以将交易记入贷方。贷记交易会将资金添加到客户的余额中,以便可用于未来的购买。由于 Paddle 会自动处理订阅积分,因此只能对手动收集的交易进行积分处理,而不能对自动收集的交易(如订阅)进行信用:
php
$transaction = $user->transactions()->first();
// Credit a specific line item fully...
$response = $transaction->credit('Compensation', 'pri_123');有关更多信息,请参阅 Paddle 的积分文档。
WARNING
积分只能应用于手动收集的交易。自动收集的交易由 Paddle 自己记入贷方。
交易
您可以通过 transactions 属性轻松检索可计费模型的事务数组:
php
use App\Models\User;
$user = User::find(1);
$transactions = $user->transactions;交易代表您的产品和购买的付款,并附有发票。只有已完成的事务才会存储在应用程序的数据库中。
在为客户列出交易时,您可以使用交易实例的方法显示相关的付款信息。例如,您可能希望在一个表格中列出每笔交易,以便用户轻松下载任何发票:
html
<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">Download</a></td>
</tr>
@endforeach
</table>download-invoice 路由可能如下所示:
php
use Illuminate\Http\Request;
use Laravel\Paddle\Transaction;
Route::get('/download-invoice/{transaction}', function (Request $request, Transaction $transaction) {
return $transaction->redirectToInvoicePdf();
})->name('download-invoice');过去和即将到来的付款
您可以使用 lastPayment 和 nextPayment 方法来检索和显示客户过去或即将到来的定期订阅付款:
php
use App\Models\User;
$user = User::find(1);
$subscription = $user->subscription();
$lastPayment = $subscription->lastPayment();
$nextPayment = $subscription->nextPayment();这两种方法都会返回一个 Laravel\Paddle\Payment 实例;但是,当 Webhook 尚未同步交易时,lastPayment 将返回 null,而当计费周期结束时(例如,当订阅已取消时),nextPayment 将返回 null:
blade
Next payment: {{ $nextPayment->amount() }} due on {{ $nextPayment->date()->format('d/m/Y') }}测试
在测试时,您应该手动测试您的计费流程,以确保您的集成按预期工作。
对于自动化测试,包括在 CI 环境中执行的测试,您可以使用 Laravel 的 HTTP Client 来伪造对 Paddle 的 HTTP 调用。虽然这不会测试来自 Paddle 的实际响应,但它确实提供了一种无需实际调用 Paddle API 即可测试应用程序的方法。
