Download the PHP package saadmajeed/laravel-entitlements without Composer
On this page you can find all versions of the php package saadmajeed/laravel-entitlements. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download saadmajeed/laravel-entitlements
More information about saadmajeed/laravel-entitlements
Files in saadmajeed/laravel-entitlements
Package laravel-entitlements
Short Description SaaS feature engine — decisions about access, limits, and usage for Laravel.
License MIT
Informations about the package laravel-entitlements
Laravel Entitlements
A feature/entitlement engine for Laravel 11, 12, and 13. Define plans, features, limits, metered usage, and conditional access rules — then check them anywhere in your app.
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 — resolution cache with configurable TTL (defaults to the array store; set
ENTITLEMENTS_CACHE_STOREfor production) - Fluent definition API, Facade, Middleware, Blade directive
Installation
Publish the config and migrations:
Or run everything in one step:
Configuration
See config/entitlements.php for all options:
| Key | Default | Description |
|---|---|---|
cache.store |
array |
Cache store for entitlement decisions (use redis/database in production) |
cache.ttl |
3600 |
Cache TTL in seconds |
cache.prefix |
ent: |
Cache key prefix |
usage.enabled |
true |
Enable synchronous, transactional usage tracking |
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 command |
Quick Start
1. Add the trait to your subject
The HasPlan trait adds a plan() relationship and convenience methods.
2. Create a plan
Assign the plan to a user:
3. Create entitlements
4. Assign values to a plan
4b. Define a plan fluently (recommended)
The manual steps above are fine, but the fluent Entitlements facade hides the
pivot-format details and keeps your seeding idempotent:
You can also pass a callback instead of chaining:
Entitlements::plan() finds-or-creates the Plan and each referenced
Entitlement, then updates the per-plan pivot value on every run — so it is
safe to call from a seeder over and over. A full example plan tree lives in
database/seeders/EntitlementsSeeder.php.
Removing entitlements
To revoke an entitlement from a plan, detach it (the global Entitlement row
is kept so other plans can still use it):
5. Check entitlements
value() is a convenience getter for the resolved value of why($key)->value.
consume() is an alias of use() for recording metered/limit usage.
Entitlement Types
Boolean
Simple on/off feature flag. The pivot value should be ['enabled' => true] or ['enabled' => false].
Limit
A numeric cap. The value is a number or ['max' => N].
Metered
Countable usage that resets on a period (daily, monthly, yearly).
Soft limits
Set soft_limit: true in the entitlement's metadata to allow usage beyond the limit (fires event instead of throwing):
An LimitReached event is fired, but the use() call succeeds.
Time-based
A feature that expires at a specific date.
If expired, an ExpiredEntitlementException is thrown.
Conditional
Access depends on subject attributes.
Supported operators: =, ==, ===, !=, !==, <>, >, >=, <, <=, in, not_in, contains.
Security: by default conditional rules may read any subject attribute. To restrict which attributes rules may reference, set
conditional.allowed_fieldsinconfig/entitlements.php. When the allowlist is non-empty, any rule referencing a field outside it causes that conditional entitlement to be denied ("deny unknown"). You can also provide a customconditional.attribute_resolver(a class implementingSaadMajeed\Entitlements\Contracts\AttributeResolver) to control how attribute values are read.
Config
A generic configuration value (always allowed, returns the raw value).
Plan Inheritance
Plans can have a parent. Child plans inherit entitlement values from the parent unless overridden.
The resolution pipeline checks:
- Per-subject override (if any)
- Expiry (for time-based)
- Conditional rules
- Subject's direct plan value
- Parent plan chain (walk up)
- Entitlement's default value
Overrides
Override an entitlement for a specific subject:
Overrides have the highest priority in the resolution pipeline. They can optionally expire.
Caching
Entitlement decisions are cached per subject per key. The cache is automatically invalidated when:
- An override is created, updated, or deleted (
OverrideAppliedevent) - A subject's plan changes (
PlanChangedevent) - Usage is recorded (
UsageRecordedevent)
Manually flush the cache:
Warm the cache:
Events
| Event | Payload | Fired when |
|---|---|---|
EntitlementChecked |
subject, entitlement, allowed | After any entitlement check |
LimitReached |
subject, entitlement, used, limit, soft | When a limit is hit |
OverrideApplied |
override | Override created/updated/deleted |
PlanChanged |
subject | Subject's plan changes |
UsageRecorded |
subject, entitlement, amount | Usage recorded |
Listeners are auto-registered to invalidate the cache on OverrideApplied, PlanChanged, and UsageRecorded.
Usage Tracking
Usage is recorded synchronously and atomically — no queue, no background jobs. When you call use(), a single database transaction:
- Acquires a per-entitlement named advisory lock (serializing concurrent consumers on MySQL/PostgreSQL),
- Inserts the
UsageRecordrow, - Reads and increments the
UsageSummaryfor the current period inside aSELECT ... FOR UPDATErow lock, - Checks the limit (hard limit rejects with
LimitExceededException; soft limit records but firesLimitReached), - Commits — and only then fires the
LimitReachedevent outside the transaction.
Because the read-modify-write of the summary happens under a lock within one transaction, there is no double-counting and no lost update even under high concurrency.
For real-time accuracy, you can query remaining usage directly:
Concurrency
Usage recording is safe under concurrency on MySQL and PostgreSQL, where it uses a named advisory lock plus SELECT ... FOR UPDATE row locking, with automatic retry on deadlock (up to 3 attempts). SQLite is single-writer, so it is also safe but does not exercise the lock paths. The package ships with a parallel-consumer test (tests/Concurrency) that is run against real MySQL/PostgreSQL in CI.
API Reference
Entitlement::for($subject)
Returns a cloned EntitlementManager scoped to the subject.
| Method | Returns | Description |
|---|---|---|
can(string $key) |
bool |
Check if an entitlement is allowed |
canMany(array $keys) |
array<string, bool> |
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 |
consume(string $key, float $amount = 1, array $metadata = []) |
Decision |
Alias of use() |
value(string $key) |
mixed |
Resolved value (why($key)->value) |
why(string $key) |
Explanation |
Get full resolution trace (debugging) |
flushCache(?string $key = null) |
void |
Clear cached decisions |
Entitlements facade (defining plans)
| Method | Returns | Description |
|---|---|---|
Entitlements::plan(string $slug, ?callable $callback = null) |
PlanBuilder |
Find-or-create a plan and define its entitlements fluently |
The returned PlanBuilder exposes feature(), limit(), meter(),
conditional(), time(), and parent() — all chainable and idempotent — plus
remove(), removeMany(), only() (keep only the given keys), and
withoutParent(), for revoking entitlements and clearing inheritance.
HasPlan trait methods
| Method | Returns | Description |
|---|---|---|
$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->consume(string $key, float $amount = 1) |
Decision |
Alias of use() |
$subject->value(string $key) |
mixed |
Resolved value |
$subject->why(string $key) |
Explanation |
Delegates to Entitlement::for($subject)->why() |
$subject->entitlement(string $key) |
FluentEntitlement |
Fluent API gateway |
Decision object
| Property | Type | Description |
|---|---|---|
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())
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
The subject argument accepts ModelClass:id syntax (e.g. User:1, App\Models\User:42).
entitlement:explain prints the same resolution trace as why() but in a
readable form — the final result (ALLOWED / DENIED), the source, plan, override,
expiry and usage details, and the ordered resolution steps. Pass --json to get
the raw Explanation as JSON (identical to entitlement:check).
Middleware
Protect routes with the entitlement middleware:
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
Requires the user to be authenticated.
Exceptions
| Exception | HTTP status | When thrown |
|---|---|---|
EntitlementNotFoundException |
n/a | Entitlement key not found in database |
ExpiredEntitlementException |
n/a | Time-based entitlement has expired |
LimitExceededException |
n/a | Hard limit exceeded (has key, limit, used, attempted) |
Extend your exception handler to map these to HTTP codes as needed:
Testing
The suite runs against SQLite by default:
Concurrency tests (parallel consumers, advisory/row locking) run against real MySQL / PostgreSQL and are isolated in their own process:
The CI matrix covers Laravel 11, 12, and 13 across MySQL 8.4 and PostgreSQL 16.
All versions of laravel-entitlements with dependencies
illuminate/support Version ^11.0|^12.0|^13.0
illuminate/database Version ^11.0|^12.0|^13.0
illuminate/container Version ^11.0|^12.0|^13.0
illuminate/cache Version ^11.0|^12.0|^13.0
illuminate/queue Version ^11.0|^12.0|^13.0