creatydev / plans
Laravel Plans 是一个用于需要管理套餐、功能、订阅和计划的事件的 SaaS 应用的包。
Requires
- laravel/framework: ~5.8.0|~6.0|~6.2.0|^8.22.2|^9.19|^10.0
- stripe/stripe-php: >=12.1
Requires (Dev)
- orchestra/database: ~3.7.0|~3.8.0|~4.0.0
- orchestra/testbench: ~3.5.0|~3.6.0|~3.7.0|~3.8.0
- phpunit/phpunit: ^6.2|^7.0
This package is auto-updated.
Last update: 2024-09-04 01:17:23 UTC
README
Laravel Plans
Laravel Plans 是一个用于需要管理套餐、功能、订阅和计划的事件的 SaaS 应用的包。
Laravel Cashier
虽然 Laravel Cashier 在这项工作中做得很好,但仍有一些功能对 SaaS 应用很有用
- 可计数的、有限的功能 - 如果你打算限制订阅者可使用的资源数量并跟踪使用情况,此包为你做到这一点。
- 内置循环,可自定义循环周期 - 当 Stripe 或限制你每天、每周、每月或每年订阅用户时,此包允许你为任何订阅或计划定义自己的天数。
- 基于事件的本质 - 你可以监听事件。如果你的用户及时支付账单,你能否给下一个订阅提供 3 天免费使用呢?
安装
安装包
$ composer require creatydev/plans
如果你的 Laravel 版本不支持包发现,请在 config/app.php
文件的 providers
数组中添加此行
Creatydev\Plans\PlansServiceProvider::class,
发布配置文件和迁移文件
$ php artisan vendor:publish --provider=Creatydev\Plans\PlansServiceProvider
迁移数据库
$ php artisan migrate
将 HasPlans
特性添加到你的 Eloquent 模型
use Creatydev\Plans\Traits\HasPlans; class User extends Model { use HasPlans; ... }
创建计划
类似订阅的系统的基本单位是计划。你可以使用 Creatydev\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()
关系并传递所需数量的 Creatydev\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()
方法也会减少使用,但永远不会达到负值。
支付
即使不明确使用集成支付,这个包也能很好地工作。这是好事,因为本文档中解释的功能无需使用集成支付系统即可工作。如果您有自己的支付系统,您可以使用它。请确保检查下面的 Recurrency 部分,了解您如何根据用户的最后订阅收费以及如何处理一般性的可续订性。
配置Stripe
本包包含一个Stripe Charge特性,帮助您在订阅或按需处理 Recurrency(以下解释)时向订阅者收费。
为了保持与Laravel Cashier一样优雅,您必须通过添加Stripe到 config/services.php
文件来配置它。
'stripe' => [ 'key' => env('STRIPE_KEY'), 'secret' => env('STRIPE_SECRET'), ],
使用Stripe
如果您现在非常熟悉订阅、扩展、升级或取消订阅而不需要主动传递支付方式,还有一些额外的功能可以让您控制支付。
-
计划的定价从您的
plans
表中获取。如果您想进行一些处理并设置其他收费价格,可以这样做。稍后会有解释。 -
扩展或升级不会向您的用户收费,只有订阅方法会自动为您完成这项工作,前提是您告诉了套餐。您想在用户订阅开始时立即向他们收费,那么您必须遍历所有订阅者并检查他们的订阅是否过期,并在cron命令中自动续订,例如。
-
您必须传递一个Stripe令牌。每次您想要进行支付时,都需要传递一个Stripe令牌。这个套餐通过有一个本地的Stripe客户表来帮助您跟踪您的客户。
-
对于成功或失败的支付将触发事件。无需设置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 Charge功能,您将需要传递一个Stripe令牌来从该用户处收费。由于Stripe令牌是一次性使用的,您将不得不管理从您的用户那里获取令牌。
$user->renewSubscription('tok...');
一如既往,如果支付已处理,将触发Creatydev\Plans\Stripe\ChargeSuccessful
事件,如果支付失败,将触发Creatydev\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令牌进行此操作
$user->withStripe()->withStripeToken('tok_...')->chargeForLastDueSubscription();
对于此方法,在成功收费或失败收费时,将抛出\Creatydev\Plans\Events\Stripe\DueSubscriptionChargeSuccess
和\Creatydev\Plans\Events\Stripe\DueSubscriptionChargeFailed
。
模型扩展
您还可以扩展Plan模型
注意 $table
、$fillable
、$cast
、Relationships
将会被继承。
PlanModel
<?php namespace App\Models; use Creatydev\Plans\Models\PlanModel; class Plan extends PlanModel { // }
PlanFeatureModel
<?php namespace App\Models; use Creatydev\Plans\Models\PlanFeatureModel; class PlanFeature extends PlanFeatureModel { // }
PlanSubscriptionModel
<?php namespace App\Models; use Creatydev\Plans\Models\PlanSubscriptionModel; class PlanSubscription extends PlanSubscriptionModel { // }
PlanSubscriptionUsageModel
<?php namespace App\Models; use Creatydev\Plans\Models\PlanSubscriptionUsageModel; class PlanSubscriptionUsage extends PlanSubscriptionUsageModel { // }
StripteCustomerModel
<?php namespace App\Models; use Creatydev\Plans\Models\StripteCustomerModel; class StripeCustomer extends StripteCustomerModel { // }
Events
当使用订阅计划时,您希望监听事件以自动运行可能会更改您应用程序代码的代码。
事件易于使用。如果您不熟悉,可以查看 Laravel官方文档关于事件的部分。
您所需要做的只是将以下事件实现到您的 EventServiceProvider.php
文件中。每个事件都将有自己的成员,您可以通过监听器中的 handle()
方法的 $event
变量来访问这些成员。
$listen = [ ... \Creatydev\Plans\Events\CancelSubscription::class => [ // $event->model = The model that cancelled the subscription. // $event->subscription = The subscription that was cancelled. ], \Creatydev\Plans\Events\NewSubscription::class => [ // $event->model = The model that was subscribed. // $event->subscription = The subscription that was created. ], \Creatydev\Plans\Events\NewSubscriptionUntil::class => [ // $event->model = The model that was subscribed. // $event->subscription = The subscription that was created. ], \Creatydev\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. ], \Creatydev\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. ], \Creatydev\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 ], \Creatydev\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 ], \Creatydev\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 ], \Creatydev\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 ], \Creatydev\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. ], \Creatydev\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. ], \Creatydev\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. ], \Creatydev\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. ], ];
作者
- Georgescu Alexandru - 初始工作。
- Dukens Thelemaque - Laravel 5.8 - 6.2 支持 - Creatydev
还可以查看参与此项目的贡献者列表。
许可证
本项目采用MIT许可证 - 请参阅LICENSE.md文件以获取详细信息。