laravel
10. 软件包
Laravel Cashier(Paddle)

介绍

[!警告] 本文档适用于 Cashier Paddle 2.x 与 Paddle 计费的集成。如果您仍在使用 Paddle Classic,则应使用Cashier Paddle 1.x (opens in a new tab)

Laravel Cashier Paddle (opens in a new tab)Paddle (opens in a new tab) 的订阅计费服务提供了一个富有表现力、流畅的接口。它处理了几乎所有您害怕的样板订阅计费代码。除了基本的订阅管理外,Cashier 还可以处理:交换订阅、订阅“数量”、订阅暂停、取消宽限期等。

在深入研究 Cashier Paddle 之前,我们建议您也查看 Paddle 的概念指南 (opens in a new tab)API 文档 (opens in a new tab)

升级 Cashier

在升级到新版本的 Cashier 时,您必须仔细查看升级指南 (opens in a new tab)

安装

首先,使用 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

[!警告] 为确保 Cashier 正确处理所有 Paddle 事件,请记住设置 Cashier 的 webhook 处理

Paddle 沙箱

在本地和暂存环境开发期间,您应该注册一个 Paddle 沙箱账户 (opens in a new tab)。此账户将为您提供一个沙箱环境,以便在不进行实际支付的情况下测试和开发您的应用程序。您可以使用 Paddle 的测试卡号 (opens in a new tab)来模拟各种支付场景。

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

PADDLE_SANDBOX=true

在完成应用程序开发后,您可以申请一个 Paddle 供应商账户 (opens in a new tab)。在将您的应用程序投入生产之前,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=您的 Paddle 客户端侧令牌
PADDLE_API_KEY=您的 Paddle API 密钥
PADDLE_RETAIN_KEY=您的 Paddle 保留密钥
PADDLE_WEBHOOK_SECRET="您的 Paddle webhook 秘密"
PADDLE_SANDBOX=true

当您使用Paddle 的沙箱环境时,PADDLE_SANDBOX 环境变量应设置为 true。如果您将应用程序部署到生产环境并使用 Paddle 的实时供应商环境,则 PADDLE_SANDBOX 变量应设置为 false

PADDLE_RETAIN_KEY 是可选的,只有在您将 Paddle 与Retain (opens in a new tab)一起使用时才应设置。

Paddle JS

Paddle 依赖于其自己的 JavaScript 库来启动 Paddle 结账小部件。您可以通过在应用程序布局的关闭 </head> 标签之前放置 @paddleJS Blade 指令来加载 JavaScript 库:

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

货币配置

您可以指定一个区域设置,用于在为发票显示格式化货币值时使用。在内部,Cashier 使用PHP 的 NumberFormatter (opens in a new tab)来设置货币区域设置:

CASHIER_CURRENCY_LOCALE=nl_BE

[!警告] 为了使用除 en 以外的区域设置,请确保在您的服务器上安装并配置了 ext-intl PHP 扩展。

覆盖默认模型

您可以自由扩展 Cashier 内部使用的模型,方法是定义自己的模型并扩展相应的 Cashier 模型:

use Laravel\Paddle\Subscription as CashierSubscription;

class Subscription extends CashierSubscription
{
    //...
}

定义模型后,您可以通过 Laravel\Paddle\Cashier 类指示 Cashier 使用您的自定义模型。通常,您应该在应用程序的 App\Providers\AppServiceProvider 类的 boot 方法中告知 Cashier 您的自定义模型:

use App\Models\Cashier\Subscription;
use App\Models\Cashier\Transaction;

/**
 * 引导任何应用程序服务。
 */
public function boot(): void
{
    Cashier::useSubscriptionModel(Subscription::class);
    Cashier::useTransactionModel(Transaction::class);
}

快速入门

销售产品

[!注意] 在使用 Paddle 结账之前,您应该在 Paddle 仪表板中定义具有固定价格的产品。此外,您应该配置 Paddle 的 webhook 处理

通过您的应用程序提供产品和订阅计费可能会令人生畏。然而,多亏了 Cashier 和Paddle 的结账覆盖 (opens in a new tab),您可以轻松构建现代、强大的支付集成。

要为非周期性、单次收费的产品向客户收费,我们将使用 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 时,“价格”是指为特定产品定义的价格 (opens in a new tab)

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

buy 视图中,我们将包含一个按钮来显示结账覆盖。paddle-button Blade 组件随 Cashier Paddle 一起提供;然而,您也可以手动渲染覆盖式结账

<x-paddle-button :checkout="$checkout" class="px-8 py-4">
    购买产品
</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');

如您在上面的示例中所见,当用户开始结账过程时,我们将向 checkout 方法提供购物车/订单的所有相关 Paddle 价格标识符。当然,您的应用程序负责在客户添加这些项目时将它们与“购物车”或订单相关联。我们还通过 customData 方法向 Paddle 结账覆盖提供订单的 ID。

当然,一旦客户完成结账过程,您可能希望将订单标记为“已完成”。要实现这一点,您可以监听由 Paddle 发送并通过 Cashier 事件引发的 webhooks,以将订单信息存储在您的数据库中。

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

use App\Listeners\CompleteOrder;
use Illuminate\Support\Facades\Event;
use Laravel\Paddle\Events\TransactionCompleted;

/**
 * 引导任何应用程序服务。
 */
public function boot(): void
{
    Event::listen(TransactionCompleted::class, CompleteOrder::class);
}

在这个示例中,CompleteOrder 监听器可能如下所示:

namespace App\Listeners;

use App\Models\Order;
use Laravel\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']);
    }
}

有关transaction.completed 事件包含的数据 (opens in a new tab)的更多信息,请参考 Paddle 的文档。

销售订阅

[!注意] 在使用 Paddle 结账之前,您应该在 Paddle 仪表板中定义具有固定价格的产品。此外,您应该配置 Paddle 的 webhook 处理

通过您的应用程序提供产品和订阅计费可能会令人生畏。然而,多亏了 Cashier 和Paddle 的结账覆盖 (opens in a new tab),您可以轻松构建现代、强大的支付集成。

要了解如何使用 Cashier 和 Paddle 的结账覆盖来销售订阅,让我们考虑一个简单的订阅服务场景,其中有一个基本的每月(price_basic_monthly)和每年(price_basic_yearly)计划。这两个价格可以在我们的 Paddle 仪表板中的“Basic”产品(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">
    订阅
</x-paddle-button>

现在,当点击“订阅”按钮时,客户将能够输入他们的支付详细信息并启动他们的订阅。要知道他们的订阅何时实际开始(因为某些支付方式需要几秒钟来处理),您还应该配置 Cashier 的 webhook 处理

既然客户可以开始订阅,我们需要限制我们应用程序的某些部分,以便只有订阅用户可以访问它们。当然,我们总是可以通过 Cashier 的 Billable 特征提供的 subscribed 方法来确定用户的当前订阅状态:

@if ($user->subscribed())
    <p>您已订阅。</p>
@endif
 
#### 手动渲染覆盖式结账
 
您也可以在不使用 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 会检测到这个类,并在点击链接时显示覆盖式结账:

<?php
$items = $checkout->getItems();
$customer = $checkout->getCustomer();
$custom = $checkout->getCustomData();
?>
 
<a
    href='#!'
    class='paddle_button'
    data-items='{!! json_encode($items)!!}'
    @if ($customer) data-customer-id='{{ $customer->paddle_id }}' @endif
    @if ($custom) data-custom-data='{{ json_encode($custom) }}' @endif
    @if ($returnUrl = $checkout->getReturnUrl()) data-success-url='{{ $returnUrl }}' @endif
>
    购买产品
</a>

内联结账

如果您不想使用 Paddle 的“覆盖”式结账小部件,Paddle 还提供了以内联方式显示小部件的选项。虽然这种方法不允许您调整结账的任何 HTML 字段,但它允许您将小部件嵌入到您的应用程序中。

为了方便您开始使用内联结账,Cashier 包含了一个paddle-checkout Blade 组件。首先,您应该生成一个结账会话

use Illuminate\Http\Request;
 
Route::get('/buy', function (Request $request) {
    $checkout = $user->checkout('pri_34567')
        ->returnTo(route('dashboard'));
 
    return view('billing', ['checkout' => $checkout]);
});

然后,您可以将结账会话传递给组件的checkout属性:

<x-paddle-checkout :checkout="$checkout" class="w-full" />

要调整内联结账组件的高度,您可以将height属性传递给 Blade 组件:

<x-paddle-checkout :checkout="$checkout" class="w-full" height="500" />

有关内联结账的自定义选项的更多详细信息,请参考 Paddle 的内联结账指南 (opens in a new tab)可用的结账设置 (opens in a new tab)

手动渲染内联结账

您也可以在不使用 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 (opens in a new tab)来演示;但是,您可以根据自己的前端栈自由修改这个示例:

<?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() }} 税)</li>
    @endforeach
</ul>

有关更多信息,请查看 Paddle 关于价格预览的 API 文档 (opens in a new tab)

客户价格预览

如果用户已经是客户,并且您想显示适用于该客户的价格,您可以直接从客户实例中检索价格:

use App\Models\User;
 
$prices = User::find(1)->previewPrices(['pri_123', 'pri_456']);

在内部,Cashier 将使用用户的客户 ID 以其货币检索价格。例如,居住在美国的用户将看到以美元计价的价格,而在比利时的用户将看到以欧元计价的价格。如果找不到匹配的货币,则将使用产品的默认货币。您可以在 Paddle 控制面板中自定义产品或订阅计划的所有价格。

折扣

您也可以选择在折扣后显示价格。在调用previewPrices方法时,您可以通过discount_id选项提供折扣 ID:

use Laravel\Paddle\Cashier;
 
$prices = Cashier::previewPrices(['pri_123', 'pri_456'], [
    'discount_id' => 'dsc_123'
]);

然后,显示计算后的价格:

<ul>
    @foreach ($prices as $price)
        <li>{{ $price->product['name'] }} - {{ $price->total() }}</li>
    @endforeach
</ul>

客户

客户默认值

Cashier 允许您在创建结账会话时为客户定义一些有用的默认值。设置这些默认值可以让您预先填写客户的电子邮件地址和姓名,以便他们可以立即进入结账小部件的付款部分。您可以通过在可计费模型上重写以下方法来设置这些默认值:

/**
 * 获取与 Paddle 关联的客户姓名。
 */
public function paddleName(): string|null
{
    return $this->name;
}
 
/**
 * 获取与 Paddle 关联的客户电子邮件地址。
 */
public function paddleEmail(): string|null
{
    return $this->email;
}

这些默认值将用于 Cashier 中生成结账会话的每个操作。

检索客户

您可以使用Cashier::findBillable方法通过其 Paddle 客户 ID 检索客户。此方法将返回可计费模型的实例:

use Laravel\Paddle\Cashier;
 
$user = Cashier::findBillable($customerId);

创建客户

有时,您可能希望在不开始订阅的情况下创建一个 Paddle 客户。您可以使用createAsCustomer方法来实现:

$customer = $user->createAsCustomer();

将返回一个Laravel\Paddle\Customer的实例。在 Paddle 中创建客户后,您可以在以后的某个日期开始订阅。您可以提供一个可选的$options数组来传递Paddle API 支持的任何其他客户创建参数 (opens in a new tab)

$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-buttonBlade 组件

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

用户完成结账后,Paddle 将发送一个subscription_created网络钩子。Cashier 将接收此网络钩子并为您的客户设置订阅。为了确保您的应用程序正确接收和处理所有网络钩子,请确保您已正确设置 Paddle 网络钩子处理

检查订阅状态

一旦用户订阅了您的应用程序,您可以使用多种便捷方法检查其订阅状态。首先,subscribed方法如果用户有有效的订阅,即使订阅目前处于试用期,也会返回true

if ($user->subscribed()) {
    //...
}

如果您的应用程序提供多个订阅,您可以在调用subscribed方法时指定订阅:

if ($user->subscribed('default')) {
    //...
}

subscribed方法也是路由中间件的一个很好的候选者,允许您根据用户的订阅状态过滤对路由和控制器的访问:

<?php
 
namespace App\Http\Middleware;
 
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
 
class EnsureUserIsSubscribed
{
    /**
     * 处理传入的请求。
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        if ($request->user() &&! $request->user()->subscribed()) {
            // 此用户不是付费客户...
            return redirect('/billing');
        }
 
        return $next($request);
    }
}

如果您想确定用户是否仍在试用期,您可以使用onTrial方法。此方法对于确定是否应向用户显示他们仍在试用期的警告很有用:

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

subscribedToPrice方法可用于根据给定的 Paddle 价格 ID 确定用户是否订阅了给定的计划。在这个示例中,我们将确定用户的default订阅是否积极订阅了每月价格:

if ($user->subscribedToPrice($monthly = 'pri_123', 'default')) {
    //...
}

recurring方法可用于确定用户是否当前处于有效订阅状态,且不在试用期或宽限期内:

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

已取消的订阅状态

要确定用户是否曾经是活跃订阅者但已取消订阅,您可以使用canceled方法:

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

您还可以确定用户是否已取消订阅,但仍处于“宽限期”,直到订阅完全过期。例如,如果用户在 3 月 5 日取消了原定于 3 月 10 日到期的订阅,用户将在 3 月 10 日之前处于“宽限期”。此外,在此期间,subscribed方法仍将返回true

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

逾期状态

如果订阅的付款失败,它将被标记为past_due。当您的订阅处于此状态时,在客户更新其付款信息之前,它将不会处于活动状态。您可以使用订阅实例上的pastDue方法确定订阅是否逾期:

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

当订阅逾期时,您应该指示用户更新他们的付款信息

如果您希望在订阅past_due时仍将其视为有效,您可以使用 Cashier 提供的keepPastDueSubscriptionsActive方法。通常,此方法应在您的AppServiceProviderregister方法中调用:

use Laravel\Paddle\Cashier;
 
/**
 * 注册任何应用程序服务。
 */
public function register(): void
{
    Cashier::keepPastDueSubscriptionsActive();
}

[!警告]
当订阅处于past_due状态时,在付款信息更新之前无法进行更改。因此,当订阅处于past_due状态时,swapupdateQuantity方法将抛出异常。

订阅范围

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

// 获取所有有效的订阅...
$subscriptions = Subscription::query()->valid()->get();
 
// 获取用户的所有已取消的订阅...
$subscriptions = $user->subscriptions()->canceled()->get();

以下是可用范围的完整列表:

Subscription::query()->valid();
Subscription::query()->onTrial();
Subscription::query()->expiredTrial();
Subscription::query()->notOnTrial();
Subscription::query()->active();
Subscription::query()->recurring();
Subscription::query()->pastDue();
Subscription::query()->paused();
Subscription::query()->notPaused();
Subscription::query()->onPausedGracePeriod();
Subscription::query()->notOnPausedGracePeriod();
Subscription::query()->canceled();
Subscription::query()->notCanceled();
Subscription::query()->onGracePeriod();
Subscription::query()->notOnGracePeriod();

订阅单次收费

订阅单次收费允许您在订阅费用之外向订阅者收取一次性费用。在调用charge方法时,您必须提供一个或多个价格 ID:

// 收取单个价格...
$response = $user->subscription()->charge('pri_123');
 
// 同时收取多个价格...
$response = $user->subscription()->charge(['pri_123', 'pri_456']);

charge方法实际上不会向客户收费,直到他们订阅的下一个计费周期。如果您想立即向客户收费,您可以使用chargeAndInvoice方法代替:

$response = $user->subscription()->chargeAndInvoice('pri_123');

更新付款信息

Paddle 会为每个订阅保存

多种产品的订阅

具有多种产品的订阅 (opens in a new tab)允许您为单个订阅分配多个计费产品。例如,假设您正在构建一个客户服务“帮助台”应用程序,其基础订阅价格为每月 10 美元,但提供每月额外 15 美元的实时聊天附加产品。

在创建订阅结账会话时,您可以通过将价格数组作为第一个参数传递给 subscribe 方法来为给定的订阅指定多个产品:

use Illuminate\Http\Request;
 
Route::post('/user/subscribe', function (Request $request) {
    $checkout = $request->user()->subscribe([
        'price_monthly',
        'price_chat',
    ]);
 
    return view('billing', ['checkout' => $checkout]);
});

在上述示例中,客户的 default 订阅将附带两个价格。这两个价格将在各自的计费间隔内收取费用。如果需要,您可以传递一个键值对的关联数组来指示每个价格的特定数量:

$user = User::find(1);
 
$checkout = $user->subscribe('default', ['price_monthly', 'price_chat' => 5]);

如果您想向现有订阅添加另一个价格,则必须使用订阅的 swap 方法。调用 swap 方法时,您还应该包括订阅的当前价格和数量:

$user = User::find(1);
 
$user->subscription()->swap(['price_chat', 'price_original' => 2]);

上述示例将添加新价格,但客户要到下一个计费周期才会为此付费。如果您想立即向客户收费,可以使用 swapAndInvoice 方法:

$user->subscription()->swapAndInvoice(['price_chat', 'price_original' => 2]);

您可以使用 swap 方法从订阅中删除价格,只需省略您要删除的价格:

$user->subscription()->swap(['price_original' => 2]);

[!警告]
您不能删除订阅上的最后一个价格。相反,您应该直接取消订阅。

多个订阅

Paddle 允许您的客户同时拥有多个订阅。例如,您经营一家健身房,提供游泳订阅和举重订阅,每个订阅可能有不同的定价。当然,客户应该能够选择订阅其中一个或两个计划。

当您的应用程序创建订阅时,您可以将订阅的类型作为第二个参数提供给 subscribe 方法。该类型可以是表示用户正在启动的订阅类型的任何字符串:

use Illuminate\Http\Request;
 
Route::post('/swimming/subscribe', function (Request $request) {
    $checkout = $request->user()->subscribe($swimmingMonthly = 'pri_123','swimming');
 
    return view('billing', ['checkout' => $checkout]);
});

在这个例子中,我们为客户启动了一个每月游泳订阅。然而,他们可能想在以后换成年度订阅。在调整客户的订阅时,我们可以简单地在 swimming 订阅上交换价格:

$user->subscription('swimming')->swap($swimmingYearly = 'pri_456');

当然,您也可以完全取消订阅:

$user->subscription('swimming')->cancel();

暂停订阅

要暂停订阅,请在用户的订阅上调用 pause 方法:

$user->subscription()->pause();

当订阅被暂停时,Cashier 将自动在您的数据库中设置 paused_at 列。此列用于确定 paused 方法何时应开始返回 true。例如,如果客户在 3 月 1 日暂停订阅,但订阅计划直到 3 月 5 日才会再次计费,那么 paused 方法将继续返回 false,直到 3 月 5 日。这是因为通常允许用户在计费周期结束前继续使用应用程序。

默认情况下,暂停会在下一个计费间隔发生,以便客户可以使用他们已付费的剩余时间段。如果您想立即暂停订阅,可以使用 pauseNow 方法:

$user->subscription()->pauseNow();

使用 pauseUntil 方法,您可以将订阅暂停到特定的时间点:

$user->subscription()->pauseUntil(now()->addMonth());

或者,您可以使用 pauseNowUntil 方法立即将订阅暂停到给定的时间点:

$user->subscription()->pauseNowUntil(now()->addMonth());

您可以使用 onPausedGracePeriod 方法确定用户是否已暂停订阅但仍处于“宽限期”内:

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

要恢复已暂停的订阅,您可以在订阅上调用 resume 方法:

$user->subscription()->resume();

[!警告]
订阅在暂停期间不能被修改。如果您想换成不同的计划或更新数量,您必须首先恢复订阅。

取消订阅

要取消订阅,请在用户的订阅上调用 cancel 方法:

$user->subscription()->cancel();

当订阅被取消时,Cashier 将自动在您的数据库中设置 ends_at 列。此列用于确定 subscribed 方法何时应开始返回 false。例如,如果客户在 3 月 1 日取消订阅,但订阅计划直到 3 月 5 日才会结束,那么 subscribed 方法将继续返回 true,直到 3 月 5 日。这是因为通常允许用户在计费周期结束前继续使用应用程序。

您可以使用 onGracePeriod 方法确定用户是否已取消订阅但仍处于“宽限期”内:

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

如果您希望立即取消订阅,可以在订阅上调用 cancelNow 方法:

$user->subscription()->cancelNow();

要停止在宽限期内取消订阅,您可以调用 stopCancelation 方法:

$user->subscription()->stopCancelation();

[!警告]
Paddle 的订阅在取消后不能恢复。如果您的客户希望恢复订阅,他们将不得不创建一个新的订阅。

订阅试用

提前收集支付方式信息

如果您想为客户提供试用期,同时提前收集支付方式信息,您应该在 Paddle 仪表板上为客户订阅的价格设置试用时间。然后,像往常一样启动结账会话:

use Illuminate\Http\Request;
 
Route::get('/user/subscribe', function (Request $request) {
    $checkout = $request->user()->subscribe('pri_monthly')
                ->returnTo(route('home'));
 
    return view('billing', ['checkout' => $checkout]);
});

当您的应用程序收到 subscription_created 事件时,Cashier 将在您的应用程序数据库中的订阅记录上设置试用期结束日期,并指示 Paddle 在该日期之后才开始向客户计费。

[!警告]
如果客户的订阅在试用期结束日期之前未被取消,试用期一结束他们将被立即收费,因此您应该确保通知您的用户试用期的结束日期。

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

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

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

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

要确定用户是否在特定订阅类型的试用期内,您可以将类型提供给 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()) {
    // 用户在试用期内...
}

当您准备为用户创建实际订阅时,您可以像往常一样使用 subscribe 方法:

use Illuminate\Http\Request;
 
Route::get('/user/subscribe', function (Request $request) {
    $checkout = $user->subscribe('pri_monthly')
        ->returnTo(route('home'));
 
    return view('billing', ['checkout' => $checkout]);
});

要检索用户的试用期结束日期,您可以使用 trialEndsAt 方法。如果用户正在试用,此方法将返回一个 Carbon 日期实例,如果用户不在试用,则返回 null。如果您想获取除默认订阅之外的特定订阅的试用期结束日期,您还可以传递一个可选的订阅类型参数:

if ($user->onTrial('default')) {
    $trialEndsAt = $user->trialEndsAt();
}

如果您想确切知道用户是否在其“通用”试用期内且尚未创建实际订阅,您可以使用 onGenericTrial 方法:

if ($user->onGenericTrial()) {
    // 用户在其“通用”试用期内...
}

延长或激活试用

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

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

或者,您可以通过在订阅上调用 activate 方法立即结束试用期并激活订阅:

$user->subscription()->activate();

处理 Paddle Webhooks

Paddle 可以通过 Webhooks 通知您的应用程序各种事件。默认情况下,Cashier 服务提供商会注册一个指向 Cashier 的 Webhook 控制器的路由。该控制器将处理所有传入的 Webhook 请求。

默认情况下,该控制器将自动处理因多次收费失败而取消的订阅、订阅更新和支付方式更改;但是,正如我们即将发现的,您可以扩展此控制器来处理您喜欢的任何 Paddle Webhook 事件。

为确保您的应用程序能够处理 Paddle Webhooks,请务必在 Paddle 控制面板中配置 Webhook URL (opens in a new tab)。默认情况下,Cashier 的 Webhook 控制器响应 /paddle/webhook URL 路径。您应该在 Paddle 控制面板中启用的所有 Webhook 的完整列表如下:

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

[!警告]
请确保使用 Cashier 包含的 Webhook 签名验证 中间件来保护传入的请求。

Webhooks 和 CSRF 保护

由于 Paddle Webhooks 需要绕过 Laravel 的 CSRF 保护,您应该确保 Laravel 不会尝试验证传入的 Paddle Webhooks 的 CSRF 令牌。要实现这一点,您应该在应用程序的 bootstrap/app.php 文件中将 paddle/* 从 CSRF 保护中排除:

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

Webhooks 和本地开发

为了使 Paddle 能够在本地开发期间向您的应用程序发送 Webhooks,您需要通过诸如 Ngrok (opens in a new tab)Expose (opens in a new tab) 等站点共享服务来暴露您的应用程序。如果您在本地使用 Laravel Sail 开发您的应用程序,您可以使用 Sail 的 站点共享命令

定义 Webhook 事件处理程序

Cashier 会自动处理因收费失败而取消的订阅以及其他常见的 Paddle Webhooks。但是,如果您有其他想要处理的 Webhook 事件,您可以通过监听 Cashier 分发的以下事件来实现:

  • Laravel\Paddle\Events\WebhookReceived
  • Laravel\Paddle\Events\WebhookHandled

这两个事件都包含 Paddle Webhook 的完整有效负载。例如,如果您想处理 transaction.billed Webhook,您可以注册一个 监听器 来处理该事件:

<?php
 
namespace App\Listeners;
 
use Laravel\Paddle\Events\WebhookReceived;
 
class PaddleEventListener
{
    /**
     * 处理接收到的 Paddle Webhooks。
     */
    public function handle(WebhookReceived $event): void
    {
        if ($event->payload['event_type'] === 'transaction.billed') {
            // 处理传入的事件...
        }
    }
}

Cashier 还会发出专门针对接收到的 Webhook 类型的事件。除了来自 Paddle 的完整有效负载外,它们还包含用于处理 Webhook 的相关模型,如可计费模型、订阅或收据:

  • Laravel\Paddle\Events\CustomerUpdated
  • Laravel\Paddle\Events\TransactionCompleted
  • Laravel\Paddle\Events\TransactionUpdated
  • Laravel\Paddle\Events\SubscriptionCreated
  • Laravel\Paddle\Events\SubscriptionUpdated
  • Laravel\Paddle\Events\SubscriptionPaused
  • Laravel\Paddle\Events\SubscriptionCanceled

您还可以通过在应用程序的 .env 文件中定义 CASHIER_WEBHOOK 环境变量来覆盖默认的内置 Webhook 路由。该值应该是您的 Webhook 路由的完整 URL,并且需要与您在 Paddle 控制面板中设置的 URL 匹配:

CASHIER_WEBHOOK=https://example.com/my-paddle-webhook-url

验证 Webhook 签名

为了确保您的 Webhooks 安全,您可以使用 Paddle 的 Webhook 签名 (opens in a new tab)。为了方便起见,Cashier 自动包含一个中间件,用于验证传入的 Paddle Webhook 请求是否有效。

要启用 Webhook 验证,请确保在应用程序的 .env 文件中定义了 PADDLE_WEBHOOK_SECRET 环境变量。Webhook 密钥可以从您的 Paddle 账户仪表板中检索。

单次收费

产品收费

如果您想为客户发起产品购买,您可以在可计费模型实例上使用 checkout 方法为购买生成结账会话。checkout 方法接受一个或多个价格 ID。如果需要,可以使用关联数组提供正在购买的产品的数量:

use Illuminate\Http\Request;
 
Route::get('/buy', function (Request $request) {
    $checkout = $request->user()->checkout(['pri_tshirt', 'pri_socks' => 5]);
 
    return view('buy', ['checkout' => $checkout]);
});

生成结账会话后,您可以使用 Cashier 提供的 paddle-button Blade 组件,允许用户查看 Paddle 结账小部件并完成购买:

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

结账会话有一个 customData 方法,允许您将任何您希望的自定义数据传递给基础交易创建。请查阅 Paddle 文档 (opens in a new tab),以了解在传递自定义数据时您可以使用的选项:

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

退款交易

退款交易将把退款金额退回到客户在购买时使用的支付方式。如果您需要为 Paddle 购买进行退款,您可以在 Cashier\Paddle\Transaction 模型上使用 refund 方法。此