PHPackages                             whilesmart/eloquent-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. whilesmart/eloquent-entitlements

ActiveLibrary

whilesmart/eloquent-entitlements
================================

Provider-neutral feature gating, plan limits and metered usage for Laravel, scoped per polymorphic owner. Ships an allow-all default so self-hosting stays free; hosts rebind to enforce.

2.0.0(1mo ago)0188↑1133.3%1MITPHPPHP ^8.2CI passing

Since Jul 17Pushed 1mo agoCompare

[ Source](https://github.com/whilesmartphp/eloquent-entitlements)[ Packagist](https://packagist.org/packages/whilesmart/eloquent-entitlements)[ RSS](/packages/whilesmart-eloquent-entitlements/feed)WikiDiscussions dev Synced 1w ago

READMEChangelog (2)Dependencies (10)Versions (8)Used By (1)

whilesmart/eloquent-entitlements
================================

[](#whilesmarteloquent-entitlements)

A provider-neutral entitlements layer for Laravel: feature flags, plan limits and metered usage, all scoped to a polymorphic owner (a workspace, organization, user) through [`whilesmart/eloquent-owner-access`](https://github.com/whilesmartphp/eloquent-owner-access).

The package ships an allow-all default, so a self-host stays fully unlocked and free. A host that wants to charge rebinds one interface to enforce plans. No payment SDK is imported anywhere; billing is a seam the host fills.

Install
-------

[](#install)

```
composer require whilesmart/eloquent-entitlements
php artisan migrate

```

Routes register automatically under the `api` prefix with `auth:sanctum`. Set `ENTITLEMENTS_REGISTER_ROUTES=false` to mount them yourself.

The gating seam
---------------

[](#the-gating-seam)

Everything gates through one contract, `Whilesmart\Entitlements\Contracts\Entitlements`:

```
$entitlements = app(Entitlements::class);

$entitlements->allows($owner, 'reports');      // bool
$entitlements->check($owner, 'reports');       // AccessResult: allowed + why not
$entitlements->limit($owner, 'seats');         // ?int, null = unlimited
$entitlements->remaining($owner, 'api_calls'); // int|float
$entitlements->consume($owner, 'api_calls', 1);
```

`check()` returns an `AccessResult` (`allowed`, `feature`, `reason`, `limit`) so a denial can say why, not just no. The reason is a stable code: `no_owner`, `no_plan`, `feature_not_in_plan`.

Gating a route
--------------

[](#gating-a-route)

Gate a route or group on a feature with the `feature` middleware. A denied request gets a 402 carrying the feature and reason:

```
Route::middleware('feature:reports')->group(function () {
    // ...
});
```

The middleware needs to know which model owns the entitlement. The default `OwnerResolver`uses the authenticated user; a host that gates by workspace or organization binds its own:

```
use Whilesmart\Entitlements\Contracts\OwnerResolver;

$this->app->bind(OwnerResolver::class, WorkspaceOwnerResolver::class);
```

The default binding, `AllowAllEntitlements`, allows every feature, imposes no limit and never meters. To enforce plans, rebind it to `PlanEntitlements` in a service provider:

```
$this->app->bind(Entitlements::class, PlanEntitlements::class);
```

Owning model
------------

[](#owning-model)

Add the trait to whatever owns entitlements:

```
use Whilesmart\Entitlements\Traits\HasEntitlements;

class Workspace extends Model
{
    use HasEntitlements;
}

$workspace->entitledTo('reports'); // delegates to the bound Entitlements
$workspace->limitFor('seats');
$workspace->activeSubscription();
```

Model layer
-----------

[](#model-layer)

- `Plan`: a `key`, `features` (map of feature to bool), `limits` (map to int or null), `meters` (map to a monthly allowance), and an optional `provider_price_id`.
- `Subscription`: the owner's plan and lifecycle status (`active`, `trialing`, `past_due`, `canceled`, `comped`). The `active()` scope selects entitling subscriptions.
- `Entitlement`: a per-owner override for a single feature, limit or meter. Overrides win over the plan, so an owner can carry a bespoke cap without one.
- `Coupon` / `CouponRedemption`: comp, plan-grant and discount coupons, with every redemption recorded for audit.

`PlanEntitlements` resolves the four contract methods: overrides first, then whatever plan the bound `PlanSource` reports for the owner.

Where plans come from
---------------------

[](#where-plans-come-from)

`PlanEntitlements` holds the resolution rules (an override wins; a comp is unlimited; an absent meter allowance is unlimited) but not the storage. Finding the owner's plan is a second seam, `Whilesmart\Entitlements\Contracts\PlanSource`:

```
public function resolve(?Model $owner): ?ResolvedPlan;
```

`ResolvedPlan` is a plain value object (`features`, `limits`, `meters` maps, read with `data_get`, so keys may nest). Return `null` when nothing entitles the owner, or `ResolvedPlan::unlimited()` for an entitling subscription with no plan behind it.

The default, `EloquentPlanSource`, reads the models above, and resolving the owner's subscription is its business rather than the caller's. So an app that models subscriptions differently, keeps plans in config, or asks a remote service, replaces only this and keeps the rules:

```
$this->app->bind(PlanSource::class, MyConfigPlanSource::class);
```

The models above are then just the default source's storage; a host that binds its own leaves those tables empty.

Metering
--------

[](#metering)

`remaining` and `consume` delegate to a `UsageMeter` the host binds. The default, `NullUsageMeter`, reports unlimited and records nothing, so the package never owns a usage table. Bind your own to count real usage:

```
$this->app->bind(UsageMeter::class, MyRedisMeter::class);
```

Coupons
-------

[](#coupons)

```
app(CouponService::class)->redeem($coupon, $owner);
```

- `comp` / `plan_grant` create a `comped` subscription against the granted plan. A comp with `bypass` (or no plan) yields an unlimited comped subscription.
- `percent_off` / `fixed_off` record a per-owner entitlement carrying the discount for the host's billing adapter to apply at checkout. No charge happens in the package.

Redemption is rejected when the coupon is expired or has reached `max_redemptions`.

Billing
-------

[](#billing)

`Whilesmart\Entitlements\Contracts\BillingProvider` is the checkout seam. The default, `NullBillingProvider`, refuses checkout and no-ops the rest. A host binds a real adapter (Stripe, Paddle, etc.); the package stays provider-neutral and imports no payment SDK.

Endpoints
---------

[](#endpoints)

MethodPathPurposeGET`/api/entitlements`Owner's current snapshot (plan, features, limits)POST`/api/entitlements/redeem-coupon`Redeem a coupon by `code` for the ownerBoth are owner-scoped through owner-access: a caller can only read or redeem into an owner the bound authorizer permits.

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

[](#configuration)

`config/entitlements.php` exposes `register_routes`, `register_migrations`, `route_prefix`, `route_middleware`, the five table names, and a `models` map so any model can be swapped for a host subclass.

An app that binds its own `PlanSource` (and does not use this package's plans, subscriptions or coupons) can set `register_migrations` to `false` to take the gating contract without the tables.

###  Health Score

44

—

FairBetter than 90% of packages

Maintenance91

Actively maintained with recent releases

Popularity15

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity51

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 ~1 days

Total

2

Last Release

44d ago

Major Versions

1.0.0 → 2.0.02026-07-19

### Community

Maintainers

![](https://www.gravatar.com/avatar/a1ca6f6e01ecbfe6640ff410c4f0321054fb48d05a5f843c2b89887b90369bcd?d=identicon)[whilesmart](/maintainers/whilesmart)

---

Top Contributors

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

###  Code Quality

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/whilesmart-eloquent-entitlements/health.svg)

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

###  Alternatives

[backpack/crud

Quickly build admin interfaces using Laravel, Bootstrap and JavaScript.

3.4k3.8M231](/packages/backpack-crud)[unopim/unopim

UnoPim Laravel PIM

10.9k2.6k](/packages/unopim-unopim)[statamic-rad-pack/runway

Eloquently manage your database models in Statamic.

138249.0k8](/packages/statamic-rad-pack-runway)[duncanmcclean/statamic-cargo

Comprehensive e-commerce addon for Statamic. Build bespoke e-commerce sites without the complexity.

3622.8k](/packages/duncanmcclean-statamic-cargo)[ecotone/laravel

Ecotone for Laravel — CQRS, Event Sourcing, Sagas, Durable Workflows, and Outbox on top of Laravel Queue, via PHP attributes.

21336.4k4](/packages/ecotone-laravel)

PHPackages © 2026

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