rennokki / 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
README
此包不再维护
尝试fork您自己的包版本或使用Laravel Spark以获得完整的计划体验。
Laravel Plans
Laravel Plans是一个用于SaaS应用程序的包,该应用程序需要管理计划、功能、订阅、计划的或有限、可计数的功能的权限。
Laravel Cashier
虽然Laravel Cashier确实可以很好地完成这项工作,但还有一些功能对于SaaS应用程序可能很有用
- 可计数、有限的功能 - 如果您计划限制订阅者拥有的资源量并跟踪使用情况,此包会为您完成。
- 内置循环,可自定义循环周期 - 当Stripe或限制您只能按日、周、月或年订阅用户时,此包允许您为任何订阅或计划定义自己的天数。
- 基于事件驱动 - 您可以监听事件。如果您的用户及时支付发票,您能给他们提供3天的免费服务吗?
安装
安装包
$ composer require rennokki/plans
如果您的Laravel版本不支持包发现,请将此行添加到您的config/app.php
文件中的providers
数组中
Rennokki\Plans\PlansServiceProvider::class,
发布配置文件和迁移文件
$ php artisan vendor:publish
迁移数据库
$ php artisan migrate
将HasPlans
特质添加到您的Eloquent模型中
use Rennokki\Plans\Traits\HasPlans; class User extends Model { use HasPlans; ... }
创建计划
订阅类似系统的基本单元是计划。您可以使用Rennokki\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()
并传递所需的Rennokki\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, ...)
默认情况下,计划被标记为循环
,因此到期后如果需要,可以像下面循环部分解释的那样进行扩展。
如果您不希望是循环订阅,可以将第三个参数传递为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);
扩展当前订阅
升级使用扩展方法,因此它使用相同的参数,但您不需要将Plan模型作为第一个参数传递。
// 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 收费功能,帮助您在订阅时或按需处理 周期性 时向订阅者收费。
为了保持与 Laravel Cashier 一样的高雅,您必须通过添加 Stripe 来配置您的 config/services.php
文件。
'stripe' => [ 'key' => env('STRIPE_KEY'), 'secret' => env('STRIPE_SECRET'), ],
使用 Stripe
如果您现在对不主动传递支付方式的情况下进行订阅、扩展、升级或取消订阅已经很熟悉了,那么还有一些额外的功能可以让您控制支付。
-
计划的价格从您的
plans
表中获取。如果您想进行一些处理并为收费设置另一个价格,您可以进行设置。这将在稍后进行解释。 -
扩展或升级不会向用户收费,只有订阅方法会自动为您执行此操作,前提是您告诉该软件包这样做。您希望在订阅开始时向用户收费,因此您必须遍历所有订阅者并检查其订阅是否已过期,然后通过例如 cron 命令自动续订。
-
您必须传递一个 Stripe 令牌。每次您想要进行支付时都需要传递一个 Stripe 令牌。该软件包通过拥有一个本地的 Stripe 客户表来帮助您跟踪您的客户。
-
将触发成功或失败的支付事件。无需设置 webhooks。Stripe 收费事件会触发,无论成功还是失败。
使用 Stripe 收费进行订阅
要使用 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()
、upgradeCurrentPlanTo()
和 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 Charge功能,您将需要传递一个Stripe Token以对该用户的账户进行扣费。由于Stripe Tokens是一次性的(仅限使用一次),您将需要管理从您的用户那里获取Token。
$user->renewSubscription('tok...');
一如既往,如果支付处理成功,将触发Rennokki\Plans\Stripe\ChargeSuccessful
事件;如果支付失败,将触发Rennokki\Plans\Stripe\ChargeFailed
事件。
到期订阅
未使用本地Stripe Charge功能的订阅永远不会被标记为到期
,因为默认情况下,所有这些订阅都已支付。
如果您的应用程序使用自己的支付方式,您可以传递一个闭包给以下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; });
在支付失败的情况下,它们会被标记为到期。它们需要支付,并且每次进行订阅、升级或扩展的操作时,都会尝试通过删除最后一个订阅、创建在上述操作中提到的期望订阅并尝试支付它来重新支付订阅。
为此,chargeForLastDueSubscription()
将帮助您对最后未支付的订阅进行扣费。您将需要明确传递一个Stripe Token。
$user->withStripe()->withStripeToken('tok_...')->chargeForLastDueSubscription();
对于此方法,在扣费成功或失败时,将抛出\Rennokki\Plans\Events\Stripe\DueSubscriptionChargeSuccess
和\Rennokki\Plans\Events\Stripe\DueSubscriptionChargeFailed
事件。
事件
当使用订阅计划时,您希望监听事件以自动运行可能对您的应用程序进行更改的代码。
事件使用简单。如果您不熟悉,可以查看Laravel官方文档关于事件的部分。
您需要做的就是将以下事件实现到您的EventServiceProvider.php
文件中。每个事件都将有自己的成员,可以在监听器的handle()
方法中的$event
变量内访问。
$listen = [ ... \Rennokki\Plans\Events\CancelSubscription::class => [ // $event->model = The model that cancelled the subscription. // $event->subscription = The subscription that was cancelled. ], \Rennokki\Plans\Events\NewSubscription::class => [ // $event->model = The model that was subscribed. // $event->subscription = The subscription that was created. ], \Rennokki\Plans\Events\NewSubscriptionUntil::class => [ // $event->model = The model that was subscribed. // $event->subscription = The subscription that was created. ], \Rennokki\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. ], \Rennokki\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. ], \Rennokki\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 ], \Rennokki\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 ], \Rennokki\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 ], \Rennokki\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 ], \Rennokki\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. ], \Rennokki\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. ], \Rennokki\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. ], \Rennokki\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. ], ];