err0r/larasub

Laravel Subscription Package

Fund package maintenance!
err0r

v3.0.0 2025-06-04 09:38 UTC

README

Latest Version on Packagist Total Downloads

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

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

  1. Pending: Created but not yet active (start_at is null)
  2. Active: Currently running subscription
  3. Cancelled: Marked for cancellation (can be immediate or at period end)
  4. Expired: Past the end date
  5. 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