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 收费功能,可以帮助您在订阅时或按需处理 Recurrency(如下所述)时向订阅者收费。
为了保持与 Laravel Cashier 一样优雅,您必须通过添加 Stripe 到 config/services.php
文件来配置它。
'stripe' => [ 'key' => env('STRIPE_KEY'), 'secret' => env('STRIPE_SECRET'), ],
使用 Stripe
如果您现在非常熟悉在不主动传递支付方式的情况下订阅、延长、升级或取消订阅,还有一些额外的功能可以控制支付。
-
计划的定价是从您的
plans
表中获取的。 如果您想进行一些处理并设置另一个收费价格,您可以这样做。稍后会有解释。 -
延长或升级不会向用户收费,只有订阅方法会自动为您这样做,如果您告诉了包这样做。您希望从订阅开始时向用户收费,因此您必须遍历所有订阅者并检查他们的订阅是否已过期,然后通过例如 cron 命令自动续订。
-
您必须传递一个 Stripe token。每次您想要进行支付时都需要传递一个 Stripe token。此包通过拥有一个本地 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()
、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 Plans & Stripe Coupons。此包可以使您成为订阅和周期性的主人,而无需使用第三方处理。主要优势是您可以为天数定义自己的周期性金额,而Stripe则限于每日、每周、每月和每年。
要处理周期性,有一个名为renewSubscription
的方法可以为您完成这项工作。您将必须遍历所有订阅者。最好运行一个cron命令,该命令将在每个订阅者上调用该方法。
此方法将(如果需要)续订用户的订阅。
foreach(User::all() as $user) { $user->renewSubscription(); }
如果您使用集成的Stripe Charge功能,您将必须传递一个Stripe Token来为此用户充电。由于Stripe Tokens是一次性的,您将必须管理从您的用户那里获取令牌。
$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 Token来完成此操作。
$user->withStripe()->withStripeToken('tok_...')->chargeForLastDueSubscription();
为此方法,在成功充电或失败充电时抛出\Creatydev\Plans\Events\Stripe\DueSubscriptionChargeSuccess
和\Creatydev\Plans\Events\Stripe\DueSubscriptionChargeFailed
。
模型扩展
您还可以扩展计划模型。
注意 $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 { // }
事件
当使用订阅计划时,您想要监听事件以自动运行可能会更改您的应用程序的代码。
事件很容易使用。如果您不熟悉,您可以在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 - 初始工作。
- 杜肯斯·特莱玛克 - 支持 Laravel 5.8 - 6.2 - Creatydev
请参阅参与此项目的贡献者列表。
许可证
本项目采用 MIT 许可证 - 有关详细信息,请参阅 LICENSE.md 文件。