承接 crumbls/subscriptions 相关项目开发

从需求分析到上线部署,全程专人跟进,保证项目质量与交付效率

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

crumbls/subscriptions

Composer 安装命令:

composer require crumbls/subscriptions

包简介

A flexible plans and subscription management system for Laravel. Manage your SaaS plans, features, and subscriber usage with ease.

README 文档

README

A flexible plans and subscription management system for Laravel. Manage SaaS plans, features, and subscriber usage tracking without coupling to any payment provider.

Inspired by rinvex/laravel-subscriptions, which is now abandoned. This package modernizes the codebase for Laravel 11/12/13, PHP 8.3+, and current Laravel conventions.

Features

  • Plan management with trial, grace, and invoice periods
  • Standalone features with many-to-many plan relationships
  • Per-plan feature values (e.g. Basic gets 5 users, Pro gets 50)
  • Feature-based usage tracking with automatic resets
  • Polymorphic subscriptions — attach to any model
  • Grace period support — subscriptions stay active during grace window
  • Lifecycle events — hook into created, canceled, renewed, plan changed
  • Translatable plan/feature names and descriptions (via Spatie)
  • Sortable plans and features (via Spatie)
  • Configurable table names and swappable models
  • Route middleware for feature gating
  • Artisan command to prune expired subscriptions

Requirements

Package PHP Laravel
2.x 8.3, 8.4 11.x, 12.x, 13.x

All combinations in the matrix above are exercised in CI on every push and pull request.

Installation

composer require crumbls/subscriptions

Publish config and migrations (optional):

php artisan vendor:publish --tag=subscriptions-config
php artisan vendor:publish --tag=subscriptions-migrations

Run migrations:

php artisan migrate

Migrations autoload by default. Set autoload_migrations to false in config/subscriptions.php to disable this.

Database Structure

plans                       features
├── id                      ├── id
├── slug                    ├── slug
├── name (json)             ├── name (json)
├── description (json)      ├── description (json)
├── price                   ├── resettable_period
├── signup_fee              ├── resettable_interval
├── currency                ├── sort_order
├── invoice_period          └── timestamps + soft deletes
├── invoice_interval
├── trial_period            plan_features (pivot)
├── trial_interval          ├── id
├── grace_period            ├── plan_id → plans
├── grace_interval          ├── feature_id → features
├── is_active               ├── value
├── active_subscribers_limit├── sort_order
├── sort_order              └── timestamps
└── timestamps + soft deletes

plan_subscriptions          plan_subscription_usage
├── id                      ├── id
├── subscriber_id           ├── subscription_id → plan_subscriptions
├── subscriber_type         ├── feature_id → features
├── plan_id → plans         ├── used
├── slug                    ├── valid_until
├── name (json)             └── timestamps + soft deletes
├── description (json)
├── trial_ends_at
├── starts_at
├── ends_at
├── cancels_at
├── canceled_at
└── timestamps + soft deletes

Features are standalone entities. The value (limit) is set per-plan on the plan_features pivot. This lets a single "Users" feature have different limits across plans.

Usage

Add subscriptions to a model

use Crumbls\Subscriptions\Traits\HasPlanSubscriptions;

class Tenant extends Model
{
    use HasPlanSubscriptions;
}

Works on any Eloquent model — User, Tenant, Team, Organization, etc.

Create a plan

use Crumbls\Subscriptions\Models\Plan;

$plan = Plan::create([
    'name' => 'Pro',
    'description' => 'Pro plan',
    'price' => 9.99,
    'signup_fee' => 1.99,
    'invoice_period' => 1,
    'invoice_interval' => 'month',
    'trial_period' => 15,
    'trial_interval' => 'day',
    'grace_period' => 7,
    'grace_interval' => 'day',
    'currency' => 'USD',
]);

Intervals accept: hour, day, week, month, year.

Create features

Features are standalone — create them once, attach to many plans:

use Crumbls\Subscriptions\Models\Feature;

$users = Feature::create([
    'name' => 'Users',
    'slug' => 'users',
    'resettable_period' => 0, // never resets — running count
]);

$apiCalls = Feature::create([
    'name' => 'API Requests',
    'slug' => 'api-requests',
    'resettable_period' => 1,
    'resettable_interval' => 'month', // resets monthly
]);

$ssl = Feature::create([
    'name' => 'SSL',
    'slug' => 'ssl',
]);

Attach features to plans with values

The value is set per-plan on the pivot — this is how different plans get different limits:

// Basic: 5 users, 100 API calls, no SSL
$basic->features()->attach($users, ['value' => '5']);
$basic->features()->attach($apiCalls, ['value' => '100']);

// Pro: 50 users, 10000 API calls, SSL enabled
$pro->features()->attach($users, ['value' => '50']);
$pro->features()->attach($apiCalls, ['value' => '10000']);
$pro->features()->attach($ssl, ['value' => 'true']);

// Enterprise: unlimited users, unlimited API calls, SSL enabled
$enterprise->features()->attach($users, ['value' => '999999']);
$enterprise->features()->attach($apiCalls, ['value' => '999999']);
$enterprise->features()->attach($ssl, ['value' => 'true']);

Factories

Every model ships with a factory for testing and seeding:

use Crumbls\Subscriptions\Models\Plan;
use Crumbls\Subscriptions\Models\Feature;
use Crumbls\Subscriptions\Models\PlanSubscription;

Plan::factory()->create();                        // a generic paid plan
Plan::factory()->free()->create();                // price = 0
Plan::factory()->paid()->withTrial(14)->create(); // 14-day trial
Plan::factory()->withGrace(7)->create();          // 7-day grace window
Plan::factory()->limitedTo(100)->create();        // capped at 100 subscribers
Plan::factory()->inactive()->create();            // is_active = false

Feature::factory()->resettableMonthly()->create();
Feature::factory()->resettableDaily()->create();

PlanSubscription::factory()
    ->for($user, 'subscriber')
    ->ended()
    ->canceled()
    ->create();

Subscribe

$tenant->subscribe('main', $plan);

// Or the longer-named original:
$tenant->newPlanSubscription('main', $plan);

// Both take an optional start date:
$tenant->subscribe('main', $plan, now()->addDay());

Subscription slugs are unique per subscriber, not globally, so both a User and a Tenant can hold a main subscription at the same time.

Check subscription status

$tenant->subscribedTo($plan->id);               // bool
$tenant->hasActiveSubscription();                // any active sub?
$subscription = $tenant->planSubscription('main');
$subscription = $tenant->currentSubscription();  // most recent active

$subscription->active();         // true if not ended, or on trial/grace
$subscription->onTrial();        // currently in trial period
$subscription->onGracePeriod();  // ended but within grace window
$subscription->canceled();       // has been canceled
$subscription->ended();          // period has expired
$subscription->inactive();       // opposite of active
$subscription->pendingCancellation(); // canceled but not yet ended
$subscription->daysUntilEnd();   // int or null
$subscription->daysUntilTrialEnd(); // int or null

Feature usage

$subscription->recordFeatureUsage('api-requests');
$subscription->recordFeatureUsage('api-requests', 5);           // add 5
$subscription->recordFeatureUsage('api-requests', 10, false);   // set to 10
$subscription->reduceFeatureUsage('api-requests', 3);
$subscription->canUseFeature('api-requests');      // bool — checks used < value
$subscription->getFeatureUsage('api-requests');     // int — current usage
$subscription->getFeatureRemainings('api-requests'); // int — remaining quota
$subscription->getFeatureValue('api-requests');     // raw value from pivot

If you call recordFeatureUsage with a slug that isn't attached to the subscription's plan, it throws Crumbls\Subscriptions\Exceptions\UnknownFeatureException — the featureSlug and plan are exposed as readonly properties on the exception.

Enforcing limits (example: user count)

// When adding a user to a tenant
$subscription = $tenant->currentSubscription();

if (!$subscription->canUseFeature('users')) {
    throw new \Exception('User limit reached for your plan.');
}

$subscription->recordFeatureUsage('users');

// When removing a user
$subscription->reduceFeatureUsage('users');

Plan lifecycle

$subscription->changePlan($newPlan);    // switch plans (resets usage if billing cycle changes)
$subscription->renew();                 // start a new period
$subscription->cancel();                // cancel at end of current period
$subscription->cancel(immediately: true); // cancel now
$subscription->reactivate();            // undo pending cancellation

Scopes

use Crumbls\Subscriptions\Models\PlanSubscription;

PlanSubscription::findActive()->get();
PlanSubscription::findEndingPeriod(7)->get();   // ending within 7 days
PlanSubscription::findEndedPeriod()->get();
PlanSubscription::findEndingTrial(3)->get();     // trial ending within 3 days
PlanSubscription::findEndedTrial()->get();
PlanSubscription::ofSubscriber($tenant)->get();
PlanSubscription::byPlanId($plan->id)->get();

Plan::active()->get();
Plan::inactive()->get();
Plan::free()->get();
Plan::paid()->get();

Events

Event Fired when
SubscriptionCreated A new subscription is created
SubscriptionCanceled A subscription is canceled (includes $immediate flag)
SubscriptionRenewed A subscription is renewed
SubscriptionPlanChanged A subscription switches plans (includes $oldPlan and $newPlan)
use Crumbls\Subscriptions\Events\SubscriptionCreated;

class SendWelcomeEmail
{
    public function handle(SubscriptionCreated $event): void
    {
        $event->subscription->subscriber->notify(/* ... */);
    }
}

These events are plain dispatchable events — they do not implement ShouldBroadcast out of the box. If you want a broadcast version, extend the event in your application and add implements ShouldBroadcast there.

Middleware

Gate routes by feature or subscription:

// Must have the "api-requests" feature available
Route::middleware('can-use-feature:api-requests')->group(/* ... */);

// Check a specific subscription by slug
Route::middleware('can-use-feature:api-requests,pro')->group(/* ... */);

// Must have any active subscription
Route::middleware('subscribed')->group(/* ... */);

// Must be subscribed to a specific plan slug
Route::middleware('subscribed:pro')->group(/* ... */);

Pruning expired subscriptions

php artisan subscriptions:prune              # soft-deletes subs ended more than 30 days ago
php artisan subscriptions:prune --days=90    # custom threshold
php artisan subscriptions:prune --force      # skip confirmation

Schedule it:

// routes/console.php or bootstrap/app.php
Schedule::command('subscriptions:prune --force')->daily();

Configuration

Publish the config to customize table names or swap model classes:

// config/subscriptions.php
return [
    'autoload_migrations' => true,

    'tables' => [
        'plans' => 'plans',
        'features' => 'features',
        'plan_features' => 'plan_features',
        'plan_subscriptions' => 'plan_subscriptions',
        'plan_subscription_usage' => 'plan_subscription_usage',
    ],

    'models' => [
        'plan' => \Crumbls\Subscriptions\Models\Plan::class,
        'feature' => \Crumbls\Subscriptions\Models\Feature::class,
        'plan_feature' => \Crumbls\Subscriptions\Models\PlanFeature::class,
        'plan_subscription' => \Crumbls\Subscriptions\Models\PlanSubscription::class,
        'plan_subscription_usage' => \Crumbls\Subscriptions\Models\PlanSubscriptionUsage::class,
    ],
];

Extending models

Every model is resolved through config. Extend the base model and update the config:

namespace App\Models;

use Crumbls\Subscriptions\Models\Plan as BasePlan;

class Plan extends BasePlan
{
    public function cancel(bool $immediately = false): static
    {
        // Cancel recurring payment with your provider
        $this->subscriber->paymentProvider()->cancelSubscription($this);

        return parent::cancel($immediately);
    }
}
// config/subscriptions.php
'models' => [
    'plan' => \App\Models\Plan::class,
],

All relationships, scopes, traits, middleware, and the prune command resolve models through config — your custom models are used everywhere automatically.

Models

Model Description
Plan A subscription plan with pricing, billing cycle, trial/grace periods
Feature A standalone feature (e.g. "Users", "API Calls", "SSL")
PlanFeature Pivot model linking features to plans with a value per plan
PlanSubscription A subscriber's subscription to a plan
PlanSubscriptionUsage Tracks how much of a feature a subscription has consumed

Considerations

  • Payments are out of scope. This package handles plan/subscription logic only. Integrate with Stripe, Paddle, etc. separately via events or model overrides.
  • Translatable fields: name and description on plans, features, and subscriptions are stored as JSON and support multiple locales via spatie/laravel-translatable.
  • Soft deletes: Plans, features, subscriptions, and usage all use soft deletes. The prune command only soft-deletes; use forceDelete() if you need permanent removal.
  • Features are standalone: Create a feature once, attach it to multiple plans with different values. This avoids duplicating feature definitions across plans.

Coming from rinvex/laravel-subscriptions

This package started as a modern reboot of rinvex/laravel-subscriptions, so the mental model is similar but a few things are worth knowing if you're porting an existing app:

  • Namespace: Crumbls\Subscriptions\... (was Rinvex\Subscriptions\...).
  • Config key: subscriptions (was rinvex.subscriptions).
  • Subscribing: prefer $user->subscribe('main', $plan)newPlanSubscription() also works. Rinvex's mix of newSubscription() / subscribe() is unified.
  • Features are standalone. Rinvex defines features inline on the plan; here a Feature is its own row, and the value (limit) lives on the plan_features pivot. That's how you get "Basic: 5 users, Pro: 50 users" from a single users feature.
  • No rinvex:* artisan commands. Use plain php artisan migrate and php artisan vendor:publish.
  • No model-level validation. Use Form Requests in your own app.
  • Events. Four Laravel events (SubscriptionCreated, SubscriptionCanceled, SubscriptionRenewed, SubscriptionPlanChanged) in place of rinvex's trait-based hooks.

Upgrading

See UPGRADING.md for version-to-version migration steps. Breaking changes are called out in CHANGELOG.md.

Contributing

See CONTRIBUTING.md.

License

MIT

crumbls/subscriptions 适用场景与选型建议

crumbls/subscriptions 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 189 次下载、GitHub Stars 达 3, 最近一次更新时间为 2026 年 02 月 21 日, 在 PHP 生态内属于活跃度较高的组件。

它主要适用于以下技术方向: 「subscription」 「feature」 「laravel」 「saas」 「plan」 「recurring」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。

我们在过去多个企业项目中使用过 crumbls/subscriptions 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 crumbls/subscriptions 我们能提供哪些服务?
定制开发 / 二次开发

基于 crumbls/subscriptions 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

与 crumbls/subscriptions 相关的其它包

同方向 / 同关键字的高下载量 PHP Composer 包推荐,方便对比选型:

统计信息

  • 总下载量: 189
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 3
  • 点击次数: 28
  • 依赖项目数: 1
  • 推荐数: 0

GitHub 信息

  • Stars: 3
  • Watchers: 0
  • Forks: 1
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-02-21