PHPackages                             vnuswilliams/laravel-subscription - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Payment Processing](/categories/payments)
4. /
5. vnuswilliams/laravel-subscription

ActiveLibrary[Payment Processing](/categories/payments)

vnuswilliams/laravel-subscription
=================================

A robust, fluent and payment-agnostic subscription management package for Laravel. Handles plans, lifecycle (trial, grace period, cancellation) and consumable feature quotas.

v1.0.1(yesterday)04↑2900%MITPHPPHP ^8.2

Since Jun 26Pushed todayCompare

[ Source](https://github.com/vnuswilliams/laravel-subscription)[ Packagist](https://packagist.org/packages/vnuswilliams/laravel-subscription)[ Docs](https://github.com/vnuswilliams/laravel-subscription)[ RSS](/packages/vnuswilliams-laravel-subscription/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (12)Versions (3)Used By (0)

Laravel Subscription
====================

[](#laravel-subscription)

[![Latest Version on Packagist](https://camo.githubusercontent.com/0235d49e0f40bc9eba246c270f5bda1569de8333e88540bf9c166e4687bbf954/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f766e757377696c6c69616d732f6c61726176656c2d737562736372697074696f6e2e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/vnuswilliams/laravel-subscription)[![Total Downloads](https://camo.githubusercontent.com/3a4955ac3b394e332fc2ab19dd382fdb2117e1acef7dcb4469de127ab8cfd7e2/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f766e757377696c6c69616d732f6c61726176656c2d737562736372697074696f6e2e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/vnuswilliams/laravel-subscription)[![License](https://camo.githubusercontent.com/55c0218c8f8009f06ad4ddae837ddd05301481fcf0dff8e0ed9dadda8780713e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d627269676874677265656e2e7376673f7374796c653d666c61742d737175617265)](LICENSE.md)[![PHP](https://camo.githubusercontent.com/fca6a5abe8cb8ca5a09d7514f79421a5acfc883e66c5e71627c5051291b2c4ce/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d382e322532422d626c75653f7374796c653d666c61742d737175617265)](https://php.net)[![Laravel](https://camo.githubusercontent.com/da2f0a63ea2d566933deef7cbad8355404a82cbbc0e76f625f365df438af15cf/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c61726176656c2d3131253230253743253230313225323025374325323031332d7265643f7374796c653d666c61742d737175617265)](https://laravel.com)

A robust, fluent, and **fully payment-agnostic** Laravel package for managing subscription plans, lifecycle states (trial, grace period, cancellation), and consumable feature quotas.

This package does not handle any payments. It exclusively manages **subscription business logic**: who has access to what, for how long, and how much they have left. You plug in the payment provider of your choice (Stripe, Paystack, Flutterwave, PayPal…) around it.

This documentation is always release in [french version](readmefr.md).

---

Table of Contents
-----------------

[](#table-of-contents)

1. [Architecture](#architecture)
2. [Installation](#installation)
3. [Configuring Plans in the Database](#configuring-plans-in-the-database)
4. [Preparing the Subscriber Model](#preparing-the-subscriber-model)
5. [Entry Points: Three Ways to Use the Package](#entry-points-three-ways-to-use-the-package)
6. [Managing Subscriptions](#managing-subscriptions)
7. [Features &amp; Quotas](#features--quotas)
8. [Lifecycle &amp; Grace Period](#lifecycle--grace-period)
9. [Route Protection Middleware](#route-protection-middleware)
10. [Laravel Events](#laravel-events)
11. [Artisan Command](#artisan-command)
12. [Full Recipe: Application Service](#full-recipe-application-service)
13. [API Reference](#api-reference)
14. [Tips &amp; Best Practices](#tips--best-practices)

---

Architecture
------------

[](#architecture)

The package is built on a strict separation of concerns:

```
SubscriptionManager          ← Public entry point (Facade or injection)
    │
    ├── SubscriptionService  ← Logic: subscribeTo, cancel, switchTo, renew…
    └── FeatureService       ← Logic: canConsume, consume, release, balance…

HasSubscriptions (Trait)     ← Ergonomic proxy on the Eloquent model

```

**Golden rule:** the `HasSubscriptions` trait contains no business logic. It delegates everything to the `SubscriptionManager`. This keeps the logic testable, injectable, and independent of the Eloquent model.

---

Installation
------------

[](#installation)

### 1. Install via Composer

[](#1-install-via-composer)

```
composer require vnuswilliams/laravel-subscription
```

The `ServiceProvider` and `Facade` are auto-discovered by Laravel. No manual registration needed.

### 2. Publish the configuration and migrations

[](#2-publish-the-configuration-and-migrations)

```
# Configuration
php artisan vendor:publish --tag=subscription-config

# Migrations
php artisan vendor:publish --tag=subscription-migrations

# Run migrations
php artisan migrate
```

### 3. (Optional) Generate the application service

[](#3-optional-generate-the-application-service)

The package provides a generator command that scaffolds a ready-to-use `SubscriptionService` tailored to the model that carries the `HasSubscriptions` trait in your application.

Run it and pass your model name via the `--model` option:

```
# With a User model
php artisan subscription:generate-service --model=User

# With a Company model
php artisan subscription:generate-service --model=Company

# With a Team model
php artisan subscription:generate-service --model=Team
```

Omit the option to be prompted interactively:

```
php artisan subscription:generate-service

# > Which model has the HasSubscriptions trait? (e.g. User, Company, Team)
# > Company
```

This generates `app/Services/SubscriptionService.php` pre-filled with your model. If the file already exists, the command asks for confirmation before overwriting it. See the [Full Recipe](#full-recipe-application-service) section for the generated content and usage examples.

> **Tip:** the model name is case-insensitive — `user`, `User`, and `USER` all produce `User` in the generated file.

---

Configuring Plans in the Database
---------------------------------

[](#configuring-plans-in-the-database)

The package does not create your plans automatically. You insert them via a seeder, a migration, or your application's admin interface.

Here is the expected structure for a monthly plan with features:

```
// database/seeders/PlanSeeder.php

use Vnuswilliams\Subscription\Models\Plan;
use Vnuswilliams\Subscription\Enums\FeatureType;
use Vnuswilliams\Subscription\Enums\PeriodicityType;

// Pro Plan — monthly, 7-day grace period
$pro = Plan::create([
    'name'             => 'Pro',
    'slug'             => 'pro',
    'description'      => 'For growing teams.',
    'periodicity_type' => PeriodicityType::Month->value,  // 'month'
    'periodicity'      => 1,
    'trial_days'       => 0,
    'grace_days'       => 7,
    'is_active'        => true,
]);

// Consumable feature: employee quota
$pro->features()->create([
    'slug'    => 'max-employees',
    'name'    => 'Number of employees',
    'type'    => FeatureType::Consumable->value,  // 'consumable'
    'charges' => 25,  // 25 available slots
]);

// Boolean feature: access to the employee portal
$pro->features()->create([
    'slug'    => 'employee-portal',
    'name'    => 'Employee Portal',
    'type'    => FeatureType::Boolean->value,  // 'boolean'
    'charges' => null,  // null = unlimited / no counter
]);

// Free Plan — permanent (no periodicity), 15-day trial
Plan::create([
    'name'             => 'Free',
    'slug'             => 'free',
    'periodicity_type' => null,   // null = permanent plan, never expires
    'periodicity'      => null,
    'trial_days'       => 15,
    'grace_days'       => 0,
    'is_active'        => true,
]);
```

> **Tip:** centralise all your feature slugs in a `FeatureEnum` in your application. This prevents typos and gives you IDE autocompletion.

```
// app/Enums/FeatureEnum.php
enum FeatureEnum: string
{
    case MAX_EMPLOYEES    = 'max-employees';
    case EMPLOYEE_PORTAL  = 'employee-portal';
    case DOCUMENTS        = 'documents';
    case ADVANCED_REPORTS = 'advanced-reports';
    case PRIORITY_SUPPORT = 'priority-support';
}
```

---

Preparing the Subscriber Model
------------------------------

[](#preparing-the-subscriber-model)

Add the `HasSubscriptions` trait to any Eloquent model that needs to subscribe to a plan: `User`, `Company`, `Team`, `Organization`…

```
// app/Models/Company.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Vnuswilliams\Subscription\Traits\HasSubscriptions;

class Company extends Model
{
    use HasSubscriptions;
}
```

That's it. The trait automatically exposes the `subscription()` relationship and all of the package's fluent methods directly on your model.

---

Entry Points: Three Ways to Use the Package
-------------------------------------------

[](#entry-points-three-ways-to-use-the-package)

The package exposes three interfaces depending on your context. Choose the one that fits your situation.

### 1. Via the Trait (on the model)

[](#1-via-the-trait-on-the-model)

The most fluent syntax for one-off calls directly on the Eloquent instance:

```
$company->subscribeTo('pro');
$company->hasActiveSubscription();
$company->canConsume('max-employees', 1);
$company->balance('max-employees');
```

Ideal in Observers, Policies, or quick checks inside a Controller.

### 2. Via the Facade (anywhere in the app)

[](#2-via-the-facade-anywhere-in-the-app)

The Laravel-style static syntax, accessible everywhere without injection:

```
use Vnuswilliams\Subscription\Facades\Subscription;

Subscription::subscribeTo($company, 'pro');
Subscription::cancel($company);
Subscription::canConsume($company, 'max-employees', 1);
Subscription::balance($company, 'max-employees');
```

Ideal in Controllers, Actions, Jobs, or Listeners.

### 3. Via SubscriptionManager injection (in your services)

[](#3-via-subscriptionmanager-injection-in-your-services)

The recommended approach for complex business logic. Fully testable, with no static dependency:

```
use Vnuswilliams\Subscription\SubscriptionManager;

class SubscriptionService
{
    public function __construct(
        private readonly SubscriptionManager $subscription,
    ) {}

    public function canAddEmployee(Company $company): bool
    {
        return $this->subscription->hasActiveSubscription($company)
            && $this->subscription->canConsume($company, FeatureEnum::MAX_EMPLOYEES->value, 1);
    }
}
```

> **Tip:** in a dedicated subscription service, always prefer direct injection. The Facade is handy for isolated calls, but makes unit testing harder.

---

Managing Subscriptions
----------------------

[](#managing-subscriptions)

### Subscribing to a plan

[](#subscribing-to-a-plan)

Pass the plan slug (string) or a `Plan` instance directly:

```
// By slug
$company->subscribeTo('pro');

// By instance
$plan = Plan::where('slug', 'pro')->firstOrFail();
$company->subscribeTo($plan);

// Via the Facade
use Vnuswilliams\Subscription\Facades\Subscription;
Subscription::subscribeTo($company, 'pro');
```

If the plan has `trial_days > 0`, the status will automatically be set to `on_trial` and `trial_ends_at` will be calculated. No additional action required.

### Subscribing with a custom expiration

[](#subscribing-with-a-custom-expiration)

Useful for free plans or fixed-duration promotional offers:

```
// Free plan with a 15-day trial
$company->subscribeTo('free', expiration: now()->addDays(15));

// Promotional offer: 3 months free
$company->subscribeTo('pro', expiration: now()->addMonths(3));
```

### Switching plans (upgrade / downgrade)

[](#switching-plans-upgrade--downgrade)

```
// Immediate switch: the old subscription is removed, the new one starts
$company->switchTo('business');

// Deferred switch: the old subscription runs until its end date
$company->switchTo('starter', immediately: false);

// Via the Facade
Subscription::switchTo($company, 'business');
```

### Cancelling a subscription

[](#cancelling-a-subscription)

Cancellation **does not cut access immediately**. The user retains access until `ends_at`, then the grace period activates if configured. This is the expected behaviour for an end-of-period cancellation.

```
$company->subscription->cancel();

// Or via the Facade
Subscription::cancel($company);
```

To check whether a subscription is cancelled but still running:

```
if ($company->subscription->isCanceled()) {
    // The user has cancelled, but still has access until ends_at
    $expiresAt = $company->subscriptionExpiresAt();
}
```

### Revoking access immediately

[](#revoking-access-immediately)

To cut access without waiting for the end of the period (suspension for non-payment, terms of service violation, etc.):

```
$company->subscription->suppress();

// Or via the Facade
Subscription::suppress($company);
```

### Renewing a subscription

[](#renewing-a-subscription)

Starts a full new cycle from now. Useful after a successful payment:

```
$company->renewSubscription();

// Or via the Facade
Subscription::renew($company);
```

### Checking subscription status

[](#checking-subscription-status)

```
// Is the subscription valid? (active, on trial, or in grace period)
$company->hasActiveSubscription(); // bool

// What is the current plan?
$plan = $company->currentPlan(); // Plan|null
echo $plan->name;  // 'Pro'
echo $plan->slug;  // 'pro'

// When does it expire?
$date = $company->subscriptionExpiresAt(); // Carbon|null

// Direct access to the Subscription model
$sub = $company->subscription;
$sub->isActive();        // bool
$sub->isOnTrial();       // bool
$sub->isOnGracePeriod(); // bool
$sub->isCanceled();      // bool
$sub->isExpired();       // bool
$sub->hasAccess();       // bool — aggregates all valid states
```

---

Features &amp; Quotas
---------------------

[](#features--quotas)

### Boolean features (yes/no access)

[](#boolean-features-yesno-access)

A boolean feature is simply attached to a plan or not. If it is not in the plan's feature list, access is denied.

```
// Does the company have access to the employee portal?
if ($company->canConsume('employee-portal')) {
    // access granted
}

// Via the Facade
if (Subscription::canConsume($company, 'employee-portal')) {
    // access granted
}
```

> For a boolean feature, the `$amount` parameter is ignored. `canConsume('feature', 0)` and `canConsume('feature', 1)` return the same result.

### Consumable features (quotas)

[](#consumable-features-quotas)

The standard flow for a quota feature: check → act → consume.

```
// ✅ Recommended pattern
if ($company->canConsume('max-employees', 1)) {

    // Business action first
    $employee = Employee::create([...]);

    // Consumption afterwards
    $company->consume('max-employees', 1);

} else {
    return back()->with('error', 'Employee quota reached. Please upgrade your plan.');
}
```

> **Important:** always call `canConsume()` before `consume()`. The package does not throw an exception if you consume beyond the quota — that guard is your responsibility.

### Releasing a slot (decrementing consumption)

[](#releasing-a-slot-decrementing-consumption)

When you delete a resource, release the corresponding slot:

```
// Deleting an employee → releases 1 slot
$employee->delete();
$company->release('max-employees', 1);
```

`release()` decrements `used` safely (never below 0). This is more reliable than deleting the last consumption record.

### Inspecting quotas (for dashboards)

[](#inspecting-quotas-for-dashboards)

```
// Total slots allocated by the plan
$total = $company->totalCharges('max-employees');  // e.g. 25

// Slots consumed in the current period
$used = $company->usedCharges('max-employees');    // e.g. 17

// Remaining slots (PHP_INT_MAX if unlimited)
$remaining = $company->balance('max-employees');   // e.g. 8
```

Example usage in a Blade view for a progress bar:

```
@php
    $total     = $company->totalCharges('max-employees');
    $used      = $company->usedCharges('max-employees');
    $remaining = $company->balance('max-employees');
    $percent   = $total > 0 ? round(($used / $total) * 100) : 0;
@endphp

{{ $used }} / {{ $total }} employees — {{ $remaining }} slots remaining
```

---

Lifecycle &amp; Grace Period
----------------------------

[](#lifecycle--grace-period)

The full lifecycle of a subscription:

```
[on_trial] ──(trial_ends_at passed)──> [active]
[active]   ──(ends_at passed)────────> [on_grace_period] ──(grace_ends_at passed)──> [expired]
[active]   ──(cancel())───────────────> [canceled] (hasAccess() = true until ends_at)
[active]   ──(suppress())─────────────> [expired]  (hasAccess() = false immediately)

```

The `hasAccess()` method is your single source of truth. It returns `true` for the `active`, `on_trial`, `on_grace_period`, and `canceled` (if `ends_at` is in the future) states. It returns `false` for `expired` and suppressed subscriptions.

### Configuring the grace period per plan

[](#configuring-the-grace-period-per-plan)

The grace period is configured in the plan data (`grace_days` column). No global configuration is required. Each plan can have its own duration:

```
Plan::create([
    'slug'       => 'pro',
    'grace_days' => 7,   // 7-day grace period after expiration
    // ...
]);

Plan::create([
    'slug'       => 'free',
    'grace_days' => 0,   // No grace period on the free plan
    // ...
]);
```

---

Route Protection Middleware
---------------------------

[](#route-protection-middleware)

The package automatically registers the `subscribed` middleware. Use it in your route files:

```
// routes/web.php

// Requires any valid subscription
Route::middleware(['auth', 'subscribed'])->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index']);
    Route::get('/employees', [EmployeeController::class, 'index']);
});

// Requires a specific plan (by slug)
Route::middleware(['auth', 'subscribed:business'])->group(function () {
    Route::get('/analytics', [AnalyticsController::class, 'index']);
    Route::get('/support', [SupportController::class, 'index']);
});
```

When access is denied, the middleware returns:

- A **JSON 403** if the request expects JSON (`Accept: application/json`)
- A **redirect** to `home` with an `error` flash message otherwise

To customise this behaviour, extend `CheckSubscription` and rebind it in your `AppServiceProvider`.

---

Laravel Events
--------------

[](#laravel-events)

The package dispatches native Laravel events on every lifecycle transition. Register your listeners in `EventServiceProvider` or using Laravel 11+ `#[AsEventListener]` attributes.

EventTriggered whenTypical use case`SubscriptionCreated`A new subscription is createdWelcome email, access activation`SubscriptionCanceled`Subscription cancelled (end of period)Retention email, exit survey`SubscriptionEnteredGracePeriod`Expiration reached, grace activatedUrgent payment reminder email`SubscriptionExpired`Grace period over, access cutSuspension, notification, archiving`FeatureQuotaReached`A feature quota is exhaustedUpsell notification, admin alert```
// app/Listeners/SendWelcomeEmail.php

use Vnuswilliams\Subscription\Events\SubscriptionCreated;

class SendWelcomeEmail
{
    public function handle(SubscriptionCreated $event): void
    {
        $subscriber = $event->subscription->subscriber;
        // $subscriber is the Company, User, etc. instance

        Mail::to($subscriber->email)->send(new WelcomeMail($subscriber));
    }
}
```

```
// app/Listeners/NotifyQuotaExhausted.php

use Vnuswilliams\Subscription\Events\FeatureQuotaReached;

class NotifyQuotaExhausted
{
    public function handle(FeatureQuotaReached $event): void
    {
        $subscriber  = $event->subscription->subscriber;
        $featureSlug = $event->feature->slug;  // e.g. 'max-employees'

        // Send a notification suggesting an upgrade
        $subscriber->notify(new QuotaReachedNotification($featureSlug));
    }
}
```

---

Artisan Command
---------------

[](#artisan-command)

The `subscription:check-lifecycle` command iterates over all subscriptions in the database and performs any missing status transitions (active → on\_grace\_period → expired).

It is useful for users who do not log in often: their subscription will move to grace or expire even without an incoming request, and the relevant events will be dispatched correctly.

```
# Manual execution
php artisan subscription:check-lifecycle
```

Schedule it to run daily in `routes/console.php` (Laravel 11+):

```
// routes/console.php

use Illuminate\Support\Facades\Schedule;

Schedule::command('subscription:check-lifecycle')->daily();
```

Or in `app/Console/Kernel.php` (Laravel 10 and earlier):

```
protected function schedule(Schedule $schedule): void
{
    $schedule->command('subscription:check-lifecycle')->daily();
}
```

---

Full Recipe: Application Service
--------------------------------

[](#full-recipe-application-service)

Generate a ready-to-use `SubscriptionService` by running the generator command with the model that carries the `HasSubscriptions` trait:

```
# Replace "Company" with your actual model name
php artisan subscription:generate-service --model=Company
```

This creates `app/Services/SubscriptionService.php` pre-wired to your model. Here is what it contains and how to use it:

```
// app/Services/SubscriptionService.php

use Vnuswilliams\Subscription\SubscriptionManager;

final class SubscriptionService
{
    public function __construct(
        private readonly SubscriptionManager $subscription,
    ) {}

    public function subscribeTo(Company $company, PlanEnum $planEnum): Subscription
    {
        $plan = $this->subscription->resolvePlan($planEnum->value);

        // App-specific business logic:
        // the FREE plan gets a manual 15-day trial
        if ($planEnum === PlanEnum::FREE) {
            return $this->subscription->subscribeTo($company, $plan, expiration: now()->addDays(15));
        }

        return $this->subscription->subscribeTo($company, $plan);
    }

    public function canAddEmployee(Company $company): bool
    {
        return $this->subscription->hasActiveSubscription($company)
            && $this->subscription->canConsume($company, FeatureEnum::MAX_EMPLOYEES->value, 1);
    }

    public function consumeEmployeeSlot(Company $company): void
    {
        $this->subscription->consume($company, FeatureEnum::MAX_EMPLOYEES->value, 1);
    }

    public function releaseEmployeeSlot(Company $company): void
    {
        $this->subscription->release($company, FeatureEnum::MAX_EMPLOYEES->value, 1);
    }

    public function remainingEmployeeSlots(Company $company): int
    {
        return $this->subscription->balance($company, FeatureEnum::MAX_EMPLOYEES->value);
    }

    public function currentPlan(Company $company): ?PlanEnum
    {
        $plan = $this->subscription->currentPlan($company);

        return $plan !== null ? PlanEnum::tryFrom($plan->slug) : null;
    }
}
```

Usage in a Controller:

```
// app/Http/Controllers/EmployeeController.php

class EmployeeController extends Controller
{
    public function __construct(
        private readonly SubscriptionService $subscriptionService,
    ) {}

    public function store(StoreEmployeeRequest $request): RedirectResponse
    {
        $company = $request->user()->company;

        if (! $this->subscriptionService->canAddEmployee($company)) {
            return back()->with('error', 'Employee quota reached.');
        }

        $employee = Employee::create($request->validated());

        $this->subscriptionService->consumeEmployeeSlot($company);

        return redirect()->route('employees.index')
            ->with('success', 'Employee added successfully.');
    }

    public function destroy(Employee $employee): RedirectResponse
    {
        $company = auth()->user()->company;

        $employee->delete();

        // Release the slot so it can be reused
        $this->subscriptionService->releaseEmployeeSlot($company);

        return redirect()->route('employees.index')
            ->with('success', 'Employee deleted.');
    }
}
```

---

API Reference
-------------

[](#api-reference)

### `HasSubscriptions` Trait

[](#hassubscriptions-trait)

MethodReturnDescription`subscription()``MorphOne`Eloquent relationship to the latest subscription`subscribeTo($plan, $expiration, $immediately)``Subscription`Subscribes or switches if an active subscription exists`switchTo($plan, $immediately)``Subscription`Switches plan`renewSubscription()``Subscription`Renews from now`hasActiveSubscription()``bool`Is the subscription valid? (active, trial, grace)`currentPlan()``Plan|null`Current plan`subscriptionExpiresAt()``Carbon|null`Expiration date`canConsume($slug, $amount)``bool`Quota or boolean access available?`consume($slug, $amount)``SubscriptionUsage`Consumes $amount units`release($slug, $amount)``SubscriptionUsage`Releases $amount units`balance($slug)``int`Remaining balance (PHP\_INT\_MAX if unlimited)`totalCharges($slug)``int`Total allocated by the plan`usedCharges($slug)``int`Amount consumed### `Subscription` Model

[](#subscription-model)

MethodReturnDescription`isActive()``bool`Status is active AND ends\_at is in the future`isOnTrial()``bool`trial\_ends\_at is in the future`isOnGracePeriod()``bool`Within the grace window`isCanceled()``bool`Cancelled (access may still be available)`isSuppressed()``bool`Immediately revoked`isExpired()``bool`No access remaining`hasAccess()``bool`Global source of truth`cancel()``static`End-of-period cancellation`suppress()``static`Immediate access revocation`renew()``static`Renewal from now### Available Enums

[](#available-enums)

```
use Vnuswilliams\Subscription\Enums\SubscriptionStatus;
use Vnuswilliams\Subscription\Enums\FeatureType;
use Vnuswilliams\Subscription\Enums\PeriodicityType;

PeriodicityType::Day;    // 'day'
PeriodicityType::Week;   // 'week'
PeriodicityType::Month;  // 'month'
PeriodicityType::Year;   // 'year'

FeatureType::Boolean;    // 'boolean'
FeatureType::Consumable; // 'consumable'

SubscriptionStatus::Active;        // 'active'
SubscriptionStatus::OnTrial;       // 'on_trial'
SubscriptionStatus::OnGracePeriod; // 'on_grace_period'
SubscriptionStatus::Canceled;      // 'canceled'
SubscriptionStatus::Expired;       // 'expired'
```

---

Tips &amp; Best Practices
-------------------------

[](#tips--best-practices)

**Centralise your feature slugs in an enum.** A typo like `'max-employes'` instead of `'max-employees'` silently returns `false`. `FeatureEnum::MAX_EMPLOYEES->value` never makes that mistake.

**Always check before consuming.** The package does not throw an exception if you call `consume()` when the quota is exhausted. The `canConsume()` guard is your responsibility.

**Use `release()` when deleting resources.** If a user deletes an employee, release the slot. Otherwise the counter stays inflated and the user loses capacity they should get back.

**Do not confuse `cancel()` and `suppress()`.** `cancel()` is a normal cancellation — access is maintained until the end of the paid period. `suppress()` is an administrative or punitive suspension — access is cut immediately.

**Inject `SubscriptionManager` in your services, use the Facade in your controllers.** Services need to be unit-testable — avoid the Facade in classes you test with `pest` or `phpunit`. In a Controller or a Livewire component, the Facade is perfectly appropriate.

**Handle exceptions.** The package throws typed exceptions for error cases:

```
use Vnuswilliams\Subscription\Exceptions\InvalidPlanException;
use Vnuswilliams\Subscription\Exceptions\SubscriptionNotFoundException;
use Vnuswilliams\Subscription\Exceptions\FeatureNotFoundException;

try {
    Subscription::subscribeTo($company, 'non-existent-plan');
} catch (InvalidPlanException $e) {
    // Plan not found or inactive
    Log::warning($e->getMessage());
}
```

**Schedule the `subscription:check-lifecycle` command without fail.** Without it, an inactive user who makes no requests will never see their subscription transition to `expired` in the database — and `SubscriptionExpired` events will never be dispatched.

---

License
-------

[](#license)

This package is open-sourced software licensed under the [MIT license](LICENSE.md).

###  Health Score

42

—

FairBetter than 89% of packages

Maintenance100

Actively maintained with recent releases

Popularity5

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity47

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

1d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/7802022?v=4)[AdaWu](/maintainers/Vnus)[@Vnus](https://github.com/Vnus)

---

Top Contributors

[![vnuswilliams](https://avatars.githubusercontent.com/u/106296680?v=4)](https://github.com/vnuswilliams "vnuswilliams (4 commits)")

---

Tags

laravelbillingsubscriptionsaasquotafeaturesplans

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/vnuswilliams-laravel-subscription/health.svg)

```
[![Health](https://phpackages.com/badges/vnuswilliams-laravel-subscription/health.svg)](https://phpackages.com/packages/vnuswilliams-laravel-subscription)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.1M337](/packages/psalm-plugin-laravel)[larastan/larastan

Larastan - Discover bugs in your code without running it. A phpstan/phpstan extension for Laravel

6.4k51.0M7.6k](/packages/larastan-larastan)[laravel/pulse

Laravel Pulse is a real-time application performance monitoring tool and dashboard for your Laravel application.

1.7k14.1M122](/packages/laravel-pulse)[laravel/cashier

Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services.

2.5k28.4M137](/packages/laravel-cashier)[roots/acorn

Framework for Roots WordPress projects built with Laravel components.

9742.3M121](/packages/roots-acorn)[laravel/mcp

Rapidly build MCP servers for your Laravel applications.

76518.2M120](/packages/laravel-mcp)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
