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
Important
This package is currently under development and is not yet ready for production use.
Click the Watch button to stay updated and be notified when the package is ready for deployment!
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