[!WARNING]
此文档适用于 Cashier Paddle 2.x 与 Paddle Billing 的集成。如果您仍在使用 Paddle Classic,您应该使用 Cashier Paddle 1.x.
Laravel Cashier Paddle 为 Paddle 的 订阅计费服务提供了一个富有表现力、流畅的接口。它处理了几乎所有你所担心的繁琐订阅计费代码。除了基本的订阅管理,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 表。此外,还会创建新的 subscriptions 和 subscription_items 表,用于存储客户的所有订阅。最后,将创建一个新的 transactions 表,用于存储与客户关联的所有 Paddle 交易:
php artisan migrate[!WARNING]
为了确保 Cashier 能够正确处理所有 Paddle 事件,请记住设置 Cashier 的 webhook 处理。
在本地和预演环境开发期间,您应该注册一个 Paddle 沙盒账户。此账户将为您提供一个沙盒环境,以测试和开发您的应用程序而无需进行实际支付。您可以使用 Paddle 的测试卡号来模拟各种支付场景。
在使用 Paddle Sandbox 环境时,您应该将 PADDLE_SANDBOX 环境变量设置为 true 在您应用程序的 .env 文件中:
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;
}接下来,您应该在您应用的.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=truePADDLE_SANDBOX 环境变量在您使用 Paddle 的沙盒环境 时应设置为 true。如果您将应用程序部署到生产环境并使用 Paddle 的实时供应商环境,则 PADDLE_SANDBOX 变量应设置为 false。
此 PADDLE_RETAIN_KEY 是可选的 并且 只应在您将 Paddle 与 Retain 结合使用时设置。
Paddle 依赖自己的 JavaScript 库来启动 Paddle 结账小部件。你可以在应用程序布局的结束 </head> 标签之前放置 @paddleJS Blade 指令来加载 JavaScript 库:
<head>
...
@paddleJS
</head>您可以指定一个区域设置,用于格式化发票上显示的金额。在内部,Cashier 利用 PHP 的 NumberFormatter 类 来设置货币区域设置:
CASHIER_CURRENCY_LOCALE=nl_BE[!警告]
为了使用en之外的其他区域设置,请确保服务器上已安装并配置了ext-intlPHP 扩展。
您可以自由地扩展 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);
}[!注意]
在使用 Paddle Checkout 之前,您应该在您的 Paddle 控制面板中定义具有固定价格的产品。此外,您应该配置 Paddle 的 Webhook 处理。
通过你的应用程序提供产品和订阅计费可能令人望而生畏。然而,感谢 Cashier 和 Paddle 的结账叠加层,你可以轻松构建现代化、健壮的支付集成。
为了向客户收取非经常性、一次性收费产品的费用,我们将利用 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 结账叠加层,用于给定的“价格标识符”。当使用 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>当销售产品时,通常会通过您自己的应用程序定义的 Cart 和 Order 模型来跟踪已完成的订单和已购买的产品。将客户重定向到 Paddle 的结账叠加层以完成购买时,您可能需要提供一个现有的订单标识符,以便在客户重定向回您的应用程序时,您可以将已完成的购买与相应的订单关联起来。
为此,你可以提供一个自定义数据数组给 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');如您在上述示例中看到的,当用户开始结账流程时,我们将向 checkout 方法提供所有购物车/订单关联的 Paddle 价格标识符。当然,您的应用程序负责在客户添加商品时,将这些商品与“购物车”或订单关联起来。我们还通过 customData 方法向 Paddle 结账叠加层提供订单 ID。
当然,一旦客户完成了结账流程,您很可能希望将订单标记为“已完成”。 为实现此目的,您可以监听 Paddle 派发的并通过 Cashier 通过事件引发的 webhook,以将订单信息存储到您的数据库中。
要开始使用,监听 Cashier 调度的 TransactionCompleted 事件。通常,您应该在应用程序的 AppServiceProvider 的 boot 方法中注册事件监听器:
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']);
}
}请参阅 Paddle 的文档,了解有关 transaction.completed 事件所包含的数据 的更多信息。
[!NOTE]
在使用 Paddle Checkout 之前,您应该在 Paddle 后台定义定价固定的产品。此外,您应该配置 Paddle 的 webhook 处理。
在您的应用程序中提供产品和订阅计费可能令人望而却步。 然而,感谢 Cashier 和 Paddle 的结账叠加层,您可以轻松构建现代化、强大的支付集成。
为了学习如何使用 Cashier 和 Paddle 的 Checkout Overlay 销售订阅,让我们考虑一个简单的订阅服务场景,该服务包含一个基本月度 (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 视图中,我们将包含一个按钮以显示 Checkout 叠加层。paddle-button Blade 组件包含在 Cashier Paddle 中;但是,您也可以 手动渲染叠加层结账:
<x-paddle-button :checkout="$checkout" class="px-8 py-4">
Subscribe
</x-paddle-button>现在,当订阅按钮被点击时,客户将能够输入其付款详情并开始订阅。为了知道他们的订阅何时真正开始 (因为某些付款方式需要几秒钟来处理),您还应该配置 Cashier 的 webhook 处理。
既然客户可以开始订阅了,我们需要限制应用程序的某些部分,以便只有订阅用户才能访问它们。当然,我们总是可以通过 Cashier 的 Billable trait 提供的 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');现在您的订阅将在其账单周期结束时被取消.
[!注意]
只要您已配置 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-button Blade 组件。您可以将结账会话作为“prop”传递给此组件。然后,当此按钮被点击时,Paddle 的结账小部件将会显示:
<x-paddle-button :checkout="$checkout" class="px-8 py-4">
Subscribe
</x-paddle-button>默认情况下,这将使用 Paddle 的默认样式显示该小部件。您可以通过添加 Paddle 支持的属性 例如 data-theme='light' 属性到组件中:
<x-paddle-button :checkout="$checkout" class="px-8 py-4" data-theme="light">
Subscribe
</x-paddle-button>Paddle 结账小部件是异步的。一旦用户在小部件内创建订阅,Paddle 将向您的应用程序发送一个 webhook,以便您可以在应用程序的数据库中正确更新订阅状态。因此,您务必正确设置 webhook 以适应来自 Paddle 的状态更改。
[!WARNING]
订阅状态更改后,接收相应 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 = 'pri_123', 'default')
->returnTo(route('home'));
return view('billing', ['checkout' => $checkout]);
});传递给 subscribe 方法的第一个参数是用户正在订阅的特定价格。此值应与 Paddle 中价格的标识符对应。returnTo 方法接受一个 URL,用户成功完成结账后将被重定向到该 URL。传递给 subscribe 方法的第二个参数应该是订阅的内部“类型”。如果您的应用程序只提供一个订阅,您可能会将其命名为 default 或 primary。此订阅类型仅供应用程序内部使用,不应向用户显示。此外,它不应包含空格,并且在创建订阅后绝不应更改。
您还可以使用 customData 方法提供一个关于该订阅的自定义元数据数组:
$checkout = $request->user()->subscribe($premium = 'pri_123', 'default')
->customData(['key' => 'value'])
->returnTo(route('home'));一旦订阅结账会话已创建,可以将该结账会话提供给 paddle-button Blade 组件 随附于 Cashier Paddle:
<x-paddle-button :checkout="$checkout" class="px-8 py-4">
Subscribe
</x-paddle-button>在用户完成结账后,一个 subscription_created webhook 将会从 Paddle 分发。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 方法. 通常, 此方法应在您的 AppServiceProvider 的 register 方法中调用:
use Laravel\Paddle\Cashier;
/**
* Register any application services.
*/
public function register(): void
{
Cashier::keepPastDueSubscriptionsActive();
}
[!WARNING] 当订阅处于past_due状态时,在支付信息更新之前无法进行更改。因此,当订阅处于past_due状态时,swap和updateQuantity方法将抛出异常。
大多数订阅状态也可用作查询范围以便您可以轻松地查询您的数据库以获取处于给定状态的订阅:
// 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 总是为每份订阅保存一种支付方式。如果您想更新订阅的默认支付方式,您应该使用订阅模型上的 redirectToUpdateUpdatePaymentMethod 方法将您的客户重定向到 Paddle 托管的支付方式更新页面:
use Illuminate\Http\Request;
Route::get('/update-payment-method', function (Request $request) {
$user = $request->user();
return $user->subscription()->redirectToUpdatePaymentMethod();
});当用户完成信息更新后,一个 subscription_updated webhook 将由 Paddle 发出,并且订阅详情将在您的应用程序数据库中被更新。
用户订阅您的应用程序后,他们可能偶尔会想更换新的订阅计划。为了更新用户的订阅计划,您应该将 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();
// 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 作为第二个参数传递给 increment / decrement 方法:
$user->subscription()->incrementQuantity(1, 'price_chat');Subscription with multiple products allow you to assign multiple billing products to a single subscription. For example, imagine you are building a customer service "helpdesk" application that has a base subscription price of $10 per month but offers a live chat add-on product for an additional $15 per month.
在创建订阅结账会话时,您可以通过将价格数组作为第一个参数传递给 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]);[!WARNING]
您不能删除订阅的最后一个价格。相反,您应该直接取消订阅。
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()) {
// ...
} `
$user->subscription()->resume();[!WARNING]
订阅暂停时无法修改。如果您想更换到不同的套餐或更新数量,您必须先恢复订阅。
To cancel a subscription, call the cancel method on the user's subscription:
$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();[!WARNING]
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 在此日期之后才开始向客户收费。
[!WARNING]
如果客户的订阅在试用结束日期前未取消,他们将在试用期一到期就被收费,因此您务必通知您的用户他们的试用结束日期。
您可以通过以下方法判断用户是否处于试用期,其中之一是用户实例的 onTrial 方法:
if ($user->onTrial()) {
// ...
}要确定现有试用是否已过期,您可以使用 hasExpiredTrial 方法:
if ($user->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()) {
// User is within their trial period...
}一旦您准备好为用户创建一个实际的订阅,您就可以像往常一样使用 subscribe 方法:
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]);
});要检索用户的试用结束日期,您可以使用 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 将各种事件通知您的应用程序。默认情况下,Cashier 服务提供商会注册一个指向 Cashier 的 webhook 控制器的路由。该控制器将处理所有传入的 webhook 请求。
默认情况下,此控制器将自动处理取消失败扣款过多的订阅、订阅更新以及支付方式变更;但是,正如我们即将发现的,您可以扩展此控制器来处理您喜欢的任何 Paddle webhook 事件。
为确保您的应用能够处理 Paddle webhook,请务必 在 Paddle 控制面板中配置 webhook URL。默认情况下,Cashier 的 webhook 控制器会响应 /paddle/webhook URL 路径。您应该在 Paddle 控制面板中启用的所有 webhook 的完整列表如下:
[!警告]
请务必使用 Cashier 内置的 Webhook 签名验证 中间件保护传入请求。
由于 Paddle webhook 需要绕过 Laravel 的 CSRF 保护,你应该确保 Laravel 不会尝试验证传入的 Paddle webhook 的 CSRF 令牌。为此,你应该将 paddle/* 从 CSRF 保护中排除,在你的应用程序的 bootstrap/app.php 文件中:
->withMiddleware(function (Middleware $middleware): void {
$middleware->validateCsrfTokens(except: [
'paddle/*',
]);
})为了让 Paddle 能够在本地开发期间发送您的应用程序 Webhook,您需要通过网站共享服务例如 Ngrok 或 Expose 公开您的应用程序。如果您正在使用 Laravel Sail 在本地开发您的应用程序,您可以使用 Sail 的 网站共享命令。
Cashier 自动处理因扣费失败导致的订阅取消以及其他常见的 Paddle Webhook。但是,如果您有需要处理的额外 Webhook 事件,您可以通过监听 Cashier 派发的以下事件来处理:
Laravel\Paddle\事件\Webhook接收这两个事件都包含 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\客户更新Laravel\Paddle\事件\交易完成Laravel\Paddle\事件\交易更新Laravel\Paddle\事件\订阅已创建Laravel\Paddle\Events\订阅更新Laravel\Paddle\Events\订阅已暂停Laravel\Paddle\Events\订阅已取消您还可以通过在您应用程序的 .env 文件中定义 CASHIER_WEBHOOK 环境变量来覆盖默认的内置 webhook 路由。此值应为您的 webhook 路由的完整 URL,并且需要与您 Paddle 控制面板中设置的 URL 匹配:
CASHIER_WEBHOOK=https://example.com/my-paddle-webhook-url为了保护您的 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-button Blade 组件 允许用户查看 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_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', // Fully refund this price...
'pri_456' => 200, // Only partially refund this price...
]);上述示例退回了一个交易中的特定行项目。如果您想退回整个交易,只需提供一个理由:
$response = $transaction->refund('Accidental charge');有关退款的更多信息,请查阅 Paddle 的退款文档。
[!WARNING]
退款在完全处理之前,必须始终获得 Paddle 的批准。
就像退款一样,您也可以贷记交易。贷记交易会将资金添加到客户的余额中,以便将来用于购买。贷记交易只能针对手动收款的交易进行,而不能针对自动收款的交易(例如订阅),因为 Paddle 会自动处理订阅贷记:
$transaction = $user->transactions()->first();
// Credit a specific line item fully...
$response = $transaction->credit('Compensation', 'pri_123');更多信息,请参阅 Paddle 关于贷记的文档。
[!WARNING]
贷项仅适用于手动收款的交易。自动收款的交易由 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');您可以使用 lastPayment 和 nextPayment 方法来检索和显示客户的往期或即将到来的循环订阅付款:
use App\Models\User;
$user = User::find(1);
$subscription = $user->subscription();
$lastPayment = $subscription->lastPayment();
$nextPayment = $subscription->nextPayment();这两种方法都将返回一个 Laravel\Paddle\Payment 实例;但是,lastPayment 将在交易尚未通过 webhooks 同步时返回 null,而 nextPayment 将在计费周期结束时(例如订阅已取消时)返回 null:
Next payment: {{ $nextPayment->amount() }} due on {{ $nextPayment->date()->format('d/m/Y') }}在测试期间,你应该手动测试你的计费流程,以确保你的集成按预期工作。
对于自动化测试,包括在 CI 环境中执行的测试,你可以使用 Laravel 的 HTTP 客户端 来模拟对 Paddle 发送的 HTTP 调用。虽然这不会测试来自 Paddle 的实际响应,但它提供了一种测试你的应用程序的方法,而无需实际调用 Paddle 的 API。