Laravel 交易工具包 (Paddle)
介绍
Laravel Cashier Paddle 为 Paddle's 订阅计费服务提供了一个富有表现力、流畅的界面。它几乎能够处理所有你所恐惧的各种订阅计费逻辑和代码。除了基本的订阅管理,Cashier 还可以处理:优惠券、交换订阅、订阅「数量」、取消宽限期等。
在使用 Cashier 时,推荐你回顾一下 Paddle 的用户手册 and API 文档。
升级 Cashier
当升级到一个新版本的 Cashier 时,推荐仔细回顾下 升级指南 这非常重要。
安装
首先,使用 Composer 包管理器安装 Paddle 的 Cashier 包:
composer require laravel/cashier-paddle
注意:为了确保 Cashier 正确处理所有 Paddle 事件,请记得 配置 Cashier 的 webhook 处理。
Paddle 沙盒
在本地和预发布开发环境中,应该 注册一个 Paddle 沙盒账号。这个账号将为你提供一个沙盒环境来测试和开发你的应用,而不会产生真实的交易。你也许会使用 Paddle 的 测试卡号 来模拟各种交易场景。
在使用 Pable 沙盒环境时,你应在应用程序的 .env
环境文件中将 PADDLE_SANDBOX
环境变量设置为 true
:
PADDLE_SANDBOX=true
在你已经完成你的应用开发之后,你也许会 申请一个 Paddle 正式账号 。 在你的应用程序投入生产环境之前,Paddle 需要批准你的应用程序的域。
数据迁移
Cashier 服务提供者注册它自己的数据迁移目录,所以你记得在安装扩展包之后执行数据迁移。Cashier 数据迁移将生成新的 customers
表。另外,新的 subscriptions
表将被创建,来存储所有你的用户的订阅。最后,新的 receipts
表也将被创建,来存储所有你的收据信息:
php artisan migrate
如果你需要重写 Cashier 中的数据迁移,你可以使用 vendor:publish
Artisan 命令来发布它们:
php artisan vendor:publish --tag="cashier-migrations"
如果你想阻止 Cashier 的数据迁移全部执行,你可以使用 Cashier 提供的 ignoreMigrations
。通常,这个方法会在 AppServiceProvider
的 register
方法中被调用:
use Laravel\Paddle\Cashier;
/**
* 注册服务。
*/
public function register(): void
{
Cashier::ignoreMigrations();
}
配置
Billable 模型
在使用 Cashier 之前,你必须将 Billable
trait 添加到你的用户模型定义中。 这里的 trait 提供了多种方法来允许你执行常见的计费任务,例如创建订阅、应用优惠券和更新付款方式信息:
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 Keys
接下来,你应该在应用程序的 .env
文件中配置你的 Paddle 。 你可以从 Paddle 控制面板检索你的 Paddle API 密钥:
PADDLE_VENDOR_ID=your-paddle-vendor-id
PADDLE_VENDOR_AUTH_CODE=your-paddle-vendor-auth-code
PADDLE_PUBLIC_KEY="your-paddle-public-key"
PADDLE_SANDBOX=true
当你使用 Paddle 的沙箱环境 时,PADDLE_SANDBOX
环境变量应该设置为 true
。如果你将应用程序部署到生产环境并使用 Paddle 的实时供应商环境,则 PADDLE_SANDBOX
变量应该设置为 false
。
Paddle JS
Paddle 依赖其自己的 JavaScript 库来启动 Paddle 结账小部件。你可以通过在应用程序布局中的 </head>
标签关闭之前放置 @paddleJS
Blade 指令来加载 JavaScript 库:
<head>
...
@paddleJS
</head>
货币配置
默认 Cashier 货币是美元(USD)。你可以在 .env
文件中定义 CASHIER_CURRENCY
环境变量来更改 默认货币:
CASHIER_CURRENCY=EUR
除了配置 Cashier 的货币之外,你还可以指定在格式化货币值以显示在发票上时要使用的区域。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\Receipt;
use App\Models\Cashier\Subscription;
/**
* 启动应用服务。
*/
public function boot(): void
{
Cashier::useReceiptModel(Receipt::class);
Cashier::useSubscriptionModel(Subscription::class);
}
核心概念
支付链接
Paddle 缺乏广泛的 CRUD API 来执行订阅状态更改。因此,与 Paddle 的大多数交互都是通过其 结帐小部件 完成的。在使用结账小部件之前,我们必须使用 Cashier 生成一个 「支付链接」。 「支付链接」将通知结账小部件我们希望执行的计费操作:
use App\Models\User;
use Illuminate\Http\Request;
Route::get('/user/subscribe', function (Request $request) {
$payLink = $request->user()->newSubscription('default', $premium = 34567)
->returnTo(route('home'))
->create();
return view('billing', ['payLink' => $payLink]);
});
Cashier 包括一个 paddle-button
Blade 组件。 我们可以将支付链接 URL 作为 「prop」传递给该组件。 单击此按钮时,将显示 Paddle 的结帐小部件:
<x-paddle-button :url="$payLink" class="px-8 py-4">
订阅
</x-paddle-button>
默认情况下,这将显示一个具有标准 Paddle 样式的按钮。 你可以通过向组件添加 data-theme="none"
属性来删除所有 Paddle 样式:
<x-paddle-button :url="$payLink" class="px-8 py-4" data-theme="none">
订阅
</x-paddle-button>
Paddle 结账小部件是异步的。 一旦用户在小部件中创建或更新订阅,Paddle 将发送你的应用程序 webhook,以便你可以在我们自己的数据库中正确更新订阅状态。 因此,正确 设置 webhooks 以同步 Paddle 的状态变化非常重要。
有关支付链接的更多信息,你可以查看 有关支付链接生成的 Paddle API 文档。
注意:订阅状态更改后,接收相应 webhook 的延迟通常很小,但你应该在应用程序中考虑到这一点,因为你的用户订阅在完成结帐后可能不会立即生效。
手动呈现支付链接
你也可以在不使用 Laravel 内置的 Blade 组件的情况下手动渲染支付链接。 首先,生成支付链接 URL,如先前所示:
$payLink = $request->user()->newSubscription('default', $premium = 34567)
->returnTo(route('home'))
->create();
接下来,只需将支付链接 URL 附加到 HTML 中的 a
元素:
<a href="#!" class="ml-4 paddle_button" data-override="{{ $payLink }}">
Paddle 支付
</a>
需要额外确认的付款
有时需要额外的验证才能确认和处理付款。发生这种情况时,Paddle 将显示付款确认屏幕。Paddle 或 Cashier 显示的付款确认屏幕可能会针对特定银行或发卡机构的付款流程进行定制,并且可能包括额外的卡确认、临时小额费用、单独的设备身份验证或其他形式的验证。
内联结账
如果你不想使用 Paddle 的 「叠加」样式结帐小部件,Paddle 还提供了内嵌显示小部件的选项。 虽然这种方法不允许你调整任何结帐的 HTML 字段,但它允许你将小部件嵌入到你的应用中。
为了让你轻松开始内联结账,Cashier 包含一个 paddle-checkout
Blade 组件。 首先,你应该 生成支付链接并将支付链接传递给组件的 override
属性:
<x-paddle-checkout :override="$payLink" class="w-full" />
要调整内联结帐组件的高度,你可以将 height
属性传递给 Blade 组件:
<x-paddle-checkout :override="$payLink" class="w-full" height="500" />
没有支付链接的内联结账
或者,你可以使用自定义选项而不是使用支付链接来自定义小部件:
@php
$options = [
'product' => $productId,
'title' => 'Product Title',
];
@endphp
<x-paddle-checkout :options="$options" class="w-full" />
请参阅 Paddle 的 Inline Checkout 指南 以及他们的 参数参考 以获取有关内联结帐可用选项的更多详细信息。
注意:如果你想在指定自定义选项时也 使用 passthrough 选项,你应该提供一个键 / 值数组作为其值。Cashier 将自动处理将数组转换为 JSON 字符串。 此外,customer_id
passthrough 选项保留供内部 Cashier 使用。
手动呈现内联结账
你也可以在不使用 Laravel 的内置 Blade 组件的情况下手动渲染内联结账。 首先,生成支付链接 URL 如前面示例中所示。
接下来,你可以使用 Paddle.js 来初始化结帐。 为了让这个例子简单,我们将使用 Alpine.js 来演示; 但是,你可以自由地将此示例转换为你自己的前端技术栈:
<div class="paddle-checkout" x-data="{}" x-init="
Paddle.Checkout.open({
override: {{ $payLink }},
method: 'inline',
frameTarget: 'paddle-checkout',
frameInitialHeight: 366,
frameStyle: 'width: 100%; background-color: transparent; border: none;'
});
">
</div>
用户识别
与 Stripe 相比,Paddle 用户在所有 Paddle 中都是独一无二的,而不是每个 Paddle 帐户都是独一无二的。因此,Paddle 的 API 目前不提供更新用户详细信息(例如电子邮件地址)的方法。在生成支付链接时,Paddle 使用 customer_email
参数识别用户。创建订阅时,Paddle 将尝试将用户提供的电子邮件与现有 Paddle 用户进行匹配。
鉴于这种行为,在使用 Cashier 和 Paddle 时需要记住一些重要的事情。首先,你应该知道,即使 Cashier 中的订阅绑定到同一个应用程序 用户,它们也可能绑定到 Paddle 内部系统中的不同用户。其次,每个订阅都有自己的连接支付方式信息,并且在 Paddle 的内部系统中也可能有不同的电子邮件地址(取决于创建订阅时分配给用户的电子邮件)。
因此,在显示订阅时,你应该始终告知用户哪些电子邮件地址或付款方式信息与订阅相关联。可以使用 Laravel\Paddle\Subscription
模型提供的以下方法检索这些信息:
$subscription = $user->subscription('default');
$subscription->paddleEmail();
$subscription->paymentMethod();
$subscription->cardBrand();
$subscription->cardLastFour();
$subscription->cardExpirationDate();
当前,没有办法通过 Paddle API 修改用户的电子邮件地址。当用户想在 Paddle 内更新他们的电子邮件地址时,他们唯一的方法是联系 Paddle 客户支持。在与 Paddle 沟通时,他们需要提供订阅的 paddleEmail
,这样 Paddle 就可以更新正确的用户。
定价
Paddle 允许你自定义每种货币对应的价格,也就是说 Paddle 允许你为不同国家和地区配置不同的价格。Cashier Paddle 允许你使用 productPrices
方法检索一个特定产品的所有价格。这个方法接受你希望检索价格的产品的产品 ID:
use Laravel\Paddle\Cashier;
$prices = Cashier::productPrices([123, 456]);
货币将根据请求的 IP 地址来确定,当然你也可以传入一个可选的国家和地区参数来检索特定国家和地区的价格:
use Laravel\Paddle\Cashier;
$prices = Cashier::productPrices([123, 456], ['customer_country' => 'BE']);
检索出价格后,你可以根据需要显示它们:
<ul>
@foreach ($prices as $price)
<li>{{ $price->product_title }} - {{ $price->price()->gross() }}</li>
@endforeach
</ul>
你也可以显示净价(不含税)并将税额显示分离:
<ul>
@foreach ($prices as $price)
<li>{{ $price->product_title }} - {{ $price->price()->net() }} (+ {{ $price->price()->tax() }} tax)</li>
@endforeach
</ul>
如果你检索了订阅的价格,你可以分别显示其原始价格和连续订阅价格:
<ul>
@foreach ($prices as $price)
<li>{{ $price->product_title }} - Initial: {{ $price->initialPrice()->gross() }} - Recurring: {{ $price->recurringPrice()->gross() }}</li>
@endforeach
</ul>
更多相关信息,请 查看 Paddle 的价格 API 文档。
客户
如果用户已经是客户并且你希望显示适用于该客户的价格,你可以通过直接从客户实例检索价格来实现:
use App\Models\User;
$prices = User::find(1)->productPrices([123, 456]);
在内部,Cashier 将使用用户的 paddleCountry
方法 来检索以他们的货币表示的价格。例如,居住在美国的用户将看到以美元为单位的价格,而位于比利时的用户将看到以欧元为单位的价格。如果找不到匹配的货币,则将使用产品的默认货币。你可以在 Paddle 控制面板中自定义产品或订阅计划的所有价格。
优惠券
你也可以展示选择优惠券后的折扣价。 在调用 productPrices
方法时,优惠券可以作为逗号分隔的字符串传递:
use Laravel\Paddle\Cashier;
$prices = Cashier::productPrices([123, 456], [
'coupons' => 'SUMMERSALE,20PERCENTOFF'
]);
然后,使用 price
方法显示计算出的价格:
<ul>
@foreach ($prices as $price)
<li>{{ $price->product_title }} - {{ $price->price()->gross() }}</li>
@endforeach
</ul>
你可以使用 listPrice
方法显示原价(没有优惠券折扣):
<ul>
@foreach ($prices as $price)
<li>{{ $price->product_title }} - {{ $price->listPrice()->gross() }}</li>
@endforeach
</ul>
注意:使用价格 API 时,Paddle 仅允许将优惠券应用于一次性购买的产品,而不允许应用于订阅计划。
客户
客户默认值
Cashier 允许你在创建支付链接时为你的客户定义一些默认值。 设置这些默认值允许你预先填写客户的电子邮件地址、国家 / 地区和邮政编码,以便他们可以立即转到结帐小部件的付款部分。 你可以通过覆盖计费模型上的以下方法来设置这些默认值:
/**
* 获取客户的电子邮件地址以与 Paddle 关联。
*/
public function paddleEmail(): string|null
{
return $this->email;
}
/**
* 获取客户的国家与 Paddle 关联。
*
* 这需要一个 2 个字母的代码。 有关支持的国家 / 地区,请参阅以下链接。
*
* @link https://developer.paddle.com/reference/platform-parameters/supported-countries
*/
public function paddleCountry(): string|null
{
// ...
}
/**
* 获取客户的邮政编码以与 Paddle 关联。
*
* 有关需要此功能的国家 / 地区,请参阅以下链接。
*
* @link https://developer.paddle.com/reference/platform-parameters/supported-countries#countries-requiring-postcode
*/
public function paddlePostcode(): string|null
{
// ...
}
这些默认值将用于 Cashier 中生成 支付链接 的每个操作。
订阅
创建订阅
要创建订阅,请首先检索计费模型的实例,该实例通常是 App\Models\User
的实例。检索模型实例后,你可以使用 newSubscription
方法来创建模型的订阅支付链接:
use Illuminate\Http\Request;
Route::get('/user/subscribe', function (Request $request) {
$payLink = $request->user()->newSubscription('default', $premium = 12345)
->returnTo(route('home'))
->create();
return view('billing', ['payLink' => $payLink]);
});
传递给 newSubscription
方法的第一个参数应该是订阅的名称。 如果你的应用只提供一个订阅,你可以将其称为 default
或 primary
。第二个参数是用户订阅的特定计划。 该值应对应于 Paddle 中的计划标识符。returnTo
方法接受一个 URL,你的用户在成功完成结帐后将被重定向到该 URL。
create
方法将创建一个支付链接,你可以使用它来生成一个支付按钮。可以使用 Cashier Paddle 附带的 paddle-button
Blade 组件 生成支付按钮:
<x-paddle-button :url="$payLink" class="px-8 py-4">
订阅
</x-paddle-button>
用户完成结帐后,将从 Paddle 发送一个 subscription_created
webhook。 Cashier 将收到此 webhook 并为你的客户设置订阅。为了确保你的应用程序正确接收和处理所有 webhook,请确保你正确地 设置 webhook 处理。
额外细节
如果你想指定额外的客户或订阅详细信息,你可以通过将它们作为键 / 值对数组传递给 create
方法来实现。要了解有关 Paddle 支持的其他字段的更多信息,请查看 Paddle 关于 生成支付链接 的文档:
$payLink = $user->newSubscription('default', $monthly = 12345)
->returnTo(route('home'))
->create([
'vat_number' => $vatNumber,
]);
优惠券
如果你想在创建订阅时申请优惠券,你可以使用 withCoupon
方法:
$payLink = $user->newSubscription('default', $monthly = 12345)
->returnTo(route('home'))
->withCoupon('code')
->create();
元数据
你还可以使用 withMetadata
方法传递元数据数组:
$payLink = $user->newSubscription('default', $monthly = 12345)
->returnTo(route('home'))
->withMetadata(['key' => 'value'])
->create();
注意:提供元数据时,请避免使用 subscription_name
作为元数据键。 此密钥保留供 Cashier 内部使用。
检查订阅状态
一旦用户订阅了你的应用程序,你就可以使用各种便利的方法检查他们的订阅状态。 首先,如果用户有活动订阅,subscribed
方法返回 true
,即使订阅当前处于试用期:
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('default')) {
// 该用户不是付费用户。。。
return redirect('billing');
}
return $next($request);
}
}
如果你想确定用户是否仍在试用期内,你可以使用 onTrial
方法。这个方法用于确定是否应向用户显示他们仍在试用期的警告:
if ($user->subscription('default')->onTrial()) {
// ...
}
该 subscribedToPlan
方法可用于根据给定的 Paddle 计划 ID 来确定用户是否订阅了给定的计划。 在这个例子中,我们将确定用户的 default
订阅是否订阅了包月计划:
if ($user->subscribedToPlan($monthly = 12345, 'default')) {
// ...
}
通过将数组传递给 subscribedToPlan
方法,你可以确定用户的 default
订阅是订阅月度计划或是年度计划:
if ($user->subscribedToPlan([$monthly = 12345, $yearly = 54321], 'default')) {
// ...
}
该 recurring
方法可用于确定用户当前是否已订阅并且不是处于试用期:
if ($user->subscription('default')->recurring()) {
// ...
}
已取消订阅状态
要确定用户是否曾经是订阅者但现在已取消订阅,你可以使用 cancelled
方法:
if ($user->subscription('default')->cancelled()) {
// ...
}
你还可以确定用户是否已取消订阅,但在订阅完全到期之前会处于 「宽限期」。 例如,如果用户在 3 月 5 日取消原定于 3 月 10 日到期的订阅,则用户将处于「宽限期」,直到 3 月 10 日。 请注意,在此期间 subscribed
方法仍然返 回 true
:
if ($user->subscription('default')->onGracePeriod()) {
// ...
}