err0r / larasub
Laravel Subscription Package
Fund package maintenance!
err0r
Requires
- php: ^8.2
- illuminate/contracts: ^10.0||^11.0|^12.0
- spatie/laravel-package-tools: ^1.16
- spatie/laravel-translatable: ^6.8
Requires (Dev)
- larastan/larastan: ^2.9
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.1.1||^7.10.0
- orchestra/testbench: ^9.0.0||^8.22.0
- pestphp/pest: ^2.34
- pestphp/pest-plugin-arch: ^2.7
- pestphp/pest-plugin-laravel: ^2.3
- phpstan/extension-installer: ^1.3
- phpstan/phpstan-deprecation-rules: ^1.1
- phpstan/phpstan-phpunit: ^1.3
- spatie/laravel-ray: ^1.35
README
A powerful and flexible subscription management system for Laravel applications with comprehensive plan versioning support.
✨ Features
Core Subscription Management
- 📦 Multi-tiered subscription plans with versioning
- 🔄 Flexible billing periods (minute/hour/day/week/month/year)
- 💳 Subscribe users with custom dates and pending status
- 🔄 Cancel, resume, and renew subscriptions
- 📈 Comprehensive subscription lifecycle tracking
Advanced Feature System
- 🎯 Feature-based access control (consumable & non-consumable)
- 📊 Usage tracking with configurable limits
- ⏰ Period-based feature resets
- 🔋 Unlimited usage support
- 🔍 Feature usage monitoring and quotas
Plan Versioning & Management
- 📋 Plan versioning for seamless updates
- 🔄 Backward compatibility for existing subscribers
- 📅 Historical pricing and feature tracking
- 🚀 Easy rollback capabilities
- 📊 Version-specific analytics
Developer Experience
- 🧩 Simple trait-based integration
- ⚙️ Configurable tables and models
- 📝 Comprehensive event system
- 🔌 UUID support out of the box
- 🌐 Multi-language support (translatable plans/features)
- 🛠️ Rich builder pattern APIs
Table of Contents
- Installation
- Quick Start
- Migration from v2.x to v3.x
- Core Concepts
- Subscription Management
- Feature Management
- Plan Versioning
- Events & Lifecycle
- API Resources
- Configuration
- Commands
- Testing
- Contributing
Installation
Install via Composer:
composer require err0r/larasub
Publish configuration:
php artisan vendor:publish --tag="larasub-config"
Run migrations:
# Publish all migrations php artisan vendor:publish --tag="larasub-migrations" php artisan migrate
Quick Start
1. Setup Your User Model
<?php namespace App\Models; use Err0r\Larasub\Traits\HasSubscription; use Illuminate\Foundation\Auth\User as Authenticatable; class User extends Authenticatable { use HasSubscription; // Your existing model code... }
2. Create Features
<?php use Err0r\Larasub\Builders\FeatureBuilder; // Consumable feature (trackable usage) $apiCalls = FeatureBuilder::create('api-calls') ->name(['en' => 'API Calls', 'ar' => 'مكالمات API']) ->description(['en' => 'Number of API calls allowed']) ->consumable() ->sortOrder(1) ->build(); // Non-consumable feature (boolean access) $prioritySupport = FeatureBuilder::create('priority-support') ->name(['en' => 'Priority Support']) ->description(['en' => 'Access to priority customer support']) ->nonConsumable() ->sortOrder(2) ->build();
3. Create Plans with Versioning
<?php use Err0r\Larasub\Builders\PlanBuilder; use Err0r\Larasub\Enums\Period; use Err0r\Larasub\Enums\FeatureValue; // Create initial plan version $premiumPlan = PlanBuilder::create('premium') ->name(['en' => 'Premium Plan', 'ar' => 'خطة مميزة']) ->description(['en' => 'Access to premium features']) ->sortOrder(2) ->versionLabel('1.0.0') ->price(99.99, 'USD') ->resetPeriod(1, Period::MONTH) ->published() ->addFeature('api-calls', fn ($feature) => $feature ->value(1000) ->resetPeriod(1, Period::DAY) ->displayValue(['en' => '1000 API Calls']) ->sortOrder(1) ) ->addFeature('priority-support', fn ($feature) => $feature ->value(FeatureValue::UNLIMITED) ->displayValue(['en' => 'Priority Support Included']) ->sortOrder(2) ) ->build();
4. Subscribe Users
<?php // Get plan (automatically uses latest published version) $plan = Plan::slug('premium')->first(); // Subscribe user $subscription = $user->subscribe($plan); // Subscribe with custom dates $subscription = $user->subscribe($plan, startAt: now(), endAt: now()->addYear() ); // Create pending subscription (useful for payment processing) $subscription = $user->subscribe($plan, pending: true);
5. Check Features & Usage
<?php // Check feature access if ($user->hasFeature('priority-support')) { // User has access to priority support } // Check consumable feature usage if ($user->canUseFeature('api-calls', 5)) { // User can make 5 API calls $user->useFeature('api-calls', 5); } // Get remaining usage $remaining = $user->remainingFeatureUsage('api-calls');
Migration from v2.x to v3.x (Plan Versioning)
If upgrading from v2.x, follow these steps:
1. Backup Your Database
# MySQL example mysqldump -u username -p database_name > backup.sql
2. Update Package & Run Migration
composer update err0r/larasub # See a summary of required changes without affecting the database php artisan larasub:migrate-to-versioning --dry-run php artisan vendor:publish --tag="larasub-migrations-upgrade-plan-versioning" php artisan migrate
3. Update Your Code
Before (v2.x):
// Accessing plan properties directly $price = $subscription->plan->price; $features = $subscription->plan->features;
After (v3.x):
// Access through plan version $price = $subscription->planVersion->price; $features = $subscription->planVersion->features;
See Changelog
Core Concepts
Plans vs Plan Versions
- Plan: A subscription template (e.g., "Premium Plan")
- Plan Version: A specific iteration with pricing and features (e.g., "Premium Plan v2.0")
- Subscriptions: Always reference a specific plan version
- Versioning Benefits: Update plans without affecting existing subscribers
Feature Types
- Consumable: Trackable usage with limits (e.g., API calls, storage)
- Non-Consumable: Boolean access features (e.g., priority support, advanced tools)
Subscription Lifecycle
- Pending: Created but not yet active (
start_at
is null) - Active: Currently running subscription
- Cancelled: Marked for cancellation (can be immediate or at period end)
- Expired: Past the end date
- Future: Scheduled to start in the future
Subscription Management
Creating Subscriptions
<?php // Basic subscription $subscription = $user->subscribe($plan); // Advanced options $subscription = $user->subscribe($plan, startAt: now()->addWeek(), // Future start endAt: now()->addYear(), // Custom end date pending: false // Active immediately ); // Pending subscription (for payment processing) $pendingSubscription = $user->subscribe($plan, pending: true);
Subscription Status
<?php $subscription = $user->subscriptions()->first(); // Status checks $subscription->isActive(); // Currently active $subscription->isPending(); // Awaiting activation $subscription->isCancelled(); // Marked for cancellation $subscription->isExpired(); // Past end date $subscription->isFuture(); // Scheduled to start // Status transitions (useful for event handling) $subscription->wasJustActivated(); $subscription->wasJustCancelled(); $subscription->wasJustResumed(); $subscription->wasJustRenewed();
Subscription Operations
<?php // Cancel subscription $subscription->cancel(); // Cancel at period end $subscription->cancel(immediately: true); // Cancel immediately // Resume cancelled subscription $subscription->resume(); $subscription->resume(startAt: now(), endAt: now()->addMonth()); // Renew subscription $newSubscription = $subscription->renew(); // From end date $newSubscription = $subscription->renew(startAt: now()); // From specific date
Querying Subscriptions
<?php // By status $user->subscriptions()->active()->get(); $user->subscriptions()->pending()->get(); $user->subscriptions()->cancelled()->get(); $user->subscriptions()->expired()->get(); // By plan $user->subscriptions()->wherePlan($plan)->get(); $user->subscriptions()->wherePlan('premium')->get(); // Using slug // By renewal status $user->subscriptions()->renewed()->get(); // Previously renewed $user->subscriptions()->notRenewed()->get(); // Not yet renewed $user->subscriptions()->dueForRenewal()->get(); // Due in 7 days $user->subscriptions()->dueForRenewal(30)->get(); // Due in 30 days
Feature Management
Checking Feature Access
<?php // Basic feature check $user->hasFeature('priority-support'); // Has the feature $user->hasActiveFeature('priority-support'); // Has active subscription with feature // Consumable feature checks $user->canUseFeature('api-calls', 10); // Can use 10 units $user->remainingFeatureUsage('api-calls'); // Remaining usage count // Next available usage (for reset periods) $nextReset = $user->nextAvailableFeatureUsage('api-calls'); // Returns Carbon instance, null (unlimited), or false (no reset)
Tracking Feature Usage
<?php // Record usage $user->useFeature('api-calls', 5); // Get usage statistics $totalUsage = $user->featureUsage('api-calls'); $usageBySubscription = $user->featuresUsage(); // All features // Through specific subscription $subscription->useFeature('api-calls', 3); $subscription->featureUsage('api-calls'); $subscription->remainingFeatureUsage('api-calls');
Feature Configuration
<?php // Get plan feature details $planFeature = $subscription->planFeature('api-calls'); echo $planFeature->value; // Usage limit echo $planFeature->reset_period; // Reset frequency echo $planFeature->reset_period_type; // Reset period type echo $planFeature->display_value; // Human-readable value echo $planFeature->is_hidden; // Whether feature is hidden from users
Feature Visibility
Control which features are displayed to end users while keeping them functional for internal logic:
<?php // Creating hidden features $plan = PlanBuilder::create('premium') ->addFeature('api-calls', fn ($feature) => $feature ->value(1000) ->displayValue('1,000 API calls') // Feature is visible to users by default ) ->addFeature('internal-tracking', fn ($feature) => $feature ->value('enabled') ->displayValue('Internal tracking') ->hidden() // Hide this feature from user interfaces ) ->addFeature('admin-feature', fn ($feature) => $feature ->value('enabled') ->hidden(true) // Explicitly hide ) ->addFeature('visible-feature', fn ($feature) => $feature ->value('enabled') ->visible() // Explicitly make visible (default behavior) ) ->build(); // Query visible/hidden features $visibleFeatures = $planVersion->visibleFeatures; // Only visible features $allFeatures = $planVersion->features; // All features (visible + hidden) // Using scopes $visible = PlanFeature::visible()->get(); // All visible plan features $hidden = PlanFeature::hidden()->get(); // All hidden plan features // Check visibility $feature = $planVersion->features->first(); $feature->isVisible(); // true/false $feature->isHidden(); // true/false
API Behavior:
- Hidden features remain fully functional for subscription logic and usage tracking
- Only the display/visibility to end users is affected
Feature Relationships
<?php use Err0r\Larasub\Models\Feature; // Get a feature instance $feature = Feature::slug('api-calls')->first(); // All plan-feature pivot rows for this feature $planFeatures = $feature->planFeatures; // All plan versions that include this feature $planVersions = $feature->planVersions; // All raw subscription feature usage rows $usages = $feature->subscriptionFeatureUsages; // All subscriptions that have used this feature $subscriptions = $feature->subscriptions;
Plan Versioning
Creating Plan Versions
<?php // Create new version of existing plan $newVersion = PlanBuilder::create('premium') // Same slug ->versionLabel('2.0.0') // Display label ->price(129.99, 'USD') // Updated price ->resetPeriod(1, Period::MONTH) ->published() ->addFeature('api-calls', fn ($feature) => $feature ->value(2000) // Increased limit ->resetPeriod(1, Period::DAY) ) ->build(); // Specify exact version number $specificVersion = PlanBuilder::create('premium') ->versionNumber(5) // Explicit version ->versionLabel('5.0.0-beta') ->price(199.99, 'USD') ->build();
Working with Versions
<?php $plan = Plan::slug('premium')->first(); // Get versions $versions = $plan->versions; // All versions $currentVersion = $plan->currentVersion(); // Latest published & active $latestVersion = $plan->versions()->latest()->first(); // Latest by number // Version properties $version = $plan->versions->first(); $version->version_number; // e.g., 2 $version->version_label; // e.g., "2.0.0" $version->getDisplayVersion(); // Returns label or "v{number}" $version->isPublished(); $version->isActive(); $version->isFree(); // Version operations $version->publish(); $version->unpublish();
Subscription Versioning
<?php // Subscribe to specific version (optional) $user->subscribe($plan); // Uses current published version $user->subscribe($planVersion); // Uses specific version // Access version data $subscription->planVersion->price; // Version-specific price $subscription->planVersion->features; // Version-specific features $subscription->planVersion->version_number; // 2 $subscription->planVersion->getDisplayVersion(); // "2.0.0" or "v2"
Events & Lifecycle
The package dispatches events for subscription lifecycle management:
Available Events
<?php use Err0r\Larasub\Events\SubscriptionEnded; use Err0r\Larasub\Events\SubscriptionEndingSoon; // Triggered when subscription expires SubscriptionEnded::class // Triggered when subscription is ending soon (configurable, default: 7 days) SubscriptionEndingSoon::class
Event Listener Example
<?php namespace App\Listeners; use Err0r\Larasub\Events\SubscriptionEnded; use Illuminate\Contracts\Queue\ShouldQueue; class HandleEndedSubscription implements ShouldQueue { public function handle(SubscriptionEnded $event): void { $subscription = $event->subscription; $user = $subscription->subscriber; // Send notification, downgrade access, etc. $user->notify(new SubscriptionExpiredNotification($subscription)); } }
Automatic Event Checking
The package includes an automated scheduler that checks and triggers subscription events every minute. You can enable and configure this scheduler in your config/larasub.php
file. The scheduler is disabled by default.
API Resources
Transform your models into JSON responses using the provided resource classes:
<?php use Err0r\Larasub\Resources\{ FeatureResource, PlanResource, PlanVersionResource, PlanFeatureResource, SubscriptionResource, SubscriptionFeatureUsageResource }; // Transform feature return FeatureResource::make($feature); // Transform plan with versions return PlanResource::make($plan); // Transform plan version with features return PlanVersionResource::make($planVersion); // Transform subscription with plan version return SubscriptionResource::make($subscription); // Transform feature usage return SubscriptionFeatureUsageResource::make($usage);
Configuration
Publish and customize the configuration file:
php artisan vendor:publish --tag="larasub-config"
Key configuration options:
<?php return [ // Database table names 'tables' => [ 'plans' => 'plans', 'plan_versions' => 'plan_versions', 'features' => 'features', 'subscriptions' => 'subscriptions', // ... ], // Event scheduling 'schedule' => [ 'check_ending_subscriptions' => '* * * * *', // Every minute ], // Notification settings 'subscription_ending_soon_days' => 7, // Model configurations 'models' => [ 'plan' => \Err0r\Larasub\Models\Plan::class, 'subscription' => \Err0r\Larasub\Models\Subscription::class, // ... ], ];
Commands
The package provides several Artisan commands:
Migration Command
# Migrate from v2.x to v3.x with plan versioning php artisan larasub:migrate-to-versioning # Dry run to preview changes php artisan larasub:migrate-to-versioning --dry-run # Force without confirmation php artisan larasub:migrate-to-versioning --force
Subscription Monitoring
# Check for ending subscriptions (usually run via scheduler)
php artisan larasub:check-ending-subscriptions
Development Tools
# Seed sample data for development
php artisan larasub:seed
Testing
Run the test suite:
composer test # With coverage composer test-coverage # Code analysis composer analyse # Code formatting composer format