lacodix/laravel-plans
Laravel package to manage SaaS plans, addons, subscriptions, and optional features. Supports countable/uncountable features with limits, resets, and consumption across plans, plus translations, ordering, and metadata—billing/invoicing not included.
Installation:
composer require lacodix/laravel-plans
php artisan vendor:publish --provider="Lacodix\LaravelPlans\LaravelPlansServiceProvider"
Publish the config and migrations, then run:
php artisan migrate
Add Trait to User Model:
use Lacodix\LaravelPlans\Models\Traits\HasSubscriptions;
class User extends Authenticatable {
use HasSubscriptions;
}
Create a Basic Plan:
use Lacodix\LaravelPlans\Enums\Interval;
use Lacodix\LaravelPlans\Models\Plan;
Plan::create([
'slug' => 'basic-plan',
'name' => 'Basic Plan',
'price' => 9.99,
'active' => true,
'billing_interval' => Interval::MONTH,
'billing_period' => 1,
]);
Subscribe a User:
$user = User::first();
$user->subscribe(Plan::where('slug', 'basic-plan')->first());
// Create a feature
$feature = Feature::create([
'slug' => 'api-access',
'name' => ['en' => 'API Access'],
]);
// Attach to a plan
$plan->features()->attach($feature, ['value' => 100]);
// Check if user has access
if ($user->hasFeature('api-access', 1)) {
// Grant access
}
Plan Creation with Features:
$plan = Plan::create([
'slug' => 'premium',
'name' => ['en' => 'Premium Plan'],
'price' => 29.99,
'billing_interval' => Interval::YEAR,
'billing_period' => 1,
]);
$feature = Feature::create([
'slug' => 'storage',
'name' => ['en' => 'Storage'],
'countable' => true,
]);
$plan->features()->attach($feature, [
'value' => 1024, // GB
'resettable_period' => 1,
'resettable_interval' => Interval::MONTH,
]);
Localized Plan Names:
$plan->name = ['en' => 'Premium', 'de' => 'Premium Plan'];
$plan->save();
Multi-Subscription with Slugs:
$user->subscribe($plan1, 'main'); // Primary subscription
$user->subscribe($addonPlan, 'addon'); // Secondary subscription
Renewal and Cancellation:
$subscription = $user->subscriptions()->first();
$subscription->renew(); // Auto-calculates next period
$subscription->renew(force: true); // Force renewal from current end date
$subscription->cancel(); // Soft cancellation (configurable)
Feature Consumption:
// Consume 50 units of 'storage' feature
$user->consumeFeature('storage', 50);
// Check remaining units
$remaining = $user->getRemainingFeature('storage');
Listen for Subscription Events:
// In EventServiceProvider
protected $listen = [
\Lacodix\LaravelPlans\Events\SubscriptionRenewed::class => [
\App\Listeners\CreateInvoice::class,
],
];
Calculate Period Price:
$price = $subscription->calculatePeriodPrice(); // Handles partial periods
Custom Price Calculation:
$percentage = $subscription->calculatePeriodLengthInPercent();
$customPrice = $subscription->plan->meta['price_per_user'] * $userCount * ($percentage / 100);
Plan Ordering:
$plan->moveToStart(); // Move to top of list
$plan->moveToEnd(); // Move to bottom
Subscription Ordering:
$subscription->moveUp();
$subscription->moveDown();
Use Meta Data for Flexibility:
Store billing-specific data (e.g., currency, tax_rate) in the meta field of plans/subscriptions:
$plan->meta = ['currency' => 'EUR', 'tax_rate' => 0.2];
Combine with Billing Providers:
Trigger Stripe/PayPal subscriptions on SubscriptionCreated events:
public function handle(SubscriptionCreated $event) {
\Stripe\Subscription::create([
'customer' => $event->subscription->user->stripe_id,
'items' => [['price' => $event->subscription->plan->stripe_price_id]],
]);
}
Feature-Based UI Logic: Dynamically show/hide UI elements based on feature availability:
@if($user->hasFeature('advanced-analytics'))
<button>Enable Analytics</button>
@endif
Grace Period Handling:
Use hasGracePeriod() to extend access after cancellation:
if ($subscription->hasGracePeriod()) {
// Grant access until grace period ends
}
Testing Subscriptions:
Use Subscription::fake() in tests to mock subscriptions:
$user->subscribe($plan);
Subscription::fake()->assertSubscribed($user, $plan);
Slug Conflicts:
main, addon). Ensure slugs are unique per user.Feature Reset Timing:
resettable_period and resettable_interval. Misconfiguration can lead to unexpected resets.php artisan plans:reset-features to verify reset logic.Partial Period Pricing:
calculatePeriodPrice() accounts for trial periods and partial months. Overriding this logic may break billing.calculatePeriodLengthInPercent() for custom calculations.Translation Dependencies:
spatie/laravel-translatable. Missing translations may cause errors.$plan->name = ['en' => 'Default Name'];
Sortable Package Conflicts:
spatie/eloquent-sortable. Conflicts may arise with other sortable implementations.position columns exist in plans or subscriptions tables.Event Listener Order:
SubscriptionRenewed) may fire out of order if multiple listeners are registered.Meta Data Serialization:
Log Subscription Events: Add a listener to log events for debugging:
public function handle($event) {
\Log::info('Subscription Event', ['event' => $event::class, 'data' => $event->subscription->toArray()]);
}
Check Feature Consumption:
Use the plans:check-features artisan command to audit feature usage:
php artisan plans:check-features --user=1
Validate Plan/Feature Data:
Ensure required fields are set (e.g., slug, billing_interval):
$plan->validate([
'slug' => 'required|unique:plans',
'billing_interval' => 'required',
]);
Test Renewal Logic: Manually trigger renewals in tests to verify period calculations:
$subscription->forceRenew();
$this->assertEquals($expectedPrice, $subscription->calculatePeriodPrice());
Inspect Sort Order:
Use dd($plan->subscriptions()->orderBy('position')->get()) to debug subscription ordering.
Feature model to add domain-specific behavior:
class CustomFeature extends Feature {
public function isActive
How can I help you explore Laravel packages today?