miroslawlach / 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
rennokki / plans 包存档
该包已不再维护
尝试创建自己版本的包或使用 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, ...)
默认情况下,该计划被标记为 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 订单功能,帮助您在订阅时或按需处理 续订(如下文所述)时向订阅者收费。
为了保持与 Laravel Cashier 一样优雅,您需要通过添加 Stripe 到 config/services.php
文件来配置它。
'stripe' => [ 'key' => env('STRIPE_KEY'), 'secret' => env('STRIPE_SECRET'), ],
使用 Stripe
如果您现在非常熟悉订阅、延伸、升级或取消订阅,而不需要主动传递支付方法,还有一些额外的功能可以让你控制支付。
-
计划的定价从您的
plans
表中获取。如果您想进行一些处理并为收费设置另一个价格,您可以这样做。稍后解释。 -
延伸或升级不会向用户收费,只有订阅方法会自动为您这样做,如果您告诉包这样做。您希望从订阅开始时向用户收费,因此您必须遍历所有订阅者并检查他们的订阅是否已过期,然后在例如 croned 命令中自动续订。
-
您必须传递一个Stripe令牌。每次您想要进行支付时,都需要传递一个Stripe令牌。这个包通过为Stripe客户创建本地表来帮助您跟踪您的客户。
-
成功或失败的支付将触发事件。无需设置webhooks。事件由Stripe Charge触发,无论其是成功还是失败。
使用Stripe Charge订阅
要使用Stripe Token订阅您的用户,您必须明确传递一个Stripe Token
$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是一次性使用的,您将需要管理从您的用户处获取令牌。
$user->renewSubscription('tok...');
像往常一样,如果支付成功,将触发Rennokki\Plans\Stripe\ChargeSuccessful
事件,如果支付失败,将触发Rennokki\Plans\Stripe\ChargeFailed
事件。
待付款订阅
不使用本地Stripe Charge功能的订阅永远不会标记为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 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. ], ];