跳至内容

Laravel 缓存 (Paddle)

简介

exclamation

本文档适用于 Cashier Paddle 2.x 与 Paddle Billing 的集成。如果您仍在使用 Paddle Classic,则应使用 Cashier Paddle 1.x

Laravel Cashier PaddlePaddle 的订阅计费服务提供了一个富有表现力且流畅的接口。它处理了您最头疼的大部分样板订阅计费代码。除了基本的订阅管理之外,Cashier 还能够处理:更换订阅、订阅“数量”、订阅暂停、取消宽限期等等。

在深入研究 Cashier Paddle 之前,我们建议您也查看 Paddle 的 概念指南API 文档

升级缓存

升级到 Cashier 的新版本时,务必仔细查看 升级指南

安装

首先,使用 Composer 包管理器为 Paddle 安装 Cashier 包

composer require laravel/cashier-paddle

接下来,您应该使用 vendor:publish Artisan 命令发布 Cashier 迁移文件

php artisan vendor:publish --tag="cashier-migrations"

然后,您应该运行应用程序的数据库迁移。Cashier 迁移将创建一个新的 customers 表。此外,还将创建新的 subscriptionssubscription_items 表来存储所有客户的订阅。最后,将创建一个新的 transactions 表来存储与您的客户关联的所有 Paddle 交易

php artisan migrate
exclamation

为了确保 Cashier 正确处理所有 Paddle 事件,请记住 设置 Cashier 的 Webhook 处理

Paddle 沙盒

在本地和登台开发期间,您应该 注册一个 Paddle 沙盒帐户。此帐户将为您提供一个沙盒环境,以便在不进行实际付款的情况下测试和开发您的应用程序。您可以使用 Paddle 的 测试卡号 模拟各种付款场景。

使用 Paddle 沙盒环境时,您应该在应用程序的 .env 文件中将 PADDLE_SANDBOX 环境变量设置为 true

PADDLE_SANDBOX=true

完成应用程序开发后,您可以 申请 Paddle 卖家帐户。在将您的应用程序投入生产之前,Paddle 需要批准您的应用程序域名。

配置

可计费模型

在使用 Cashier 之前,您必须将 Billable 特性添加到用户模型定义中。此特性提供了各种方法,使您可以执行常见的计费任务,例如创建订阅和更新付款信息

use Laravel\Paddle\Billable;
 
class User extends Authenticatable
{
use Billable;
}

如果您有不可计费的实体(而非用户),也可以将特性添加到这些类中

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 结账窗口小部件。您可以通过将 @paddleJS Blade 指令放在应用程序布局的结束 </head> 标记之前来加载 JavaScript 库

<head>
...
 
@paddleJS
</head>

货币配置

您可以指定在格式化用于在发票上显示的货币值时要使用的区域设置。在内部,Cashier 利用 PHP 的 NumberFormatter 来设置货币区域设置

CASHIER_CURRENCY_LOCALE=nl_BE
exclamation

为了使用除 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;
 
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Cashier::useSubscriptionModel(Subscription::class);
Cashier::useTransactionModel(Transaction::class);
}

快速入门

销售产品

lightbulb

在利用 Paddle 结账之前,您应该在 Paddle 仪表板中定义具有固定价格的产品。此外,您还应 配置 Paddle 的 Webhook 处理

通过您的应用程序提供产品和订阅计费可能会让人望而生畏。但是,借助 Cashier 和 Paddle 的结账覆盖层,您可以轻松构建现代、强大的支付集成。

要向客户收取非循环的单次收费产品费用,我们将利用 Cashier 通过 Paddle 的结账覆盖层向客户收取费用,客户将在其中提供其付款详细信息并确认其购买。通过结账覆盖层进行付款后,客户将被重定向到您在应用程序中选择的成功 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 结账覆盖层。使用 Paddle 时,“价格”指的是 特定产品的已定义价格

如有必要,checkout 方法将自动在 Paddle 中创建客户,并将该 Paddle 客户记录连接到应用程序数据库中的相应用户。完成结账会话后,客户将被重定向到一个专用的成功页面,您可以在其中向客户显示信息消息。

buy 视图中,我们将包含一个按钮以显示结账覆盖层。paddle-button Blade 组件包含在 Cashier Paddle 中;但是,您也可以 手动呈现覆盖层结账

<x-paddle-button :checkout="$checkout" class="px-8 py-4">
Buy Product
</x-paddle-button>

向 Paddle 结账提供元数据

销售产品时,通常会通过您自己的应用程序定义的 CartOrder 模型跟踪已完成的订单和已购买的产品。将客户重定向到 Paddle 的结账覆盖层以完成购买时,您可能需要提供现有的订单标识符,以便在客户重定向回您的应用程序时将已完成的购买与相应的订单关联。

为此,您可以向 checkout 方法提供一个自定义数据数组。假设在用户开始结账流程时,在我们的应用程序中创建了一个挂起的 Order。请记住,此示例中的 CartOrder 模型仅用于说明,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方法向 Paddle 结账覆盖层提供订单 ID。

当然,您可能希望在客户完成结账流程后将订单标记为“已完成”。为此,您可以监听 Paddle 派发的 Webhook,并通过 Cashier 触发的事件将订单信息存储到您的数据库中。

首先,请监听 Cashier 派发的TransactionCompleted事件。通常,您应该在应用程序的AppServiceProviderboot方法中注册事件监听器。

use App\Listeners\CompleteOrder;
use Illuminate\Support\Facades\Event;
use Laravel\Paddle\Events\TransactionCompleted;
 
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Event::listen(TransactionCompleted::class, CompleteOrder::class);
}

在本例中,CompleteOrder监听器可能如下所示。

namespace App\Listeners;
 
use App\Models\Order;
use Laravel\Paddle\Cashier;
use Laravel\Paddle\Events\TransactionCompleted;
 
class CompleteOrder
{
/**
* Handle the incoming Cashier webhook event.
*/
public function handle(TransactionCompleted $event): void
{
$orderId = $event->payload['data']['custom_data']['order_id'] ?? null;
 
$order = Order::findOrFail($orderId);
 
$order->update(['status' => 'completed']);
}
}

有关transaction.completed事件包含的数据的更多信息,请参阅 Paddle 的文档数据包含在transaction.completed事件中

销售订阅

lightbulb

在利用 Paddle 结账之前,您应该在 Paddle 仪表板中定义具有固定价格的产品。此外,您还应 配置 Paddle 的 Webhook 处理

通过您的应用程序提供产品和订阅计费可能会让人望而生畏。但是,借助 Cashier 和 Paddle 的结账覆盖层,您可以轻松构建现代、强大的支付集成。

要了解如何使用 Cashier 和 Paddle 的结账覆盖层销售订阅,让我们考虑一个简单的订阅服务的场景,该服务具有基本的每月(price_basic_monthly)和每年(price_basic_yearly)套餐。这两个价格可以在 Paddle 仪表盘中分组到“基本”产品(pro_basic)下。此外,我们的订阅服务可能会提供一个专家套餐,如pro_expert

首先,让我们了解客户如何订阅我们的服务。当然,您可以想象客户可能会在我们的应用程序定价页面上点击“订阅”按钮以订阅基本套餐。此按钮将为他们选择的套餐调用 Paddle 结账覆盖层。首先,让我们通过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视图中,我们将包含一个按钮以显示结账覆盖层。paddle-button Blade 组件包含在 Cashier Paddle 中;但是,您也可以手动渲染覆盖层结账

<x-paddle-button :checkout="$checkout" class="px-8 py-4">
Subscribe
</x-paddle-button>

现在,当点击“订阅”按钮时,客户将能够输入其付款详细信息并启动其订阅。为了了解他们的订阅何时真正开始(因为某些付款方式需要几秒钟才能处理),您还应该配置 Cashier 的 Webhook 处理

现在客户可以开始订阅了,我们需要限制应用程序的某些部分,以便只有订阅用户才能访问它们。当然,我们始终可以通过 Cashier 的Billable特质提供的subscribed方法确定用户的当前订阅状态。

@if ($user->subscribed())
<p>You are subscribed.</p>
@endif

我们甚至可以轻松确定用户是否订阅了特定产品或价格。

@if ($user->subscribedToProduct('pro_basic'))
<p>You are subscribed to our Basic product.</p>
@endif
 
@if ($user->subscribedToPrice('price_basic_monthly'))
<p>You are subscribed to our monthly Basic plan.</p>
@endif

构建已订阅中间件

为了方便起见,您可能希望创建一个中间件,用于确定传入请求是否来自已订阅的用户。定义此中间件后,您可以轻松地将其分配给路由,以防止未订阅的用户访问该路由。

<?php
 
namespace App\Http\Middleware;
 
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
 
class Subscribed
{
/**
* Handle an incoming request.
*/
public function handle(Request $request, Closure $next): Response
{
if (! $request->user()?->subscribed()) {
// Redirect user to billing page and ask them to subscribe...
return redirect('/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); // With "$price" being "price_basic_yearly" for this example.
 
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');

现在,您的订阅将在其账单周期结束时取消。

lightbulb

只要您已配置 Cashier 的 Webhook 处理,Cashier 就会通过检查来自 Paddle 的传入 Webhook 自动使应用程序的 Cashier 相关数据库表保持同步。因此,例如,当您通过 Paddle 的仪表盘取消客户的订阅时,Cashier 将接收相应的 Webhook 并将订阅标记为应用程序数据库中的“已取消”。

结账会话

大多数向客户收费的操作都是使用 Paddle 的结账覆盖层小部件或利用内嵌结账通过“结账”执行的。

在使用 Paddle 处理结账付款之前,您应该在 Paddle 结账设置仪表盘中定义应用程序的默认付款链接

覆盖层结账

在显示结账覆盖层小部件之前,您必须使用 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-buttonBlade 组件。您可以将结账会话作为“prop”传递给此组件。然后,当点击此按钮时,将显示 Paddle 的结账小部件。

<x-paddle-button :checkout="$checkout" class="px-8 py-4">
Subscribe
</x-paddle-button>

默认情况下,这将使用 Paddle 的默认样式显示小部件。您可以通过向组件添加Paddle 支持的属性(如data-theme='light'属性)来自定义小部件。

<x-paddle-button :url="$payLink" class="px-8 py-4" data-theme="light">
Subscribe
</x-paddle-button>

Paddle 结账小部件是异步的。一旦用户在小部件中创建订阅,Paddle 将向您的应用程序发送一个 Webhook,以便您可以在应用程序的数据库中正确更新订阅状态。因此,正确设置 Webhook以适应 Paddle 的状态更改非常重要。

exclamation

订阅状态更改后,接收相应 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
>
Buy Product
</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::previewPrices(['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['name'] }} - {{ $price->subtotal() }} (+ {{ $price->tax() }} 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 允许您在创建结账会话时为您的客户定义一些有用的默认值。设置这些默认值可以让您预先填写客户的电子邮件地址和姓名,以便他们可以立即转到结账小部件的付款部分。您可以通过覆盖可计费模型上的以下方法来设置这些默认值。

/**
* 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 检索客户。此方法将返回可计费模型的实例。

use Laravel\Paddle\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方法的第二个参数应该是订阅的内部“类型”。如果您的应用程序仅提供单个订阅,则可以将其称为defaultprimary。此订阅类型仅供内部应用程序使用,不应显示给用户。此外,它不应包含空格,并且在创建订阅后不应更改。

您还可以使用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">
Subscribe
</x-paddle-button>

用户完成结账后,Paddle 会发送一个subscription_created webhook。Cashier 将接收此 webhook 并为您的客户设置订阅。为了确保您的应用程序正确接收和处理所有 webhook,请确保已正确设置 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
{
/**
* 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 方法。此方法可用于确定是否应向用户显示有关其仍处于试用期的警告。

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 方法。通常,此方法应在您的AppServiceProviderregister 方法中调用。

use Laravel\Paddle\Cashier;
 
/**
* Register any application services.
*/
public function register(): void
{
Cashier::keepPastDueSubscriptionsActive();
}
exclamation

当订阅处于past_due 状态时,在更新付款信息之前无法更改。因此,当订阅处于past_due 状态时,swapupdateQuantity 方法将引发异常。

订阅范围

大多数订阅状态也可以作为查询范围使用,以便您可以轻松地查询数据库中处于给定状态的订阅。

// Get all valid subscriptions...
$subscriptions = Subscription::query()->valid()->get();
 
// Get all of the canceled subscriptions for a user...
$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。

// Charge a single price...
$response = $user->subscription()->charge('pri_123');
 
// Charge multiple prices at once...
$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 美元。要轻松增加或减少订阅的数量,请使用incrementQuantitydecrementQuantity 方法。

$user = User::find(1);
 
$user->subscription()->incrementQuantity();
 
// Add five to the subscription's current quantity...
$user->subscription()->incrementQuantity(5);
 
$user->subscription()->decrementQuantity();
 
// Subtract five from the subscription's current quantity...
$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]);
exclamation

不能删除订阅上的最后一个价格。相反,应简单地取消订阅。

多个订阅

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();
exclamation

订阅在暂停期间无法修改。如果要切换到其他套餐或更新数量,则必须先恢复订阅。

取消订阅

要取消订阅,请调用用户订阅上的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();
exclamation

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 在此日期之后才开始向客户开具账单。

exclamation

如果客户在试用期结束日期之前没有取消订阅,他们将在试用期到期后立即被收费,因此您应确保通知用户他们的试用期结束日期。

您可以使用用户实例的onTrial方法或订阅实例的onTrial方法来确定用户是否处于试用期。以下两个示例是等效的

if ($user->onTrial()) {
// ...
}
 
if ($user->subscription()->onTrial()) {
// ...
}

要确定现有的试用期是否已过期,您可以使用hasExpiredTrial方法

if ($user->hasExpiredTrial()) {
// ...
}
 
if ($user->subscription()->hasExpiredTrial()) {
// ...
}

要确定用户是否正在试用特定订阅类型,您可以向onTrialhasExpiredTrial方法提供类型

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 将此类型的试用期称为“通用试用期”,因为它没有附加到任何现有订阅。如果当前日期没有超过trial_ends_at的值,则User实例上的onTrial方法将返回true

if ($user->onTrial()) {
// User is within their trial period...
}

准备好为用户创建实际订阅后,您可以像往常一样使用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()) {
// User is within their "generic" trial period...
}

延长或激活试用

您可以通过调用extendTrial方法并指定试用期应结束的时间点来延长订阅的现有试用期

$user->subscription()->extendTrial(now()->addDays(5));

或者,您可以通过调用订阅上的activate方法来结束其试用期,从而立即激活订阅

$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的完整列表如下

  • 客户更新
  • 交易完成
  • 交易更新
  • 订阅创建
  • 订阅更新
  • 订阅暂停
  • 订阅取消
exclamation

确保您使用Cashier包含的Webhook签名验证中间件来保护传入的请求。

Webhook和CSRF保护

由于Paddle Webhook需要绕过Laravel的CSRF保护,因此您应确保Laravel不会尝试验证传入Paddle Webhook的CSRF令牌。为此,您应在应用程序的bootstrap/app.php文件中将paddle/*排除在CSRF保护之外

->withMiddleware(function (Middleware $middleware) {
$middleware->validateCsrfTokens(except: [
'paddle/*',
]);
})

Webhook和本地开发

为了使Paddle能够在本地开发期间向您的应用程序发送Webhook,您需要通过网站共享服务(例如NgrokExpose)公开您的应用程序。如果您正在使用Laravel Sail在本地开发应用程序,则可以使用Sail的网站共享命令

定义 Webhook 事件处理程序

Cashier自动处理因收费失败和其他常见的Paddle Webhook而导致的订阅取消。但是,如果您还有其他想要处理的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
{
/**
* 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\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 签名

要保护您的Webhook,您可以使用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-buttonBlade组件允许用户查看Paddle结账窗口小部件并完成购买

<x-paddle-button :checkout="$checkout" class="px-8 py-4">
Buy
</x-paddle-button>

结账会话具有customData方法,允许您将任何自定义数据传递到基础交易创建。请参阅Paddle文档以了解在传递自定义数据时可用的选项

$checkout = $user->checkout('pri_tshirt')
->customData([
'custom_option' => $value,
]);

退款交易

退款交易将把退款金额退还给客户在购买时使用的付款方式。如果您需要退款Paddle购买,则可以使用Cashier\Paddle\Transaction模型上的refund方法。此方法接受原因作为第一个参数,一个或多个要退款的价格ID以及可选金额作为关联数组。您可以使用transactions方法检索给定可计费模型的交易。

例如,假设我们要为价格pri_123pri_456退款特定交易。我们希望完全退款pri_123,但仅为pri_456退款两美元

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...
]);

以上示例退还交易中的特定项目。如果要退款整个交易,只需提供一个原因即可

$response = $transaction->refund('Accidental charge');

有关退款的更多信息,请参阅Paddle的退款文档

exclamation

退款必须始终先获得Paddle批准才能完全处理。

为交易记入贷方

与退款一样,您也可以为交易记入贷方。为交易记入贷方将把资金添加到客户的余额中,以便将来用于购买。为交易记入贷方只能对手动收集的交易进行,不能对自动收集的交易(如订阅)进行,因为Paddle会自动处理订阅贷方

$transaction = $user->transactions()->first();
 
// Credit a specific line item fully...
$response = $transaction->credit('Compensation', 'pri_123');

有关更多信息,请查看Paddle关于记入贷方的文档

exclamation

贷方只能应用于手动收集的交易。自动收集的交易由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">Download</a></td>
</tr>
@endforeach
</table>

download-invoice路由可能如下所示

use Illuminate\Http\Request;
use Laravel\Paddle\Transaction;
 
Route::get('/download-invoice/{transaction}', function (Request $request, Transaction $transaction) {
return $transaction->redirectToInvoicePdf();
})->name('download-invoice');

过去和即将到来的付款

您可以使用lastPaymentnextPayment方法检索和显示客户过去或即将到来的定期订阅付款

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

Next payment: {{ $nextPayment->amount() }} due on {{ $nextPayment->date()->format('d/m/Y') }}

测试

在测试期间,您应该手动测试您的计费流程,以确保您的集成按预期工作。

对于自动化测试(包括在CI环境中执行的测试),您可以使用Laravel的HTTP客户端来模拟对Paddle发出的HTTP调用。虽然这不会测试Paddle的实际响应,但它确实提供了一种无需实际调用Paddle的API即可测试应用程序的方法。