qanoune / plans
Laravel Plans 是一个用于需要管理计划、功能、订阅、计划或有限、可计数的功能的 SaaS 应用程序的包。
Requires
- doctrine/dbal: ^2.8.0
- laravel/framework: ~5.5.0|~5.6.0|~5.7.0
- stripe/stripe-php: ^6.11.0
Requires (Dev)
- orchestra/database: ~3.5.0|~3.6.0
- orchestra/testbench: ~3.5.0|~3.6.0
- phpunit/phpunit: ^6.2|^7.0
This package is auto-updated.
Last update: 2024-09-05 07:36:58 UTC
README
Laravel Plans
Laravel Plans 是一个用于需要管理计划、功能、订阅、计划或有限、可计数的功能的 SaaS 应用程序的包。
Laravel Cashier
虽然 Laravel Cashier 做得很好,但还有一些对于 SaaS 应用程序有用的功能
- 可计数的,有限的特性 - 如果您打算限制订阅者可以拥有的资源量并跟踪使用情况,这个包会为您做到这一点。
- 内置可重复性,可自定义的可重复性周期 - 虽然 Stripe 或限制您每天、每周、每月或每年订阅用户,但此包允许您为任何订阅或计划定义自己的天数。
- 基于事件的本质 - 您可以监听事件。如果用户及时支付发票,您能否给下一个订阅提供3天免费试用?
安装
安装包
$ composer require Qanoune/plans
如果您的 Laravel 版本不支持包发现,请在您的 config/app.php
文件中的 providers
数组中添加此行
Qanoune\Plans\PlansServiceProvider::class,
发布配置文件和迁移文件
$ php artisan vendor:publish
迁移数据库
$ php artisan migrate
将 HasPlans
特性添加到您的 Eloquent 模型中
use Qanoune\Plans\Traits\HasPlans; class User extends Model { use HasPlans; ... }
创建计划
类似于订阅的系统的基本单位是计划。您可以使用 Qanoune\Plans\Models\PlanModel
或您的模型创建它,如果您已经实现了自己的模型。
$plan = PlanModel::create([ 'name' => 'Enterprise', 'description' => 'The biggest plans of all.', 'price' => 20.99, 'currency' => 'EUR', 'duration' => 30, // in days 'metadata' => ['key1' => 'value1', ...], ]);
功能
每个计划都有功能。它们可以是可计数的,这些是有限或无限的,或者只是为了存储信息,例如特定的权限。
使用以下方式标记功能类型:
feature
,是一个单独的字符串,不需要计数。例如,您可以存储权限。limit
,是一个数字。对于此类功能,limit
属性将被填充。它的目的是衡量用户已消耗了多少此类功能,从本次订阅。例如,您可以计算用户在一个月内(或在此示例中的 30 天周期内)消耗了多少构建分钟。
注意:对于无限功能,limit
字段将被设置为任何负值。
要将功能附加到您的计划,您可以使用关系 features()
并传递所需数量的 Qanoune\Plans\Models\PlanFeatureModel
实例。
$plan->features()->saveMany([ new PlanFeatureModel([ 'name' => 'Vault access', 'code' => 'vault.access', 'description' => 'Offering access to the vault.', 'type' => 'feature', 'metadata' => ['key1' => 'value1', ...], ]), new PlanFeatureModel([ 'name' => 'Build minutes', 'code' => 'build.minutes', 'description' => 'Build minutes used for CI/CD.', 'type' => 'limit', 'limit' => 2000, 'metadata' => ['key1' => 'value1', ...], ]), new PlanFeatureModel([ 'name' => 'Users amount', 'code' => 'users.amount', 'description' => 'The maximum amount of users that can use the app at the same time.', 'type' => 'limit', 'limit' => -1, // or any negative value 'metadata' => ['key1' => 'value1', ...], ]), ... ]);
稍后,您可以直接从订阅中检索权限。
$subscription->features()->get(); // All features $subscription->features()->code($codeId)->first(); // Feature with a specific code. $subscription->features()->limited()->get(); // Only countable/unlimited features. $subscription->features()->feature()->get(); // Uncountable, permission-like features.
订阅计划
您的用户可以订阅计划,持续一定天数或直到特定日期。
$subscription = $user->subscribeTo($plan, 30); // 30 days $subscription->remainingDays(); // 29 (29 days, 23 hours, ...)
默认情况下,计划被标记为 recurring
,因此如果计划续订,则合格。
如果您不希望有重复订阅,可以将 false
作为第三个参数传递。
$subscription = $user->subscribeTo($plan, 30, false); // 30 days, non-recurrent
如果您计划将用户订阅到特定日期,您可以传递包含日期、日期时间或 Carbon 实例的字符串。
如果您的订阅是重复的,则可重复周期的时间是到期日期与当前日期之间的差异。
$user->subscribeToUntil($plan, '2018-12-21'); $user->subscribeToUntil($plan, '2018-12-21 16:54:11'); $user->subscribeToUntil($plan, Carbon::create(2018, 12, 21, 16, 54, 11)); $user->subscribeToUntil($plan, '2018-12-21', false); // no recurrency
注意:如果用户已经订阅,则 subscribeTo()
将返回 false。为了避免这种情况,升级或扩展订阅。
升级订阅
升级当前订阅的计划可以通过两种方式完成:它要么通过传入的天数扩展当前订阅,要么创建另一个扩展当前订阅的订阅。
无论哪种方式,您都必须传递一个布尔值作为第三个参数。默认情况下,它扩展当前订阅。
// The current subscription got longer with 60 days. $currentSubscription = $user->upgradeCurrentPlanTo($anotherPlan, 60, true); // A new subscription, with 60 days valability, starting when the current one ends. $newSubscription = $user->upgradeCurrentPlanTo($anotherPlan, 60, false);
与订阅方法类似,升级也支持将日期作为第三个参数,如果您计划在当前订阅结束时创建新的订阅。
$user->upgradeCurrentPlanToUntil($anotherPlan, '2018-12-21', false); $user->upgradeCurrentPlanToUntil($anotherPlan, '2018-12-21 16:54:11', false); $user->upgradeCurrentPlanToUntil($anotherPlan, Carbon::create(2018, 12, 21, 16, 54, 11), false);
如果您的第三个参数为false
,则可以传递第四个参数,如果您想将新的订阅标记为周期性订阅,则应该传递它。
// Creates a new subscription that starts at the end of the current one, for 30 days and recurrent. $newSubscription = $user->upgradeCurrentPlanTo($anotherPlan, 30, false, true);
扩展当前订阅
升级使用扩展方法,因此使用相同的参数,但您不需要将计划模型作为第一个参数传递。
// The current subscription got extended with 60 days. $currentSubscription = $user->extendCurrentSubscriptionWith(60, true); // A new subscription, which starts at the end of the current one. $newSubscrioption = $user->extendCurrentSubscriptionWith(60, false); // A new subscription, which starts at the end of the current one and is recurring. $newSubscrioption = $user->extendCurrentSubscriptionWith(60, false, true);
扩展也支持使用日期
$user->extendCurrentSubscriptionUntil('2018-12-21');
取消订阅
您可以通过取消订阅。如果一个订阅尚未完成(尚未过期),它将被标记为待取消
。当过期日期超过当前时间并且仍然取消时,它将完全取消。
// Returns false if there is not an active subscription. $user->cancelCurrentSubscription(); $lastActiveSubscription = $user->lastActiveSubscription(); $lastActiveSubscription->isCancelled(); // true $lastActiveSubscription->isPendingCancellation(); // true $lastActiveSubscription->isActive(); // false $lastActiveSubscription->hasStarted(); $lastActiveSubscription->hasExpired();
消耗可计数的特性
要消耗limit
类型特性,您必须在订阅实例中调用consumeFeature()
方法。
要检索订阅实例,您可以在实现该特性的用户实例中调用activeSubscription()
方法。作为一个预检查,不要忘记从用户实例中调用hasActiveSubscription()
以确保它已经订阅了。
if ($user->hasActiveSubscription()) { $subscription = $user->activeSubscription(); $subscription->consumeFeature('build.minutes', 10); $subscription->getUsageOf('build.minutes'); // 10 $subscription->getRemainingOf('build.minutes'); // 1990 }
consumeFeature()
方法将返回
false
,如果特性不存在,特性不是limit
或数量超出当前特性允许的量true
,如果消耗操作成功
// Note: The remaining of build.minutes is now 1990 $subscription->consumeFeature('build.minutes', 1991); // false $subscription->consumeFeature('build.hours', 1); // false $subscription->consumeFeature('build.minutes', 30); // true $subscription->getUsageOf('build.minutes'); // 40 $subscription->getRemainingOf('build.minutes'); // 1960
如果consumeFeature()
遇到无限特性,它会消耗它,并且它也会像数据库中的正常记录一样跟踪使用情况,但永远不会返回false。剩余的总是无限特性的-1
。
consumeFeature()
方法的逆方法为unconsumeFeature()
。这工作方式相同,但方向相反
// Note: The remaining of build.minutes is 1960 $subscription->consumeFeature('build.minutes', 60); // true $subscription->getUsageOf('build.minutes'); // 100 $subscription->getRemainingOf('build.minutes'); // 1900 $subscription->unconsumeFeature('build.minutes', 100); // true $subscription->unconsumeFeature('build.hours', 1); // false $subscription->getUsageOf('build.minutes'); // 0 $subscription->getRemainingOf('build.minutes'); // 2000
在无限特性上使用unconsumeFeature()
方法也会减少使用,但永远不会达到负值。
支付
即使没有明确使用集成的支付,这个包也能很好地工作。这是好的,因为本文档中之前解释的特性可以在不使用集成的支付系统的情况下工作。如果您有自己的支付系统,您可以使用它。请确保您检查下面的周期性部分,以了解您如何根据用户的最后订阅向用户收费以及如何处理周期性。
配置Stripe
此包附带Stripe Charge特性,可以帮助您在订阅时或按需处理周期性(如下所述)时向订阅者收费。
为了保持与Laravel Cashier一样优雅,您必须通过添加Stripe来配置您的config/services.php
文件。
'stripe' => [ 'key' => env('STRIPE_KEY'), 'secret' => env('STRIPE_SECRET'), ],
使用Stripe
如果您现在非常熟悉在不主动传递支付方法的情况下订阅、扩展、升级或取消订阅,还有一些额外的功能可以控制支付
-
计划的定价从您的
plans
表获取。如果您想进行一些处理并设置不同的收费价格,您可以这样做。稍后会解释。 -
扩展或升级不会向用户收费,只有订阅方法会自动为您这样做,如果您告诉包这样做。您希望从订阅开始时向用户收费,因此您必须遍历所有订阅者并检查他们的订阅是否已过期,然后在cron命令中自动续订。
-
您必须传递一个Stripe令牌。每次您想要进行支付时,都需要传递一个Stripe令牌。此包通过有一个本地的Stripe Customers表来帮助您跟踪您的客户。
-
会触发成功或失败的支付事件。无需设置webhooks。事件由Stripe Charge驱动,无论是失败还是成功。
使用Stripe Charge进行订阅
要使用 Stripe 令牌订阅用户,您必须显式传递一个 Stripe 令牌。
$user->withStripe()->withStripeToken('tok_...')->subscribeTo($plan, 53); // 53 days
默认情况下,充电金额是从 plans
表中检索的。然而,您可以在过程中更改价格,由您自行决定。
$user->withStripe()->setChargingPriceTo(10, 'USD')->withStripeToken('tok_...')->subscribeTo($plan, 30);
无论计划的金额是多少,充电价格将是 $10,因为我们覆盖了充电价格。
由于充电不支持 extendCurrentSubscriptionWith()
、extendCurrentSubscriptionUntil()
、upgradeupgradeCurrentPlanTo()
和 upgradeCurrentPlanToUntil()
,使用 withStripe()
将没有效果,除非您告诉它们创建一个新计划,作为当前计划的扩展
// This will create a new upgraded plan that starts at the end of the current one, which is recurring and will be needed to be paid to be active. $user->withStripe()->upgradeCurrentPlanTo($plan, 30, false, true);
请注意,即使如此,该方法也不会对用户收费,因为新的订阅尚未开始。由于这个新的订阅将在当前订阅结束后开始,您必须按照以下说明手动收费。
定期性
这个包不支持 Cashier 支持的功能:Stripe 计划和 Stripe 优惠券。这个包可以使您成为主人,无需使用第三方来处理订阅和定期性。主要优势是您可以定义自己的周期天数,而 Stripe 限制为每日、每周、每月和每年。
要处理定期性,有一个名为 renewSubscription
的方法可以为您完成这项工作。您将必须遍历所有订阅者。最好运行一个 cron 命令,该命令将调用每个订阅者的方法。
此方法将在需要时为用户更新(续订)订阅。
foreach(User::all() as $user) { $user->renewSubscription(); }
如果您使用的是集成的 Stripe 充值功能,您将必须传递一个 Stripe 令牌来从该用户那里进行充值。由于 Stripe 令牌是一次性的(一次性使用),您必须管理从您的用户那里获取令牌。
$user->renewSubscription('tok...');
一如既往,如果支付已处理,将触发 Qanoune\Plans\Stripe\ChargeSuccessful
事件,如果支付失败,将触发 Qanoune\Plans\Stripe\ChargeFailed
事件。
到期订阅
不使用本地 Stripe 充值功能的订阅永远不会被标记为 Due
,因为它们默认都是已支付的。
如果您的应用程序使用自己的支付方式,您可以传递一个闭包给以下 chargeForLastDueSubscription()
方法,这有助于您控制到期订阅。
$user->chargeForLastDueSubscription(function($subscription) { // process the payment here if($paymentSuccessful) { $subscription->update([ 'is_paid' => true, 'starts_on' => Carbon::now(), 'expires_on' => Carbon::now()->addDays($subscription->recurring_each_days), ]); return $subscription; } return null; });
在支付失败时,它们会被标记为 Due。它们需要支付,并且每个动作,如订阅、升级或扩展,都总是尝试通过删除最后一个,创建在上述动作中提到的期望之一并尝试支付它。
为此,chargeForLastDueSubscription()
将帮助您对最后一个未支付的订阅进行收费。您必须显式传递一个 Stripe 令牌进行此操作。
$user->withStripe()->withStripeToken('tok_...')->chargeForLastDueSubscription();
为此方法,在成功充电或失败充电时,将抛出 \Qanoune\Plans\Events\Stripe\DueSubscriptionChargeSuccess
和 \Qanoune\Plans\Events\Stripe\DueSubscriptionChargeFailed
。
事件
当使用订阅计划时,您希望监听事件以自动运行可能会更改您应用程序的代码。
事件使用简单。如果您不熟悉,您可以查看 Laravel 官方文档中的事件。
您需要做的就是将以下事件实现到您的 EventServiceProvider.php
文件中。每个事件都将有自己的成员,可以在监听器的 handle()
方法中的 $event
变量中访问。
$listen = [ ... \Qanoune\Plans\Events\CancelSubscription::class => [ // $event->model = The model that cancelled the subscription. // $event->subscription = The subscription that was cancelled. ], \Qanoune\Plans\Events\NewSubscription::class => [ // $event->model = The model that was subscribed. // $event->subscription = The subscription that was created. ], \Qanoune\Plans\Events\NewSubscriptionUntil::class => [ // $event->model = The model that was subscribed. // $event->subscription = The subscription that was created. ], \Qanoune\Plans\Events\ExtendSubscription::class => [ // $event->model = The model that extended the subscription. // $event->subscription = The subscription that was extended. // $event->startFromNow = If the subscription is exteded now or is created a new subscription, in the future. // $event->newSubscription = If the startFromNow is false, here will be sent the new subscription that starts after the current one ends. ], \Qanoune\Plans\Events\ExtendSubscriptionUntil::class => [ // $event->model = The model that extended the subscription. // $event->subscription = The subscription that was extended. // $event->expiresOn = The Carbon instance of the date when the subscription will expire. // $event->startFromNow = If the subscription is exteded now or is created a new subscription, in the future. // $event->newSubscription = If the startFromNow is false, here will be sent the new subscription that starts after the current one ends. ], \Qanoune\Plans\Events\UpgradeSubscription::class => [ // $event->model = The model that upgraded the subscription. // $event->subscription = The current subscription. // $event->startFromNow = If the subscription is upgraded now or is created a new subscription, in the future. // $event->oldPlan = Here lies the current (which is now old) plan. // $event->newPlan = Here lies the new plan. If it's the same plan, it will match with the $event->oldPlan ], \Qanoune\Plans\Events\UpgradeSubscriptionUntil::class => [ // $event->model = The model that upgraded the subscription. // $event->subscription = The current subscription. // $event->expiresOn = The Carbon instance of the date when the subscription will expire. // $event->startFromNow = If the subscription is upgraded now or is created a new subscription, in the future. // $event->oldPlan = Here lies the current (which is now old) plan. // $event->newPlan = Here lies the new plan. If it's the same plan, it will match with the $event->oldPlan ], \Qanoune\Plans\Events\FeatureConsumed::class => [ // $event->subscription = The current subscription. // $event->feature = The feature that was used. // $event->used = The amount used. // $event->remaining = The total amount remaining. If the feature is unlimited, will return -1 ], \Qanoune\Plans\Events\FeatureUnconsumed::class => [ // $event->subscription = The current subscription. // $event->feature = The feature that was used. // $event->used = The amount reverted. // $event->remaining = The total amount remaining. If the feature is unlimited, will return -1 ], \Qanoune\Plans\Events\Stripe\ChargeFailed::class => [ // $event->model = The model for which the payment failed. // $event->subscription = The subscription. // $event->exception = The exception thrown by the Stripe API wrapper. ], \Qanoune\Plans\Events\Stripe\ChargeSuccessful::class => [ // $event->model = The model for which the payment succeded. // $event->subscription = The subscription which was updated as paid. // $event->stripeCharge = The response coming from the Stripe API wrapper. ], \Qanoune\Plans\Events\Stripe\DueSubscriptionChargeFailed::class => [ // $event->model = The model for which the payment failed. // $event->subscription = The due subscription that cannot be paid. // $event->exception = The exception thrown by the Stripe API wrapper. ], \Qanoune\Plans\Events\Stripe\DueSubscriptionChargeSuccess::class => [ // $event->model = The model for which the payment succeded. // $event->subscription = The due subscription that was paid. // $event->stripeCharge = The response coming from the Stripe API wrapper. ], ];