kazistm/subscriptions
Composer 安装命令:
composer require kazistm/subscriptions
包简介
Flexible Laravel subscription and plan management system with features, limitations, and usage tracking.
README 文档
README
A flexible and extendable subscription and plan management system for Laravel applications, particularly suited for SaaS products. This package provides tools to manage recurring plans, features with usage limits, and subscription lifecycles.
This package is based on, and aims to enhance, the foundation laid by laravelcm/laravel-subscriptions.
Table of Contents
- Key Features & Enhancements
- Requirements
- Installation
- Configuration
- Database Migrations
- Core Concepts & Models
- 1. Subscribable Model
- 2. Plan (KaziSTM\Subscriptions\Models\Plan)
- 3. Limitation (KaziSTM\Subscriptions\Models\Limitation)
- 4. Feature (KaziSTM\Subscriptions\Models\Feature)
- 5. Subscription (KaziSTM\Subscriptions\Models\Subscription)
- 6. Subscription Usage (KaziSTM\Subscriptions\Models\SubscriptionUsage)
- Note on Slugs vs Names/Titles
- Usage
- Middleware
- Extending Models
- Support & Issues
- License
- Credits
Key Features & Enhancements
- Manages Plans, Subscriptions, Features, and Usage tracking.
- Introduces a Limitation model: Logically groups related features (e.g., 'Build Minutes', 'Seats', 'Storage Size'), providing translatable titles and descriptions. Features now primarily hold the value or quota, inheriting context from the Limitation.
- Supports trial periods, grace periods, plan changes, cancellations, and renewals.
- Features can have usage recorded against them, with optional periodic resetting (e.g., monthly email quotas).
- Includes an
artisan subscriptions:installcommand for easy setup (config, migrations, optional model stubs). - Includes Middleware for checking active subscriptions (
subscription.active) and plan features (subscription.feature). - Provides helper relationships (
limitations()) and scopes (scopeWhereHasLimitationSlug()) on the Plan model. - Uses Spatie packages for core functionalities like slugs (
spatie/laravel-sluggable), translatable fields (spatie/laravel-translatable), and sortable models (spatie/eloquent-sortable). - Built with
spatie/laravel-package-toolsfor standard package structure.
Requirements
- PHP: >= 8.2
- Laravel: >= 10.x
Installation
-
Install the package via Composer:
composer require kazistm/subscriptions
-
Run the installation command:
php artisan subscriptions:install
This interactive command will:
- Ask if you want to publish the configuration file (
config/subscriptions.php). Defaults to yes. - Publish the necessary database migrations to your
database/migrationsfolder (it checks for existing migrations with the same base name to avoid duplication). - Ask if you want to publish basic model stubs (
Plan.php,Subscription.php, etc.) to yourapp/Modelsdirectory. This is optional and only needed if you plan to directly extend or override the package's models. Defaults to no. - Automatically run
php artisan migrateto create the required database tables.
- Ask if you want to publish the configuration file (
Configuration
The main configuration file is located at config/subscriptions.php after publishing.
-
Publishing (if skipped during install):
php artisan vendor:publish --tag=subscriptions-config
-
Options:
tables: Customize the names of the database tables used by the package. Defaults are usually fine.models: Define which Eloquent models the package should use. This allows you to override the default models with your own extended versions (see Extending Models section).
Database Migrations
The subscriptions:install command handles publishing and running migrations. The following tables are created (using default names):
plans: Stores subscription plans details.limitations: Defines types of features/limits (e.g., 'users', 'projects').features: Links Plans to Limitations and sets the value/quota.subscriptions: Links subscribable models (e.g., Users) to Plans and tracks lifecycle.subscription_usage: Tracks consumption of resettable features.
Core Concepts & Models
1. Subscribable Model
Your model that will have subscriptions (typically App\Models\User, but can be any model like App\Models\Company) needs to use the KaziSTM\Subscriptions\Traits\HasPlanSubscriptions trait.
<?php namespace App\Models; use Illuminate\Foundation\Auth\User as Authenticatable; use KaziSTM\Subscriptions\Traits\HasPlanSubscriptions; // Import the trait class User extends Authenticatable // or your base model (e.g., Model) { use HasPlanSubscriptions; // Use the trait // ... rest of your model }
2. Plan (KaziSTM\Subscriptions\Models\Plan)
Defines a subscription plan your users can subscribe to.
| Attribute | Type | Description |
|---|---|---|
| name | json | Translatable display name |
| slug | string | Unique identifier |
| description | json | Translatable description |
| is_active | boolean | If plan is usable |
| price | decimal | Cost per interval |
| signup_fee | decimal | One-time fee |
| currency | string | e.g., "USD" |
| trial_period | int | Duration of trial |
| trial_interval | string | Unit: day, month, year (from Interval Enum) |
| invoice_period | int | Duration of billing cycle |
| invoice_interval | string | Unit: day, month, year (from Interval Enum) |
| grace_period | int | Duration of grace period |
| grace_interval | string | Unit: day, month, year (from Interval Enum) |
| sort_order | int | Ordering |
| deleted_at | timestamp | Soft deletes |
Relationships
features()subscriptions()limitations()(HasManyThrough Features)
Scopes
scopeWhereHasLimitationSlug(Builder $query, string $limitationSlug)
Methods
hasLimitation(string $limitationSlug)isFree()hasTrial()hasGrace()getFeatureBySlug()activate()deactivate())
Note: The
IntervalEnum (KaziSTM\Subscriptions\Enums\Interval) typically uses values likeInterval::DAY->value('day'),Interval::MONTH->value('month'),Interval::YEAR->value('year') when interacting with these fields programmatically, although the database stores the string values.
3. Limitation (KaziSTM\Subscriptions\Models\Limitation)
Defines a category or type of feature/limit (the "what").
Attributes
| Attribute | Type | Description |
|---|---|---|
| title | json | Translatable display title (e.g., "User Seats") |
| slug | string | Unique identifier (e.g., "users", "projects") |
| description | json | Translatable description |
| type | string | Custom type field (optional) |
| sort_order | int | Ordering |
| deleted_at | timestamp | Soft deletes |
Relationships
features()
4. Feature (KaziSTM\Subscriptions\Models\Feature)
Links a Plan to a Limitation, defining the specific value/quota (the "how much").
Attributes
| Attribute | Type | Description |
|---|---|---|
| plan_id | foreignId | Belongs to Plan |
| limitation_id | foreignId | Belongs to Limitation |
| value | string | Limit/value (e.g., "10", "500", "true"). Use strings for consistency. |
| resettable_period | int | How often usage resets (e.g., 1 for 1 month) |
| resettable_interval | string | Unit for reset (day, month, year). Use 0/null if usage never resets. |
| sort_order | int | Ordering |
| deleted_at | timestamp | Soft deletes |
Relationships
plan()limitation()usages()
Methods
getResetDate()
5. Subscription (KaziSTM\Subscriptions\Models\Subscription)
Links a subscribable model to a Plan and tracks its lifecycle.
Attributes
| Attribute | Type | Description |
|---|---|---|
| subscriber_id | int | ID of the subscribing model |
| subscriber_type | string | Class name of the subscribing model |
| plan_id | foreignId | The Plan being subscribed to |
| name | json | Translatable identifier (e.g., "main", "default") |
| slug | string | Unique slug for this subscription instance |
| trial_ends_at | datetime | When the trial period ends |
| starts_at | datetime | When the current billing period started |
| ends_at | datetime | When the current billing period ends |
| cancels_at | datetime | If scheduled, when cancellation takes effect |
| canceled_at | datetime | When the cancellation was requested/marked |
| deleted_at | timestamp | Soft deletes |
Relationships
subscriber()(MorphTo)plan()usage()
Methods
active()inactive()onTrial()canceled()ended()cancel()changePlan()renew()recordFeatureUsage()reduceFeatureUsage()canUseFeature()getFeatureUsage()getFeatureRemainings()getFeatureValue()
Scopes
ofSubscriber()findEndingTrial()findEndedTrial()findEndingPeriod()findEndedPeriod()findActive()
6. Subscription Usage (KaziSTM\Subscriptions\Models\SubscriptionUsage)
Tracks consumption of specific, resettable features.
Attributes
| Attribute | Type | Description |
|---|---|---|
subscription_id |
foreignId |
The subscription instance |
feature_id |
foreignId |
The specific feature being tracked |
used |
int |
How much has been used in the current period |
valid_until |
datetime |
When the current usage period ends and resets |
deleted_at |
timestamp |
Soft deletes |
Relationships
subscription()feature()
Scopes
scopeByFeatureSlug()
Methods
expired()
Note on Slugs vs Names/Titles
Throughout the package, slug attributes (on Plan, Limitation, and Subscription) serve as unique, immutable identifiers intended for programmatic use. These are what you'll use in your code when:
- Fetching specific records
- Checking features
- Applying middleware
For example:
$subscription->planSubscription('main'); $subscription->canUseFeature('api-calls');
Usage
Preparation
1. Add Trait
Ensure your subscribable model(s) (e.g., App\Models\User, App\Models\Company) use the KaziSTM\Subscriptions\Traits\HasPlanSubscriptions trait.
use KaziSTM\Subscriptions\Traits\HasPlanSubscriptions; class User extends Authenticatable { use HasPlanSubscriptions; }
2. Create Plans, Limitations & Features
Define plans, limitations, and features in your database, typically using seeders.
Example Seeder Logic:
use KaziSTM\Subscriptions\Models\Plan; use KaziSTM\Subscriptions\Models\Limitation; // Create Limitations (the "what") $usersLimit = Limitation::firstOrCreate(['slug' => 'users'], ['title' => ['en' => 'User Seats']]); $projectsLimit = Limitation::firstOrCreate(['slug' => 'projects'], ['title' => ['en' => 'Projects']]); $apiLimit = Limitation::firstOrCreate(['slug' => 'api-calls'], ['title' => ['en' => 'API Calls per Month']]); $supportLimit = Limitation::firstOrCreate(['slug' => 'priority-support'], ['title' => ['en' => 'Priority Support']]); // Create Basic Plan $basicPlan = Plan::firstOrCreate(['slug' => 'basic'], [ 'name' => ['en' => 'Basic Plan'], 'price' => 9.99, 'invoice_interval' => 'month', 'invoice_period' => 1, 'currency' => 'USD' ]); // Assign Feature values (the "how much") $basicPlan->features()->updateOrCreate(['limitation_id' => $usersLimit->id], ['value' => '5']); $basicPlan->features()->updateOrCreate(['limitation_id' => $projectsLimit->id], ['value' => '10']); $basicPlan->features()->updateOrCreate(['limitation_id' => $apiLimit->id], ['value' => '1000', 'resettable_interval' => 'month', 'resettable_period' => 1]); $basicPlan->features()->updateOrCreate(['limitation_id' => $supportLimit->id], ['value' => 'false']); // No priority support // Create Pro Plan $proPlan = Plan::firstOrCreate(['slug' => 'pro'], [ 'name' => ['en' => 'Pro Plan'], 'price' => 29.99, 'invoice_interval' => 'month', 'invoice_period' => 1, 'currency' => 'USD', 'trial_period' => 14, 'trial_interval' => 'day' ]); // Assign Feature values $proPlan->features()->updateOrCreate(['limitation_id' => $usersLimit->id], ['value' => '25']); $proPlan->features()->updateOrCreate(['limitation_id' => $projectsLimit->id], ['value' => '50']); $proPlan->features()->updateOrCreate(['limitation_id' => $apiLimit->id], ['value' => '10000', 'resettable_interval' => 'month', 'resettable_period' => 1]); $proPlan->features()->updateOrCreate(['limitation_id' => $supportLimit->id], ['value' => 'true']); // Has priority support
Managing Subscriptions
use KaziSTM\Subscriptions\Models\Plan; use App\Models\User; // Your subscribable model $user = User::find(1); $proPlan = Plan::where('slug', 'pro')->firstOrFail(); $premiumPlan = Plan::where('slug', 'premium')->first(); // Assuming a premium plan exists // --- Create / Retrieve --- // Create a new subscription named 'main'. The name helps if a model can have multiple subscriptions. $subscription = $user->newPlanSubscription('main', $proPlan); // Retrieve the 'main' subscription later $subscription = $user->planSubscription('main'); // --- Check Status --- if ($subscription?->active()) { echo "Active!"; } // Not ended, could be on trial or paid period if ($subscription?->inactive()) { echo "Inactive!"; } // Ended or canceled if ($subscription?->onTrial()) { echo "On Trial until " . $subscription->trial_ends_at->toDateString(); } if ($subscription?->canceled()) { echo "Canceled."; } // Cancellation requested/processed if ($subscription?->ended()) { echo "Current period ended."; } // ends_at is in the past // --- Modify --- if ($subscription && $premiumPlan) { $subscription->changePlan($premiumPlan); // Change plan (handles proration based on config/logic) echo "Plan changed!"; } // Cancel subscription at the end of the current billing period $subscription?->cancel(); // Cancel immediately (ends the current period now) // $subscription?->cancel(true); // Renew subscription (e.g., after manual payment or admin action) // Resets usage for resettable features and sets a new billing cycle (starts_at, ends_at) // Throws LogicException if subscription was canceled AND has already ended try { $subscription?->renew(); echo "Renewed!"; } catch (\LogicException $e) { /* Handle error */ } // --- Querying --- $isSubscribedToPro = $user->subscribedTo($proPlan->id); // Check active subscription to specific plan ID $activePlans = $user->subscribedPlans(); // Get collection of Plan models user is actively subscribed to $allUserSubscriptions = $user->planSubscriptions; // Get collection of Subscription models for user $activeUserSubscriptions = $user->activePlanSubscriptions(); // Get collection of active Subscription models for user // --- Plan Queries --- // Find plans that offer a certain feature (by Limitation slug) $plansWithApi = Plan::whereHasLimitationSlug('api-calls')->get(); // Check if a specific plan instance offers a feature $proPlanHasSupport = $proPlan->hasLimitation('priority-support'); // true
Managing Feature Usage
Interact with features using the slug of the Limitation model.
// Assuming $user and $subscription are available $subscription = $user->planSubscription('main'); $usersSlug = 'users'; $apiCallsSlug = 'api-calls'; $supportSlug = 'priority-support'; // --- Checking Ability --- // canUseFeature checks boolean features AND depleting quotas if ($subscription?->canUseFeature($usersSlug)) { echo "User limit not reached."; // ... perform action like creating a user ... // Then record usage: $subscription->recordFeatureUsage($usersSlug); // Increment by 1 } else { echo "User limit reached!"; } if ($subscription?->canUseFeature($supportSlug)) { echo "Priority support is enabled for this plan."; } // --- Recording Usage --- // Increment usage (default) $usage = $subscription?->recordFeatureUsage($apiCallsSlug, 15); // Record 15 calls if ($usage) echo "Recorded usage for {$apiCallsSlug}. New usage: {$usage->used}"; // Set usage to a specific value (non-incremental) $usage = $subscription?->recordFeatureUsage('storage-gb', 15.5, false); // --- Reducing Usage --- // Useful when a resource tied to a quota is removed $usage = $subscription?->reduceFeatureUsage($usersSlug, 1); // Decrement by 1 if ($usage) echo "Reduced usage for {$usersSlug}. New usage: {$usage->used}"; // --- Retrieving Usage Data --- $currentApiUsage = $subscription?->getFeatureUsage($apiCallsSlug); $remainingApiCalls = $subscription?->getFeatureRemainings($apiCallsSlug); $apiLimitOnPlan = $subscription?->getFeatureValue($apiCallsSlug); // Gets the value from the features table echo "API Calls: Used={$currentApiUsage}, Limit={$apiLimitOnPlan}, Remaining={$remainingApiCalls}"; // --- Resets --- // If 'api-calls' feature has resettable_interval='month' and resettable_period=1, // when recordFeatureUsage() is called after the 'valid_until' date on the usage record, // the 'used' count automatically resets to 0, and 'valid_until' is updated for the next period.
Middleware
Protect your application routes based on subscription status or included features. The middleware automatically attempts to detect the subscribable entity from Route Model Binding, Filament Tenancy, or the Authenticated User.
Registration (Laravel 11+)
Register the middleware aliases in your bootstrap/app.php file:
// bootstrap/app.php use Illuminate\Foundation\Configuration\Middleware; return Application::configure(...) ->withRouting(...) ->withMiddleware(function (Middleware $middleware) { $middleware->alias([ 'subscription.active' => \KaziSTM\Subscriptions\Http\Middleware\CheckSubscription::class, 'subscription.feature' => \KaziSTM\Subscriptions\Http\Middleware\CheckPlanFeatures::class, // Add other aliases here ]); // Optionally add globally to groups if needed // $middleware->web(append: [ ... ]); }) ->withExceptions(...) ->create();
Registration (Pre-Laravel 11)
Register the middleware aliases in your app/Http/Kernel.php:
// app/Http/Kernel.php protected $middlewareAliases = [ // Or $routeMiddleware in older versions // ... other aliases 'subscription.active' => \KaziSTM\Subscriptions\Http\Middleware\CheckSubscription::class, 'subscription.feature' => \KaziSTM\Subscriptions\Http\Middleware\CheckPlanFeatures::class, ];
Usage in Routes
Check Active Subscription (subscription.active)
Verifies the relevant subscribable entity has an active subscription (not ended or canceled). Aborts with 403 (Forbidden) otherwise.
// routes/web.php or routes/api.php // Checks Auth::user()'s 'main' subscription Route::get('/dashboard', /* ... */)->middleware(['auth', 'subscription.active']); // Checks 'main' subscription for the {company} route model (if Company uses the trait) Route::get('/company/{company}/settings', /* ... */)->middleware(['auth', 'subscription.active']); // Checks a specific subscription named 'pro_tools' (applies to the found entity) Route::get('/pro-feature', /* ... */)->middleware(['auth', 'subscription.active:pro_tools']);
Check Plan Features (subscription.feature)
Verifies the relevant subscribable entity's active subscription plan includes all specified features (by Limitation slug). Aborts with 403 (Forbidden) otherwise. Implicitly checks for an active subscription first.
// routes/web.php or routes/api.php // Requires the plan to have the 'users' limitation defined Route::get('/users/invite', /* ... */)->middleware(['auth', 'subscription.feature:users']); // Requires both 'reports' and 'exports' limitations Route::get('/reports/export', /* ... */)->middleware(['auth', 'subscription.feature:reports,exports']); // Check a specific subscription ('pro_tools') for a specific feature ('advanced-analytics') Route::get('/analytics/deep-dive', /* ... */) ->middleware(['auth', 'subscription.feature:advanced-analytics,pro_tools']); // Feature slug first, optional subscription slug second
Subscribable Entity Detection
The middleware automatically attempts to determine the subscribable entity to check in the following order of precedence:
-
Route Model Binding
Looks for a route parameter that resolves to a model using theHasPlanSubscriptionstrait. -
Filament Tenancy
Iffilament/filamentis installed, checks the current tenant model. -
Authenticated User
Falls back toAuth::user()as a default.
Extending Models
If you need custom logic or relationships on the package's models:
- Run
php artisan subscriptions:install. - Answer "yes" when asked to Publish model stubs...?
- Edit the generated files in
app/Models/(e.g.,app/Models/Plan.php). They extend the base package models using aliases.
<?php namespace App\Models; use KaziSTM\Subscriptions\Models\Plan as BasePlan; // Alias is used class Plan extends BasePlan { // Your custom logic here... }
- Update
config/subscriptions.phpto use your models:
// config/subscriptions.php 'models' => [ 'plan' => \App\Models\Plan::class, 'limitation' => \App\Models\Limitation::class, 'feature' => \App\Models\Feature::class, 'subscription' => \App\Models\Subscription::class, 'subscription_usage' => \App\Models\SubscriptionUsage::class, ],
Support & Issues
If you discover any security-related issues, please email [email address removed] instead of using the issue tracker.
All other issues, feature requests, or questions should be submitted via the GitHub Issues page.
License
This package is open-sourced software licensed under the MIT license.
Credits
- Forked from and inspired by LaravelCM Subscriptions
- Inspired by patterns in Spatie open-source packages.
- Author: Nezrek Youcef
kazistm/subscriptions 适用场景与选型建议
kazistm/subscriptions 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 1 次下载、GitHub Stars 达 2, 最近一次更新时间为 2025 年 05 月 03 日, 在 PHP 生态内属于活跃度较高的组件。
它主要适用于以下技术方向: 「database」 「subscription」 「feature」 「laravel」 「saas」 「billing」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。
我们在过去多个企业项目中使用过 kazistm/subscriptions 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。
基于 kazistm/subscriptions 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。
线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。
承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。
与 kazistm/subscriptions 相关的其它包
同方向 / 同关键字的高下载量 PHP Composer 包推荐,方便对比选型:
Dibi is Database Abstraction Library for PHP
A block that displays featured content - large image, title, description and link.
Store your language lines in the database, yaml or other sources
A package for automatically encrypting and decrypting Eloquent attributes in Laravel 5.5+, based on configuration settings.
MaxAl Subscriptions is a flexible plans and subscription management system for Laravel, with the required tools to run your SAAS like services efficiently. It's simple architecture, accompanied by powerful underlying to afford solid platform for your business.
A PSR-7 compatible library for making CRUD API endpoints
统计信息
- 总下载量: 1
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 2
- 点击次数: 22
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2025-05-03