最近,我在一个的需求包含两个可验证模型(带有独立订阅的)的 Laravel 项目上工作。该项目自然使用 Laravel Cashier 来管理用户订阅。
默认情况下,Laravel Cashier 假设 App\Model\User
类作为一个账单模型。我们可以将其配置为使用不同的模型,但在我们的情况下有两个不同的模型。所以,我必须采取不同的方法。
CASHIER_MODEL=App\Models\User
提示:这将是一个非常长的教程!我将从创建模型、更新迁移、配置webhook等一切进行说明。
但如果你很着急,这里就是你的解决方案,技巧是在运行时使用 config
辅助函数设置 Cashier 的账单模型。
config(['cashier.model' => 'App\Models\Seller']);
初始设置
让我们从假设我们的应用程序有两个账单模型,一个是 User
,另一个是 Seller
。这两个模型都将具有订阅。使用 Cashier 与多个模型的方式有多种,但我们为了简单起见,将把它们的订阅详情存储在不同的表中。
让我们首先安装 Cashier 包的条纹功能。
composer require laravel/cashier
Cashier 将使用 subscriptions
和 subscription_items
表来存储有关用户订阅的信息。让我们发布 Cashier 的默认迁移,以便我们可以看一下表结构。
php artisan vendor:publish --tag="cashier-migrations"
现在我们应在 database/migrations
目录中看到以下文件。
-
2019_05_03_000001_create_customer_columns.php
-
2019_05_03_000002_create_subscriptions_table.php
-
2019_05_03_000003_create_subscription_items_table.php
这些文件包含订阅表的模式信息。不要担心,我们稍后会来处理这些文件。
User
模型配置
首先,我们来设置我们的第一个计费模型,使用出纳功能设置User
模型。将Billable
特性添加到我们的第一个计费模型,该模型位于App\Models\User
。
use Laravel\Cashier\Billable;
class User extends Authenticatable
{
use Billable;
}
此用户模型的订阅信息将存储在subscriptions
表中。
接下来,让我们创建我们的第二个计费模型并为其添加迁移。
Seller
模型配置
我们的Seller
模型将像User
模型一样包含订阅。但是,我们不仅仅只需要将Billable
特性添加到我们的模型中。我们还需要为Seller
模型添加迁移、配置认证保护等。
除了Seller
模型,我们还将创建另外两个模型:SellerSubscription
和SellerSubscriptionItem
。SellerSubscription
将包含Seller
模型的订阅信息,而SellerSubscriptionItem
模型将负责处理多计划订阅。
简而言之,我们为Seller
模型需要以下模型和表。
- 带有
sellers
表的Seller
模型。 - 带有
seller_subscriptions
表的SellerSubscription
模型。 - 带有
seller_subscription_items
表的SellerSubscriptionItem
模型。
让我们首先使用以下 artisan 命令生成我们的模型。还可以通过在我们的命令中添加-m
标志来生成模型和迁移文件。
php artisan make:model Seller -m
它应该在以下位置生成以下两个文件。
-
Seller.php
(位于/app/models
目录)- Seller模型 -
2021_XX_XX_XXXXXX_create_sellers_table.php
(位于/database/migrations
目录)- 迁移文件
现在,让我们设置我们的Seller
模型。就像User
模型一样,我们需要将Billable
特性添加到位于App\Models\Seller
的Seller
模型中。
use Laravel\Cashier\Billable;
class Seller extends Authenticatable
{
use Billable;
}
在创建sellers
表的迁移文件(2021_XX_XX_XXXXXX_create_sellers_table.php
)中,添加以下模式内容。我们还将引入在发布出纳默认迁移后从2019_05_03_000001_create_customer_columns.php
获得的列。
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateSellersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('sellers', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
// Stripe Cashier's columns
$table->string('stripe_id')->nullable()->index();
$table->string('card_brand')->nullable();
$table->string('card_last_four', 4)->nullable();
$table->timestamp('trial_ends_at')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('sellers');
}
}
我们的Seller
模型基本完成了。但是我们还需要添加特定的卖家订阅模型和迁移。
SellerSubscription
模型配置
默认情况下,App\Models\User
模型的订阅信息将存储在subscriptions
表中。通过使用Billable
特性,我们向Laravel指令,User
模型将具有与Laravel\Cashier\Subscription
模型的hasMany
关系。我们可以通过ManagesSubscriptions
特性来确认这一点。
在这里的Laravel\Cashier\Concerns\ManagesSubscriptions
特性中,我们可以看到subscriptions
方法,它定义了与Laravel\Cashier\Subscription
模型的hasMany
关系。
use Laravel\Cashier\Subscription;
public function subscriptions()
{
return $this->hasMany(Subscription::class, $this->getForeignKey())->orderBy('created_at', 'desc');
}
所以,我们将创建一个名为SellerSubscription
的类,该类继承自Laravel\Cashier\Subscription
模型并继承其属性。
在控制台中运行以下命令以生成SellerSubscription
模型和迁移。
php artisan make:model SellerSubscription -m
这将为以下文件生成以下文件。
-
SellerSubscription.php
(位于/app/models
目录) -
2021_XX_XX_XXXXXX_create_seller_subscriptions_table.php
(位于/database/migrations
目录)
SellerSubscriptionItem
模型配置
User
模型具有存储在subscription_items
表中的多计划订阅。那么我们为什么要把Seller
模型排除在外呢?让我们通过定义一个新的模型SellerSubscriptionItem
为Seller
模型添加多计划订阅的功能。
在终端中运行以下命令以生成SellerSubscriptionItem
模型和相关迁移。
php artisan make:model SellerSubscriptionItem -m
该命令将生成以下文件。
-
SellerSubscriptionItem.php
(位于/app/models
目录) -
文件名:
2021_XX_XX_XXXXXX_create_seller_subscription_items_table.php
(位于/database/migrations
目录)
接下来,稍微修改一下 SellerSubscription
类,使其扩展 Laravel\Cashier\Subscription
类。同时,还需要定义与 Seller
类的 belongsTo
关系和与 SellerSubscriptionItem
类的 hasMany
关系。
<?php
namespace App\Models;
use Laravel\Cashier\Subscription;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class SellerSubscription extends Subscription
{
use HasFactory;
public function owner()
{
return $this->belongsTo(Seller::class);
}
public function items()
{
return $this->hasMany(SellerSubscriptionItem::class);
}
}
接下来,也修改 SellerSubscriptionItem
类,使其扩展 Laravel\Cashier\SubscriptionItem
类。同样地,定义与 SellerSubscription
类的 belongsTo
关系。
<?php
namespace App\Models;
use Laravel\Cashier\SubscriptionItem;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class SellerSubscriptionItem extends SubscriptionItem
{
use HasFactory;
public function subscription()
{
return $this->belongsTo(SellerSubscription::class);
}
}
现在,是时候从 Cashier 的默认迁移中汲取灵感,并相应地更新 seller_subscriptions
和 seller_subscription_items
迁移了。
请更新 seller_subscriptions
表的迁移文件 2021_XX_XX_XXXXXX_create_seller_subscriptions_table.php
,使用以下架构结构。注意引用键 seller_id
并根据您的自定义模型进行修改。
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateSellerSubscriptionsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('seller_subscriptions', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('seller_id');
$table->string('name');
$table->string('stripe_id');
$table->string('stripe_status');
$table->string('stripe_plan')->nullable();
$table->integer('quantity')->nullable();
$table->timestamp('trial_ends_at')->nullable();
$table->timestamp('ends_at')->nullable();
$table->timestamps();
$table->index(['seller_id', 'stripe_status']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('seller_subscriptions');
}
}
接下来,也更新 seller_subscription_items
表的迁移文件 2021_XX_XX_XXXXXX_create_seller_subscription_items_table.php
,使用以下架构结构。同样注意引用键 seller_subscription_id
并根据您的自定义订阅物品模型进行修改。
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateSellerSubscriptionItemsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('seller_subscription_items', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('seller_subscription_id');
$table->string('stripe_id')->index();
$table->string('stripe_plan');
$table->integer('quantity');
$table->timestamps();
// Short key name to support 64 character limit- https://dev.mysqlserver.cn/doc/refman/5.5/en/identifiers.html
$table->unique(['seller_subscription_id', 'stripe_plan'], 'seller_subscription_id_stripe_plan_unique');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('seller_subscription_items');
}
}
在定义了 SellerSubscription
和 SellerSubscriptionItem
模型之后,通过在 Seller
类中添加 subscriptions
方法来定义一个 hasMany
关系。
最后,运行迁移命令以在数据库中创建/更新表。
php artisan migrate
Seller
模型
现在,修改 Seller
模型以覆盖从 Billable
特性继承来的 subscriptions
关系。而不是将关系定义为 Laravel\Cashier\Subscription
类,应当使用 App\Models\SellerSubscription
定义。
您的完成后的卖家模型应该看起来像这样。
<?php
namespace App\Models;
use Laravel\Cashier\Billable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class Seller extends Model
{
use HasFactory, Billable;
public function subscriptions()
{
return $this->hasMany(SellerSubscription::class)->orderBy('created_at', 'desc');
}
}
接下来,让我们为 Seller
模型设置 Stripe webhooks。默认情况下,Cashier 将使用 /stripe/webhook
路由来处理默认配置模型的 Stripe webhooks。
Seller
模型的 webhooks。
当客户的支付方式被拒绝或发生许多此类事件时,Stripe 可以通知您的应用。我们需要确保我们的应用正在处理这些事件。Cashier 包通过使用 /stripe/webhook
路由来处理这些事件而变得简单。
默认情况下,它为默认配置模型 App\Models\User
处理这些事件。在我们的情况下,我们应该注册不同的路由来处理我们的自定义模型的 Stripe webhooks。
首先,使用 artisan 命令生成一个名为 SellerWebhookController
的控制器。
php artisan make:controller SellerWebhookController
接下来,修改 SellerWebhookController
,通过扩展 Laravel\Cashier\Http\Controllers\WebhookController
类,并修改 getUserByStripeId
方法以使用我们的自定义模型 Seller
。
<?php
namespace App\Http\Controllers;
use App\Models\Seller;
use Laravel\Cashier\Http\Controllers\WebhookController;
class SellerWebhookController extends WebhookController
{
protected function getUserByStripeId($stripeId)
{
return Seller::where('stripe_id', $stripeId)->first();
}
}
控制器之后,注册一个新的路由以处理来自 Stripe 的 webhook 事件。
use App\Http\Controllers\SellerWebhookController;
// Route for handling Stripe events
Route::post('/stripe/seller/webhook', [SellerWebhookController::class, 'handleWebhook']);
接下来,在您的 Stripe 控制面板中,您应该在 https://{yourapplication.com}/stripe/seller/webhook
URL 上启用以下 webhooks。
-
customer.subscription.updated
- 当订阅更新时 -
customer.subscription.deleted
- 当订阅取消/删除时。 -
customer.updated
- 当客户信息更新时。 -
customer.deleted
- 当客户被删除时。 -
invoice.payment_action_required
- 当支付方式需要采取行动时。通常是当客户的卡被拒绝时。
就这样,我们的应用现在已准备好处理自定义可计费模型 Seller
的 Stripe webhooks。
配置认证守卫和提供商
在我们的应用中,卖家可以对自己进行认证。因此,将 Seller
模型配置为利用 Laravel 的认证功能是有意义的。
首先,在 config/auth.php
文件的 providers
数组中注册一个新的用户提供者,用于我们的可计费模型 Seller
,并在 guards
数组中也注册一个新的防护者。
'guards' => [
// ...
'seller' => [
'driver' => 'session',
'provider' => 'sellers',
],
// ...
],
// ...
'providers' => [
// ...
'sellers' => [
'driver' => 'eloquent',
'model' => App\Models\Seller::class,
],
// ...
]
完成这一切后,我们应该可以使用 $request->user('seller')
辅助函数来检索认证过的卖家。
用途
经过所有设置,我们终于可以开始使用 Cashier 与 Seller
模型了。现在,我们应该可以使用 $request->user('seller')
来检索认证过的卖家,并使用 config
辅助函数覆盖 Cashier 的默认模型。
// retrieve authenticated seller
$seller = $request->user('seller');
// override cashiers default model at the runtime
config(['cashier.model' => 'App\Models\Seller']);
让我们看看在实际代码中如何使用它。
创建新的计划
在为自定义模型 Seller
创建新的计划时,首先通过使用 $request->user('seller')
辅助函数检索它。然后使用 config
辅助函数在运行时将 Cashier 的可计费模型设置为 Seller
。
use Illuminate\Http\Request;
Route::post('/seller/subscribe', function (Request $request) {
config(['cashier.model' => 'App\Models\Seller']);
$request->user('seller')->newSubscription(
'default', 'price_premium'
)->create($request->paymentMethodId);
// ...
});
检索 Stripe 客户端
当我们需要使用 Stripe ID 检索一个客户时,应使用 Cashier::findBillable()
。但在那之前,不要忘记使用 config
辅助函数设置 Cashier 的可计费模型。
use Laravel\Cashier\Cashier;
config(['cashier.model' => 'App\Models\Seller']);
$seller = Cashier::findBillable($stripeId);
Stripe 计费门户
如果您在应用程序中使用 Stripe 的 计费门户,可以像这样将我们的自定义 Seller
模型重定向到其计费门户。
use Illuminate\Http\Request;
Route::get('/billing-portal', function (Request $request) {
// Override cashier's default model in case
config(['cashier.model' => 'App\Models\Seller']);
// Retrive authenticated seller
$seller = $request->user('seller');
return $seller->redirectToBillingPortal();
});
这就完成了!这些是使用 Cashier 包处理多个模型的一些方法。您可以在 文档 中找到有关 Cashier 的更多信息。
总结
Laravel Cashier 是一个用于管理 Laravel 订阅的出色包。当然,可以以其他方式使用 Cashier 多个模型。当您有两个以上可计费模型时,这是一种您可以使用的解决方案。
无论如何,这是一篇很长的文章,感谢您阅读!如果您对文章有任何疑问,请在我推特上 @swapnil_bhavsar 上留言。
PS:以下是该项目在 GitHub 上的源代码 - https://github.com/IamSwap/laravel-cashier-multiple-models
rajesh, sumit, robindirksen1, ajay1991cse, driesvints 喜欢了这篇文章