PHPackages                             particle-academy/laravel-fms - 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. [Utility &amp; Helpers](/categories/utility)
4. /
5. particle-academy/laravel-fms

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

particle-academy/laravel-fms
============================

Laravel Feature Management System (FMS) for product feature entitlements and usage tracking

v0.7.0(1mo ago)045511MITPHPPHP ^8.2CI passing

Since Jan 4Pushed 3w agoCompare

[ Source](https://github.com/Particle-Academy/laravel-feature-management-system)[ Packagist](https://packagist.org/packages/particle-academy/laravel-fms)[ RSS](/packages/particle-academy-laravel-fms/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (15)Versions (11)Used By (1)

[![Powered by Tynn](https://camo.githubusercontent.com/db5bfff78510a1a7471a9ad18c48da044e62a4bcd1661875963411643b84303e/68747470733a2f2f696d672e736869656c64732e696f2f656e64706f696e743f75726c3d687474707325334125324625324674796e6e2e61692532466f2532467061727469636c652d61636164656d792532466c61726176656c2d636174616c6f6725324662616467652e6a736f6e)](https://tynn.ai/o/particle-academy/laravel-catalog)

[![Fancy UI suite](art/fancy-ui.svg)](https://particle.academy)

Laravel Feature Management System (FMS)
=======================================

[](#laravel-feature-management-system-fms)

A standalone Laravel package for flexible feature access control and management. FMS provides simple, intuitive ways to control feature access using multiple strategies: Gates/Policies, config-based, registry-based, and database lookups.

Features
--------

[](#features)

- **Multiple Access Control Strategies**: Gates/Policies, config files, feature registry, or database
- **Boolean &amp; Resource Features**: Support for simple on/off features and metered resource features
- **Feature Groups**: Bundle features into reusable groups (Pro plan, Enterprise, AI Beta cohort, etc.) and assign them polymorphically to any model
- **Middleware Protection**: Protect routes based on feature access
- **Facade &amp; Helpers**: Clean API via facade and global helper functions
- **Standalone Package**: Zero dependencies on other packages
- **Configurable schema**: Override the `feature_usages` / `subscriptions` / `product_features` table names without forking
- **Laravel 13 Compatible**: Built for Laravel 11+, 12+, and 13+

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

[](#installation)

```
composer require particle-academy/laravel-fms
```

The package will auto-discover and register its service provider.

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

[](#configuration)

Publish the configuration file:

```
php artisan vendor:publish --tag=fms-config
```

Define your features in `config/fms.php`:

```
return [
    'features' => [
        // Simple boolean feature
        'use-mcp' => [
            'name' => 'Use MCP',
            'description' => 'Access to MCP-powered assistants and tools.',
            'type' => 'boolean',
            'enabled' => true, // or callable: fn($user) => $user->isPremium()
        ],

        // Resource feature with limit
        'ai-tokens' => [
            'name' => 'AI Tokens',
            'description' => 'Metered AI token usage per billing period.',
            'type' => 'resource',
            'limit' => 10000, // or callable
            'usage' => fn($user) => $user->getTokenUsage(), // optional
        ],
    ],
];
```

Usage
-----

[](#usage)

### Using the Facade

[](#using-the-facade)

```
use ParticleAcademy\Fms\Facades\FMS;

// Check if feature is accessible
if (FMS::canAccess('use-mcp')) {
    // Feature is enabled
}

// Check if feature is enabled (alias)
if (FMS::isEnabled('use-mcp')) {
    // Feature is enabled
}

// Check if user has feature
if (FMS::hasFeature('use-mcp', $user)) {
    // User has access
}

// Get remaining quantity for resource features
$remaining = FMS::remaining('ai-tokens', $user);
if ($remaining > 0) {
    // Allow action
}

// Get all enabled features
$enabled = FMS::enabled($user);
```

### Using Helper Functions

[](#using-helper-functions)

```
// Get feature manager or check feature
if (feature('use-mcp')) {
    // Feature is enabled
}

// Check feature access
if (can_access_feature('use-mcp', $user)) {
    // User has access
}

// Check if feature is enabled
if (feature_enabled('use-mcp')) {
    // Feature is enabled
}

// Get remaining quantity
$remaining = feature_remaining('ai-tokens', $user);

// Get all enabled features
$enabled = enabled_features($user);
```

### Feature Groups

[](#feature-groups)

Big apps grow lots of features, and remembering which to flip on for which plan/tier becomes tedious. **Feature groups** bundle features under a single key, and any model that uses the `HasFeatureGroups` trait can be polymorphically assigned to one or more groups.

#### Define groups in `config/fms.php`

[](#define-groups-in-configfmsphp)

```
'groups' => [
    'pro-plan' => [
        'name' => 'Pro Plan',
        'features' => ['use-mcp', 'ai-tokens', 'team-sharing'],
        'overrides' => [
            'ai-tokens' => ['limit' => 50000],   // lift the base limit
        ],
    ],

    'enterprise' => [
        'name' => 'Enterprise',
        'extends' => ['pro-plan'],               // one level deep
        'features' => ['sso', 'audit-log'],
        'overrides' => [
            'ai-tokens' => ['limit' => 250000],
        ],
    ],

    'ai-beta' => [
        'name' => 'AI Beta cohort',
        'features' => ['experimental-llm'],
        'enabled' => fn ($user) => $user?->in_ai_beta === true,   // callable gate, no pivot needed
    ],
],
```

#### Make a model assignable

[](#make-a-model-assignable)

```
use ParticleAcademy\Fms\Concerns\HasFeatureGroups;

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

#### Assign + check

[](#assign--check)

```
$user->attachFeatureGroup('pro-plan');
$user->attachFeatureGroup('enterprise');

FMS::canAccess('use-mcp', $user);     // true (via pro-plan)
FMS::remaining('ai-tokens', $user);    // 250000 (max of all enabled groups)
FMS::enabledGroupsFor($user);          // ['pro-plan', 'enterprise']
FMS::explain('use-mcp', $user);        // ['source' => 'group', 'detail' => ['groups' => ['pro-plan', 'enterprise'], ...]]
```

#### Resolution semantics

[](#resolution-semantics)

A feature is enabled if **any** of these returns true:

1. `Gate::has($feature)` is defined and grants access (Gate is authoritative — also the only path that can DENY)
2. The feature's registry definition has `enabled: true` or its `check` returns true
3. **Any enabled feature group containing this feature** (NEW)
4. The config file has `enabled: true` for this feature

For resource features, the `limit` is the MAX across:

- All enabled groups providing an override for this feature
- The base feature's own limit

So a "Pro" plan with `ai-tokens.limit = 50000` lifts the base config's `1000` to `50000` for users in that group, while users not in any group still see the base limit.

#### Catalog integration

[](#catalog-integration)

`LaravelCatalog\Models\Product` uses `HasFeatureGroups`, so a Stripe Product can be tagged with feature groups directly:

```
$product->attachFeatureGroup('pro-plan');
```

The implication: a user subscribed to that product gets every feature in the group (assuming your subscription resolution layer reads `Product::featureGroups()`).

#### Debugging tools

[](#debugging-tools)

Two artisan commands make "why is this on/off?" trivial:

```
php artisan fms:groups                          # list all registered groups
php artisan fms:groups pro-plan                 # inspect one group (resolved features + overrides)
php artisan fms:resolve 42                      # explain every feature for User #42
php artisan fms:resolve 42 --feature=use-mcp    # explain a single feature
php artisan fms:resolve org-7 --type=App\\Models\\Org   # any HasFeatureGroups model
```

`fms:resolve` walks every feature and reports which source resolved it (gate / registry / group / config / none) plus the structured detail (matching groups, limit overrides, etc.).

### Using Middleware

[](#using-middleware)

Protect routes with feature requirements:

```
use ParticleAcademy\Fms\Http\Middleware\RequireFeature;

Route::middleware(['auth', RequireFeature::class . ':use-mcp'])->group(function () {
    Route::get('/mcp', [McpController::class, 'index']);
});

// Multiple features (OR logic - user needs at least one)
Route::middleware(['auth', RequireFeature::class . ':feature1,feature2'])->group(function () {
    // Route protected by feature1 OR feature2
});
```

### Using Gates/Policies

[](#using-gatespolicies)

FMS automatically checks Laravel Gates if they exist:

```
// In AuthServiceProvider
Gate::define('use-mcp', function ($user) {
    return $user->subscription->plan === 'pro';
});

// FMS will automatically use this gate
if (FMS::canAccess('use-mcp')) {
    // Gate check passed
}
```

### Feature Registry

[](#feature-registry)

Register features programmatically:

```
use ParticleAcademy\Fms\Services\FmsFeatureRegistry;

app(FmsFeatureRegistry::class)->register('custom-feature', [
    'name' => 'Custom Feature',
    'type' => 'boolean',
    'enabled' => fn($user) => $user->hasPermission('custom'),
]);
```

Access Control Strategies
-------------------------

[](#access-control-strategies)

FMS checks features in this order:

1. **Pre-strategies** (app-registered) - Run before Gate; first non-null verdict wins
2. **Gates/Policies** - If a Gate exists with the feature name, it's checked first
3. **Feature Registry** - Checks registered features via `FmsFeatureRegistry`
4. **Feature Groups** - Any enabled group containing the feature flips it on
5. **Config File** - Checks `config/fms.features.{feature}`
6. **Database** - If `FeatureUsage` model exists, checks database (extensible)

### Pre-strategies (v0.6.0+)

[](#pre-strategies-v060)

When you need an external system — a billing service, a remote entitlements provider, a feature flag platform — to be **authoritative**about access (even over a Gate), register a *pre-strategy*. Strategies receive `(feature, user, context)` and return `?bool`:

- `true` — granted, no further checks
- `false` — denied, no further checks
- `null` — "I don't know", fall through to the next strategy / chain

```
use ParticleAcademy\Fms\Services\FeatureManager;

// In an app service provider's boot():
app(FeatureManager::class)->registerPreStrategy('subscription', function ($feature, $user, $context): ?bool {
    if (! $user) return null;
    $sub = app(BillingService::class)->subscriptionFor($user);
    if (! $sub) return null;                       // no subscription -> fall through
    return $sub->allowsFeature($feature);          // authoritative when subscription exists
});
```

Strategies run in registration order, and `explain()` will report `source: 'pre-strategy'` with the strategy `name` so devtools can show *"blocked by subscription"* etc.

For resource features, register a `?int` counterpart that answers `remaining()`:

```
app(FeatureManager::class)->registerPreRemainingStrategy('subscription-quota', function ($feature, $user, $context): ?int {
    $sub = app(BillingService::class)->subscriptionFor($user);
    return $sub?->remainingFor($feature);          // null falls through
});
```

Re-registering the same name replaces the strategy. `unregisterPreStrategy('subscription')` and `unregisterPreRemainingStrategy('subscription-quota')` undo it (useful in tests).

Resource Features
-----------------

[](#resource-features)

Resource features support metered usage:

```
'api-calls' => [
    'type' => 'resource',
    'limit' => 1000,
    'usage' => fn($user) => $user->apiCalls()->thisMonth()->count(),
    'remaining' => fn($user) => 1000 - $user->apiCalls()->thisMonth()->count(), // optional
],
```

Custom table names (v0.7.0+)
----------------------------

[](#custom-table-names-v070)

The `feature_usages` table and the two tables its foreign keys point at are config-driven, so you don't have to fork the package or hand-edit a published migration when your schema differs. Override any of them in `config/fms.php`:

```
'tables' => [
    'feature_usages'   => 'fms_feature_usages',        // prefixed schema
    'subscriptions'    => 'subscriptions',             // your billing table
    'product_features' => 'catalog_product_features',  // laravel-catalog's table
],
```

Both the `FeatureUsage` model (`getTable()`) and the create migration read these values, so model and schema stay in sync from one change.

The create migration also **self-skips** (no error) when:

- the `feature_usages` table already exists (e.g. created by an older fork under a different name), or
- either FK target table (`subscriptions` / `product_features`) is missing at apply time.

The second guard means the migration can sit harmlessly early in your chronological migration order — if your subscription / product-feature tables are created later, FMS just defers, and you can build the real usages table in your own migration if you prefer.

Requirements
------------

[](#requirements)

- PHP 8.2+
- Laravel 11+, 12+, or 13+

Testing
-------

[](#testing)

Run tests using Pest:

```
pkg laravel-fms php vendor/bin/pest
```

Integration with Laravel Catalog
--------------------------------

[](#integration-with-laravel-catalog)

FMS integrates seamlessly with [Laravel Catalog](https://github.com/particle-academy/laravel-catalog) for feature-based product management. When both packages are installed, Catalog automatically configures FMS to use Catalog's `ProductFeature` model.

### Quick Integration Setup

[](#quick-integration-setup)

1. **Install both packages**:

```
composer require particle-academy/laravel-fms
composer require particle-academy/laravel-catalog
```

2. **Configure FMS features** in `config/fms.php`:

```
return [
    'features' => [
        'manage-products' => [
            'name' => 'Manage Products',
            'type' => 'boolean',
            'enabled' => fn($user) => $user->hasRole('admin'),
        ],
    ],
];
```

3. **Use FMS in your Catalog controllers**:

```
use ParticleAcademy\Fms\Facades\FMS;
use LaravelCatalog\Models\Product;

class ProductController extends Controller
{
    public function store(Request $request)
    {
        if (!FMS::canAccess('manage-products')) {
            abort(403);
        }

        $product = Product::create($request->validated());
        // ...
    }
}
```

### Product Features Integration

[](#product-features-integration)

Catalog's `ProductFeature` model works with FMS to provide feature-based access control:

```
use LaravelCatalog\Models\Product;
use LaravelCatalog\Models\ProductFeature;

// Attach features to products
$product = Product::find($productId);
$feature = ProductFeature::where('key', 'advanced-editing')->first();

$product->productFeatures()->attach($feature->id, [
    'enabled' => true,
    'included_quantity' => 100,
]);

// Check feature access for user's subscription
if (FMS::canAccess('advanced-editing', $user)) {
    // User has access via their subscription
}
```

### Subscription-Based Feature Access

[](#subscription-based-feature-access)

When integrated with Catalog, you can check feature access based on user subscriptions:

```
use ParticleAcademy\Fms\Facades\FMS;

// Check if user's subscription includes a feature
$user = auth()->user();
$subscription = $user->subscriptions()->active()->first();

if ($subscription) {
    $product = $subscription->product();

    // Check if product has feature and user has access
    foreach ($product->productFeatures as $feature) {
        if (FMS::canAccess($feature->key, $user)) {
            // Feature is available
        }
    }
}
```

### Example: Feature-Gated Product Actions

[](#example-feature-gated-product-actions)

```
use ParticleAcademy\Fms\Facades\FMS;
use LaravelCatalog\Facades\Catalog;

class ProductController extends Controller
{
    public function sync(Product $product)
    {
        // Check if user can sync products
        if (!FMS::canAccess('sync-products', auth()->user())) {
            abort(403, 'You do not have permission to sync products.');
        }

        Catalog::syncProductAndPrices($product);

        return redirect()->back()->with('success', 'Product synced.');
    }

    public function create()
    {
        // Check remaining product creations
        $remaining = FMS::remaining('product-creations', auth()->user());

        if ($remaining back()
                ->with('error', 'Product creation limit reached.');
        }

        return view('admin.products.create');
    }
}
```

### Protecting Catalog Routes

[](#protecting-catalog-routes)

Use FMS middleware to protect catalog admin routes:

```
use ParticleAcademy\Fms\Http\Middleware\RequireFeature;

Route::prefix('admin')->middleware([
    'auth',
    RequireFeature::class . ':manage-products'
])->group(function () {
    Route::resource('products', ProductController::class);
});
```

For more detailed integration examples and patterns, see [INTEGRATION.md](INTEGRATION.md).

License
-------

[](#license)

MIT

---

⭐ Star Fancy UI
---------------

[](#-star-fancy-ui)

If this package is useful to you, a quick ⭐ on the repo really helps us build a better kit. Thank you!

###  Health Score

44

—

FairBetter than 90% of packages

Maintenance94

Actively maintained with recent releases

Popularity19

Limited adoption so far

Community12

Small or concentrated contributor base

Maturity44

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

Recently: every ~5 days

Total

10

Last Release

38d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/461446?v=4)[Wish Born](/maintainers/wishborn)[@wishborn](https://github.com/wishborn)

---

Top Contributors

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

---

Tags

laravelfeature-flagsfeature managemententitlements

###  Code Quality

TestsPest

### Embed Badge

![Health badge](/badges/particle-academy-laravel-fms/health.svg)

```
[![Health](https://phpackages.com/badges/particle-academy-laravel-fms/health.svg)](https://phpackages.com/packages/particle-academy-laravel-fms)
```

###  Alternatives

[laravel/octane

Supercharge your Laravel application's performance.

4.0k26.6M223](/packages/laravel-octane)[statamic/cms

The Statamic CMS Core Package

4.8k3.6M992](/packages/statamic-cms)[bagisto/bagisto

Bagisto Laravel E-Commerce

27.6k172.1k9](/packages/bagisto-bagisto)[laravel/nightwatch

The official Laravel Nightwatch package.

36210.1M36](/packages/laravel-nightwatch)[firefly-iii/data-importer

Firefly III Data Import Tool.

8035.8k](/packages/firefly-iii-data-importer)[markwalet/nova-modal-response

A Laravel Nova asset for Modal responses on an action.

17878.9k](/packages/markwalet-nova-modal-response)

PHPackages © 2026

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