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\Plans\PlansServiceProvider" --tag="migrations"
php artisan migrate
Configure: Publish the config file:
php artisan vendor:publish --provider="Lacodix\Plans\PlansServiceProvider" --tag="config"
Update config/plans.php with your Stripe/Paddle/etc. API keys and billing settings.
First Use Case: Define a plan via a migration or Tinker:
use Lacodix\Plans\Models\Plan;
Plan::create([
'name' => 'Basic',
'slug' => 'basic',
'price' => 9.99,
'billing_interval' => 'month',
'currency' => 'USD',
'description' => 'Basic features for individuals',
]);
Key Models:
Plan: Represents a subscription tier.Feature: Optional features tied to plans (countable/uncountable).Subscription: Tracks user subscriptions (active, canceled, etc.).Addon: Optional add-ons for extra features.// Create a plan
$plan = Plan::create([...]);
// Fetch plans for checkout
$plans = Plan::with('features')->where('is_active', true)->get();
price and billing_interval (e.g., month, year) to calculate recurring charges.
$plan->price; // 9.99
$plan->billing_interval; // 'month'
$feature = Feature::create([
'name' => 'Storage',
'slug' => 'storage',
'is_countable' => true,
'max_count' => 10, // Optional for countable features
]);
$plan->features()->attach($feature, ['count' => 5]); // Attach with a count
$feature = Feature::create([
'name' => 'API Access',
'slug' => 'api_access',
'is_countable' => false,
]);
$plan->features()->attach($feature); // Simple boolean attachment
use Lacodix\Plans\Facades\Plans;
$subscription = Plans::subscribe($user, $planId, [
'trial_days' => 7,
'billing_cycle_anchor' => now()->startOfMonth(),
]);
if ($user->hasFeature('storage', 10)) {
// Grant access
}
$subscription->cancel(); // Soft cancel
$subscription->resume(); // Resume canceled subscription
$addon = Addon::create([...]);
$subscription->addons()->attach($addon);
if ($subscription->hasAddon('premium_support')) {
// Enable premium support
}
php artisan vendor:publish --provider="Lacodix\Plans\PlansServiceProvider" --tag="webhook-config"
Update config/plans/webhooks.php with your endpoint and secret.
Handle events in app/Http/Controllers/WebhookController:
use Lacodix\Plans\Events\SubscriptionCreated;
public function handleWebhook(Request $request) {
event(new SubscriptionCreated($request->all()));
}
Create middleware to check features before accessing routes:
namespace App\Http\Middleware;
use Closure;
use Lacodix\Plans\Facades\Plans;
class CheckFeature
{
public function handle($request, Closure $next, $featureSlug, $minCount = null)
{
if (!Plans::userHasFeature($request->user(), $featureSlug, $minCount)) {
abort(403, 'Access denied');
}
return $next($request);
}
}
Register in app/Http/Kernel.php:
'check.feature' => \App\Http\Middleware\CheckFeature::class,
Create a directive to display feature availability:
Blade::directive('feature', function ($featureSlug) {
return "<?php if(\Lacodix\Plans\Facades\Plans::userHasFeature(auth()->user(), {$featureSlug})): ?>";
});
Blade::directive('endif', function () {
return "<?php endif; ?>";
});
Usage in Blade:
@feature('api_access')
<button>Enable API</button>
@endif
Use a seeder to populate initial plans/features:
public function run()
{
$basicPlan = Plan::create([...]);
$proPlan = Plan::create([...]);
$storageFeature = Feature::create([
'name' => 'Storage',
'slug' => 'storage',
'is_countable' => true,
'max_count' => 100,
]);
$basicPlan->features()->attach($storageFeature, ['count' => 10]);
$proPlan->features()->attach($storageFeature, ['count' => 100]);
}
Use factories and mocks for testing:
$user = User::factory()->create();
$plan = Plan::factory()->create();
$subscription = Plans::subscribe($user, $plan->id);
$this->assertTrue($user->hasFeature('storage', 10));
Plans::syncSubscription() to force-sync subscriptions on critical paths (e.g., feature checks).\Log::info('Webhook received', $request->all());
Plans::userHasFeature() instead of caching counts in the session.Plans::refreshUserFeatures($user) to manually refresh feature counts.billing_interval (e.g., month vs. year) can lead to misaligned billing cycles.protected $rules = [
'billing_interval' => 'required|in:day,week,month,year',
];
Plans::getUserFeatureCount($user, 'storage') to aggregate counts from both plans and add-ons.subscription table.Plans::syncWithProvider() method to reconcile differences:
$subscription = Plans::findByProviderId($providerId);
$subscription->syncWithProvider(); // Updates local state
Enable debug logging in config/plans.php:
'debug' => env('PLANS_DEBUG', false),
Check logs for subscription lifecycle events:
[2025-11-20] INFO Subscription created for user #1: Plan #5 (Basic)
[2025-11-20] INFO Subscription canceled for user #1: Plan #5 (Basic)
Use Laravel Debugbar to inspect feature queries
How can I help you explore Laravel packages today?