PHPackages                             saadmajeed/laravel-entitlements - 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. [Authentication &amp; Authorization](/categories/authentication)
4. /
5. saadmajeed/laravel-entitlements

ActiveLibrary[Authentication &amp; Authorization](/categories/authentication)

saadmajeed/laravel-entitlements
===============================

SaaS feature engine — decisions about access, limits, and usage for Laravel.

00PHPCI failing

Since Jun 4Pushed 1mo agoCompare

[ Source](https://github.com/SaadMajeed565/laravel-entitlements)[ Packagist](https://packagist.org/packages/saadmajeed/laravel-entitlements)[ RSS](/packages/saadmajeed-laravel-entitlements/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependenciesVersions (1)Used By (0)

Laravel Entitlements
====================

[](#laravel-entitlements)

A feature/entitlement engine for Laravel 11+. Define plans, features, limits, metered usage, and conditional access rules — then check them anywhere in your app.

Features
--------

[](#features)

- **Boolean features** — on/off feature flags per plan
- **Numeric limits** — caps like "max 5 projects"
- **Metered usage** — countable consumption with daily/monthly/yearly resets
- **Time-based entitlements** — features that expire after a date
- **Conditional entitlements** — access gated by subject attributes (role, region, etc.)
- **Plan inheritance** — child plans inherit values from parent plans
- **Per-subject overrides** — override plan values for individual users
- **Audit logging** — tracks changes to entitlements, plans, and overrides
- **Caching** — Redis-backed resolution cache with configurable TTL
- **Fluent API, Facade, Middleware, Blade directive**

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

[](#installation)

```
composer require saadmajeed/laravel-entitlements
```

Publish the config and migrations:

```
php artisan vendor:publish --tag=entitlements-config
php artisan vendor:publish --tag=entitlements-migrations
php artisan migrate
```

Configuration
-------------

[](#configuration)

See `config/entitlements.php` for all options:

KeyDefaultDescription`cache.store``redis`Cache store for entitlement decisions`cache.ttl``3600`Cache TTL in seconds`cache.prefix``ent:`Cache key prefix`usage.queue``default`Queue for usage aggregation jobs`audit.enabled``true`Enable audit logging`middleware.default_http_code``403`HTTP status when entitlement check fails`default_subject_model``null`Default model for artisan commandQuick Start
-----------

[](#quick-start)

### 1. Add the trait to your subject

[](#1-add-the-trait-to-your-subject)

```
use SaadMajeed\Entitlements\Traits\HasPlan;

class User extends Authenticatable
{
    use HasPlan;
}
```

The `HasPlan` trait adds a `plan()` relationship and convenience methods.

### 2. Create a plan

[](#2-create-a-plan)

```
use SaadMajeed\Entitlements\Models\Plan;

$plan = Plan::create(['name' => 'Pro', 'slug' => 'pro']);
```

Assign the plan to a user:

```
$user->plan()->associate($plan);
$user->save();
```

### 3. Create entitlements

[](#3-create-entitlements)

```
use SaadMajeed\Entitlements\Models\Entitlement;

// Boolean feature flag
Entitlement::create([
    'key' => 'sso',
    'type' => 'boolean',
    'default_value' => false,
]);

// Numeric limit
Entitlement::create([
    'key' => 'max_projects',
    'type' => 'limit',
    'default_value' => 3,
]);

// Metered usage (resets monthly)
Entitlement::create([
    'key' => 'api_calls',
    'type' => 'metered',
    'default_value' => 1000,
    'reset_period' => 'monthly',
]);

// Time-based feature
Entitlement::create([
    'key' => 'early_access',
    'type' => 'time',
    'metadata' => ['expires_at' => '2025-12-31'],
]);

// Conditional feature
Entitlement::create([
    'key' => 'beta_feature',
    'type' => 'conditional',
    'metadata' => [
        'conditions' => [
            ['field' => 'region', 'operator' => '=', 'value' => 'us'],
        ],
    ],
]);
```

### 4. Assign values to a plan

[](#4-assign-values-to-a-plan)

```
$plan->entitlements()->attach($entitlement->id, [
    'value' => ['enabled' => true],  // boolean
    // or 'value' => 10,             // numeric limit
    // or 'value' => ['max' => 50],  // alternative limit syntax
]);
```

### 5. Check entitlements

[](#5-check-entitlements)

```
// Via trait
$user->can('sso');                    // bool
$user->limit('max_projects');         // ?float
$user->remaining('api_calls');        // ?float
$user->use('api_calls', 1);           // Decision
$user->why('sso');                    // Explanation

// Via facade
Entitlement::for($user)->can('sso');
Entitlement::for($user)->why('sso');

// Via helper
entitlement($user)->can('sso');
```

Entitlement Types
-----------------

[](#entitlement-types)

### Boolean

[](#boolean)

Simple on/off feature flag. The pivot value should be `['enabled' => true]` or `['enabled' => false]`.

```
$user->can('sso'); // true or false
```

### Limit

[](#limit)

A numeric cap. The value is a number or `['max' => N]`.

```
$user->limit('max_projects');     // e.g. 5
$user->remaining('max_projects'); // e.g. 3 (if 2 used)
```

### Metered

[](#metered)

Countable usage that resets on a period (daily, monthly, yearly).

```
$user->remaining('api_calls');          // e.g. 950 (of 1000)
$user->use('api_calls', 1);             // record usage, returns Decision
$user->use('api_calls', 5);             // record multiple at once
```

#### Soft limits

[](#soft-limits)

Set `soft_limit: true` in the entitlement's `metadata` to allow usage beyond the limit (fires event instead of throwing):

```
Entitlement::create([
    'key' => 'bonus_features',
    'type' => 'metered',
    'default_value' => 100,
    'reset_period' => 'monthly',
    'metadata' => ['soft_limit' => true],
]);
```

An `LimitReached` event is fired, but the `use()` call succeeds.

### Time-based

[](#time-based)

A feature that expires at a specific date.

```
$user->can('early_access'); // false if expired, true otherwise
```

If expired, an `ExpiredEntitlementException` is thrown.

### Conditional

[](#conditional)

Access depends on subject attributes.

```
// metadata: ['conditions' => [['field' => 'role', 'operator' => '=', 'value' => 'admin']]]
$user->can('beta_feature'); // true only for admins
```

Supported operators: `=`, `==`, `===`, `!=`, `!==`, ``, `>`, `>=`, ` 'Base', 'slug' => 'base']);
$pro = Plan::create(['name' => 'Pro', 'slug' => 'pro', 'parent_id' => $base->id]);

// Pro inherits all entitlements from Base,
// plus any values explicitly set on Pro
```

The resolution pipeline checks:

1. Per-subject override (if any)
2. Expiry (for time-based)
3. Conditional rules
4. Subject's direct plan value
5. Parent plan chain (walk up)
6. Entitlement's default value

Overrides
---------

[](#overrides)

Override an entitlement for a specific subject:

```
use SaadMajeed\Entitlements\Models\EntitlementOverride;

EntitlementOverride::create([
    'subject_type' => $user->getMorphClass(),
    'subject_id' => $user->getKey(),
    'entitlement_id' => $entitlement->id,
    'value' => ['enabled' => true],
    'expires_at' => now()->addDays(7),
    'reason' => 'Granted temporary access',
    'performed_by_type' => $admin->getMorphClass(),
    'performed_by_id' => $admin->getKey(),
]);
```

Overrides have the highest priority in the resolution pipeline. They can optionally expire.

Caching
-------

[](#caching)

Entitlement decisions are cached per subject per key. The cache is automatically invalidated when:

- An override is created, updated, or deleted (`OverrideApplied` event)
- A subject's plan changes (`PlanChanged` event)
- Usage is recorded (`UsageRecorded` event)

Manually flush the cache:

```
Entitlement::for($user)->flushCache();       // all entitlements for user
Entitlement::for($user)->flushCache('sso');  // specific key
```

Warm the cache:

```
php artisan entitlement:cache-warm
```

Events
------

[](#events)

EventPayloadFired when`EntitlementChecked`subject, entitlement, allowedAfter any entitlement check`LimitReached`subject, entitlement, used, limit, softWhen a limit is hit`OverrideApplied`overrideOverride created/updated/deleted`PlanChanged`subjectSubject's plan changes`UsageRecorded`subject, entitlement, amountUsage recordedListeners are auto-registered to invalidate the cache on `OverrideApplied`, `PlanChanged`, and `UsageRecorded`.

Usage Tracking
--------------

[](#usage-tracking)

Behind the scenes, `use()` creates a `UsageRecord` row and dispatches a queue job (`AggregateUsageJob`) to update the `UsageSummary` for the current period.

Usage aggregation is queued by default. Configure the queue name in `config/entitlements.php`.

For real-time accuracy, you can query remaining usage directly:

```
$decision = Entitlement::for($user)->use('api_calls', 1);
$decision->remaining; // 999 (of 1000)
```

### Concurrency

[](#concurrency)

Usage recording uses a database transaction with row-level locking (`SELECT ... FOR UPDATE`) to prevent double-counting when two requests consume usage simultaneously.

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

[](#api-reference)

### `Entitlement::for($subject)`

[](#entitlementforsubject)

Returns a cloned `EntitlementManager` scoped to the subject.

MethodReturnsDescription`can(string $key)``bool`Check if an entitlement is allowed`canMany(array $keys)``array`Check multiple entitlements`limit(string $key)``?float`Get the numeric limit value`remaining(string $key)``?float`Get remaining usage`use(string $key, float $amount = 1, array $metadata = [])``Decision`Record usage`why(string $key)``Explanation`Get full resolution trace (debugging)`flushCache(?string $key = null)``void`Clear cached decisions### `HasPlan` trait methods

[](#hasplan-trait-methods)

MethodReturnsDescription`$subject->can(string $key)``bool`Delegates to `Entitlement::for($subject)->can()``$subject->limit(string $key)``?float`Delegates to `Entitlement::for($subject)->limit()``$subject->remaining(string $key)``?float`Delegates to `Entitlement::for($subject)->remaining()``$subject->use(string $key, float $amount = 1)``Decision`Delegates to `Entitlement::for($subject)->use()``$subject->why(string $key)``Explanation`Delegates to `Entitlement::for($subject)->why()``$subject->entitlement(string $key)``FluentEntitlement`Fluent API gateway### `Decision` object

[](#decision-object)

PropertyTypeDescription`allowed``bool`Whether access is granted`key``string`The entitlement key`value``mixed`The resolved value`type``string`Entitlement type`source``?string`Where the value came from (plan, override, default, etc.)`limit``?float`The numeric limit (if applicable)`used``?float`Current usage (if metered/limit)`remaining``?float`Remaining usage (if metered/limit)`overLimit``bool`Whether the limit is exceeded`softLimit``bool`Whether a soft limit is active`expiresAt``?CarbonInterface`Expiration date (if time-based)`expired``bool`Whether the entitlement has expired`path``array`Resolution steps taken### `Explanation` object (from `why()`)

[](#explanation-object-from-why)

A detailed trace of how an entitlement was resolved, including:

- The resolution steps and their results
- Plan info (id, name, slug)
- Override info (value, expires\_at, reason)
- Expiry info (expires\_at, expired)
- Usage info (used, limit, remaining)

Artisan Commands
----------------

[](#artisan-commands)

```
# Check an entitlement for a subject
php artisan entitlement:check sso "User:1"

# List all entitlements
php artisan entitlement:list

# Show usage for a subject
php artisan entitlement:usage "User:1"

# Warm the entitlement cache
php artisan entitlement:cache-warm
```

The subject argument accepts `ModelClass:id` syntax (e.g. `User:1`, `App\Models\User:42`).

Middleware
----------

[](#middleware)

Protect routes with the `entitlement` middleware:

```
// In routes/web.php
Route::get('/sso', function () {
    return view('sso');
})->middleware('entitlement:sso');

// With a redirect
Route::get('/beta', function () {
    return view('beta');
})->middleware('entitlement:beta_feature:/upgrade');
```

If the subject (authenticated user) doesn't have the entitlement, the middleware aborts with 403 (or redirects if a redirect path is provided).

Blade Directive
---------------

[](#blade-directive)

```
@entitlement('sso')
    You have SSO access.
@endentitlement
```

Requires the user to be authenticated.

Exceptions
----------

[](#exceptions)

ExceptionHTTP statusWhen thrown`EntitlementNotFoundException`n/aEntitlement key not found in database`ExpiredEntitlementException`n/aTime-based entitlement has expired`LimitExceededException`n/aHard limit exceeded (has `key`, `limit`, `used`, `attempted`)Extend your exception handler to map these to HTTP codes as needed:

```
// In bootstrap/app.php or ExceptionHandler
->withExceptions(function (Exceptions $exceptions) {
    $exceptions->render(function (LimitExceededException $e) {
        return response()->json(['error' => $e->getMessage()], 429);
    });
});
```

Testing
-------

[](#testing)

```
phpunit
```

The test suite uses SQLite in-memory database.

Changelog
---------

[](#changelog)

See the GitHub releases for version history.

###  Health Score

19

—

LowBetter than 9% of packages

Maintenance59

Moderate activity, may be stable

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity11

Early-stage or recently created project

 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.

### Community

Maintainers

![](https://www.gravatar.com/avatar/36383b724cfe66dff2ca20d8b0cc2f7f3c0977a1a6ee59d3f3577a5280abd879?d=identicon)[Saad Majeed](/maintainers/Saad%20Majeed)

---

Top Contributors

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

### Embed Badge

![Health badge](/badges/saadmajeed-laravel-entitlements/health.svg)

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

###  Alternatives

[vitalybaev/laravel5-dkim

Laravel 5/6 package for signing outgoing messages with DKIM.

3163.1k](/packages/vitalybaev-laravel5-dkim)[firemultimedia/mautic-multi-captcha-bundle

This plugin brings Google's reCAPTCHA, hCaptcha, and Cloudflare Turnstile integration to mautic.

141.3k](/packages/firemultimedia-mautic-multi-captcha-bundle)

PHPackages © 2026

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