Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laraplans Laravel Package

gerardojbaez/laraplans

LaraPlans is a Laravel package for adding subscription plans and feature limits to your application. Define plans, subscribe users, manage renewals, and gate features through a clean, configurable API.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and scaffold

composer require gerardojbaez/laraplans
php artisan vendor:publish --provider="Gerardojbaez\Laraplans\LaraplansServiceProvider"
php artisan migrate

The publish step drops config/laraplans.php and the package migrations (plans, features, plan_features, plan_subscriptions, subscriptions). Run migrations before you touch the User model.

Wire the billable model

use Gerardojbaez\Laraplans\Traits\HasPlans;

class User extends Authenticatable
{
    use HasPlans;
}

First use case — gate a feature

  1. Seed a plan with features:
$plan = Plan::create([
    'name' => 'Pro',
    'slug' => 'pro',
    'description' => 'Pro tier',
    'price' => 29.00,
    'interval' => 'month',
    'interval_count' => 1,
    'trial_period' => 14,
    'trial_interval' => 'day',
    'grace_period' => 3,
    'grace_interval' => 'day',
]);

$plan->features()->create([
    'name' => 'API Calls',
    'slug' => 'api-calls',
    'value' => 10000,
    'resettable_period' => 1,
    'resettable_interval' => 'month',
]);
  1. Subscribe and check in a controller:
$user->subscribeTo($plan, 'main');
$user->subscribed();              // bool
$user->onPlan('pro');            // bool
$user->canConsume('api-calls', 1); // bool

Where to look first — config/laraplans.php (model bindings and table names), then the HasPlans and ConsumesFeatures traits. Those two files are the real API surface; the docs are thin, so reading the traits is faster than hunting for examples.

Implementation Patterns

Feature gating at the route layer. Register the package middleware and attach it per-route rather than globally:

// app/Http/Kernel.php
'plan'         => \Gerardojbaez\Laraplans\Http\Middleware\CheckPlan::class,
'subscription' => \Gerardojbaez\Laraplans\Http\Middleware\CheckSubscription::class,
Route::middleware(['auth', 'plan:pro'])->group(function () {
    Route::get('/reports', ReportController::class);
});

Use plan: for tier checks and subscription: when a user can hold multiple named subscriptions and you need to target one.

Metered feature consumption. Treat canConsume + consume as a check-then-act pair inside a transaction for anything billable:

DB::transaction(function () use ($user) {
    abort_unless($user->canConsume('api-calls', 1), 429);
    $user->consume('api-calls', 1);
});

Store the feature value as null/0 semantics carefully — a null value usually means "unlimited" in this package, so don't assume canConsume returns false for a null quota; verify against your seeded plans.

Named subscriptions for multi-product accounts. subscribeTo($plan, 'reports') and subscribeTo($plan, 'api') let one user hold independent plans. Always pass the name through to subscribed('reports'), subscription('reports')->cancel(), etc. — the default name is main, and forgetting it is the single most common source of "why is my user still subscribed?" bugs.

Plan changes are yours to build. The package records a plan on a subscription but does not implement upgrade/downgrade/proration. A workable pattern:

$subscription = $user->subscription('main');
$subscription->changePlan($newPlan);   // swap the plan_id
// you own: proration math, invoice adjustment, refunds

Keep an audit trail (a subscription_changes table or an event listener on SubscriptionUpdated) because the package stores only the current state.

Resetting metered features. Features declare resettable_period / resettable_interval but nothing resets them automatically. Schedule it:

// app/Console/Kernel.php
$schedule->call(function () {
    \Gerardojbaez\Laraplans\Models\Subscription::query()
        ->with('plan.features')
        ->chunk(200, fn ($subs) => $subs->each->resetFeaturesUsage());
})->hourly();

Verify the exact reset method name against the Subscription model in your installed version — it has changed across releases.

Blade directives for UI. The package registers @subscribed, @plan, @feature, @onTrial, and @cancelled:

@subscribed('main')
    <a href="/billing">Manage billing</a>
@else
    <a href="/pricing">Upgrade</a>
@endsubscribed

@feature('api-calls')
    <p>You have API access.</p>
@endfeature

These read from the authenticated user; they're convenience wrappers, not authorization. Don't rely on them server-side.

Testing. Because subscribeTo accepts explicit start/end dates, you can build deterministic tests without touching the clock:

$user->subscribeTo($plan, 'main', now()->subDays(20), now()->addDays(10));
$this->assertTrue($user->subscribed('main'));
$this->travel(11)->days();
$this->assertFalse($user->subscribed('main'));

Prefer Carbon::setTestNow() over real sleeps; grace periods and trial expiry are date-comparison logic.

Gotchas and Tips

No payment integration, by design. This is a state machine for plans and features, not a billing client. There is no Stripe/Cashier/Paddle bridge. Plan accordingly: either layer it on top of Cashier, or sync subscription state from your payment provider's webhooks into subscribeTo / cancel.

Trait collision with Cashier. Cashier's Billable and this package's HasPlans both assume ownership of the User model's subscription concepts. Composing them requires care — Cashier names its relationship subscriptions, and so does this package. If you use both, alias one relationship or keep Laraplans strictly for feature gating and let Cashier own billing state.

Config model swapping. config/laraplans.php lets you substitute your own Plan, Feature, and Subscription classes. Do this early if you plan to add columns — swapping after data exists means writing a migration that also updates the config, and any cached config will silently point at the old class until php artisan config:clear.

Slug uniqueness is on you. Plans are looked up by slug throughout (onPlan, @plan, plan: middleware). A duplicate slug won't necessarily error at the DB level depending on your migration state, but lookups will return the wrong plan. Enforce uniqueness in the migration and validate in the seeder/admin form.

Deleting plans with subscribers. Nothing prevents deleting a Plan that active subscriptions reference, and the FK behavior depends on how you wrote your migrations. Guard deletes in application code, or archive plans with an is_active flag and filter them out of your pricing page.

Grace periods and subscribed() disagree with intuition. A cancelled subscription inside its grace window can still report cancelled() as true while subscribed() also returns true. Always model your UI around all three states — subscribed(), cancelled(), onTrial() — rather than a single boolean, and write an explicit test for the "cancelled but in grace" case.

Trial dates live on the subscription, not the plan. The plan's trial_period seeds the subscription's trial_ends_at at subscribe time. Changing the plan's trial period later does not retroactively affect existing subscriptions — which is usually what you want, but surprises people debugging "why doesn't this user have a trial."

Timezone drift in date comparisons. Subscription boundaries are compared against now() in the app timezone. If you write admin tooling that sets ends_at from a user-supplied date string, normalize to the app timezone explicitly, or you'll get off-by-a-day cancellations.

N+1 on feature checks. canConsume/consume walk subscription -> plan -> features. On a dashboard that renders ten quotas, eager load once:

$user->load('subscription.plan.features');

Otherwise expect a query storm on pages you'd assume were cheap.

Middleware aliases moved. On Laravel 11+ there's no Kernel.php; register the aliases in bootstrap/app.php via ->withMiddleware(fn ($m) => $m->alias([...])). The package docs predate this, so the snippet you'll find online is for Laravel ≤10.

Extension points worth knowing. The models are Macroable-adjacent through normal Eloquent extension (custom accessors, scopes), and the service provider binds the models from config — so a custom Subscription subclass with changePlan(), swap(), and invoice() helpers is the natural place to put your proration logic. Keep those helpers out of controllers; they're the part of your app most likely to be reused by webhooks, admin panels, and CLI commands.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle