daacreators / pennant-manager
A Filament plugin for managing Laravel Pennant feature flags
Requires
- php: ^8.2
- filament/filament: ^4.0|^5.0
- illuminate/contracts: ^11.0|^12.0
- laravel/pennant: ^1.0|^2.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.0
- nunomaduro/collision: ^8.0
- orchestra/testbench: ^9.0|^10.0|^11.0
- pestphp/pest: ^3.7|^4.0
- pestphp/pest-plugin-arch: ^3.0|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
- rector/rector: ^2.0
- spatie/laravel-ray: ^1.26
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A Filament panel plugin for managing Laravel Pennant feature flags - toggle globally, per user or team, bulk import scopes, discover defined features, and track adoption - all without leaving your admin panel.
Screenshots
Requirements
- PHP 8.2+
- Laravel 11.x or 12.x
- Filament 4.x or 5.x
- Laravel Pennant (
laravel/pennant)
Installation
Install via Composer:
composer require daacreators/pennant-manager
If you haven't already, install Pennant and run its migration:
composer require laravel/pennant php artisan migrate
Publish the plugin config:
php artisan vendor:publish --tag="pennant-manager-config"
Optionally publish the views to customise them:
php artisan vendor:publish --tag="pennant-manager-views"
Setup
Register the plugin in your Filament panel provider:
use daacreators\PennantManager\PennantManagerPlugin; public function panel(Panel $panel): Panel { return $panel ->plugins([ PennantManagerPlugin::make(), ]); }
Configuration
The existing default store and value-resolution behavior remain unchanged. Optional settings in config/pennant-manager.php:
'store' => 'database', // named Pennant database store; honors its table and connection 'inherit_global' => false, // opt in to global fallback in FeatureValue::resolveValue()
Only database-backed stores are managed by this package. Use the same configured store when reading values through Pennant. The helper defaults to database even when the host's default Pennant store is different.
Upgrading to 1.1.0
No migrations or stored-value rewrites are required. Existing plugin registration, the default features table, class-string scope configuration, and JSON envelopes remain supported. Existing published configuration does not need to be overwritten; package defaults supply the new settings. Clear cached configuration after changing settings.
The following corrections can affect custom integrations:
- Registered feature policies now govern navigation and mutations. Scope management requires
viewAnyandupdate; creation/import usescreate, and deletion usesdelete. Without a policy, existing Filament panel-user defaults are retained. Restrict your host panel to trusted administrators. - Scope deletion is confined to the selected feature's non-global rows. Only configured scope models are loaded for display, and configured segment column restrictions are enforced server-side.
- Reapplying a rollout tops up to its target percentage instead of adding the same percentage again. Existing active and inactive overrides are preserved; other models' IDs no longer exclude candidates.
- Malformed schedules are skipped with a warning so other scheduled work can continue. Review command output/logs for skipped entries.
- Missing scoped values still resolve to
falseby default. Enableinherit_globalexplicitly for global fallback. Native JSON scalar variants now resolve consistently; booleans and the existing envelope retain their format. - New scopes use Pennant's serializer and model primary key. Keep your existing morph-map setting; changing it requires migrating existing scope keys in the host application.
- The scope page accepts a model or record ID during Livewire mounting. Custom subclasses that redeclare its public
recordproperty must use the compatibleFeature|int|stringunion; subclasses that inherit the property need no change.
The new store option selects a configured Pennant database store and its connection/table. The package does not migrate tables or move existing records when that option changes.
Laravel 11 remains a compatibility target, but its current resolved framework branch reports dependency advisories. CI reports those separately from compatibility tests. Use a patched dependency graph in production and audit the host application; passing package tests does not establish dependency security. Laravel 12 dependency audits remain blocking in CI.
// config/pennant-manager.php return [ 'navigation_group' => 'Settings', /* |-------------------------------------------------------------------------- | Scope Models |-------------------------------------------------------------------------- | Models that can be used as feature flag scopes. | Extended format lets you configure per-model search and display columns. */ 'scope_models' => [ 'User' => [ 'model' => \App\Models\User::class, 'search_column' => 'email', // column used when searching for a scope 'label_column' => 'name', // column shown in the scopes table 'segment_columns' => ['plan', 'role', 'country'], // optional - restricts Activate by Segment to these columns ], // 'Team' => [ // 'model' => \App\Models\Team::class, // 'search_column' => 'name', // ], ], // Global fallback search column - used when a scope_models entry is a plain class string 'scope_search_column' => 'email', /* |-------------------------------------------------------------------------- | Feature Discovery |-------------------------------------------------------------------------- | Paths scanned for Feature::define() calls (files or directories). | Scanned features are merged with the manual list in discovered_features. */ 'discovery_paths' => [ app_path('Providers/AppServiceProvider.php'), // app_path('Features'), ], // Manually list features here - useful for features defined dynamically // or inside packages that cannot be reliably scanned. 'discovered_features' => [ // 'some-dynamic-feature', ], ];
Features
Feature List
The main table shows all globally-scoped features. For each feature you can:
- Toggle it globally on or off - affects everyone who does not have an explicit scope row
- See how many per-scope overrides exist (individual users or teams)
- Open Settings for advanced configuration
- Navigate to Manage Scopes for per-user or per-team control
- Delete the feature entirely
Global Toggle vs Scopes
With pennant-manager.inherit_global enabled, the plugin's FeatureValue::resolveValue() helper uses the global value for a scope without an explicit row:
User checks feature
├── Has a scope row? → use that value (global is ignored)
└── No scope row? → fall back to the global toggle
An explicit scope row always wins, including false. Global inheritance is disabled by default to preserve earlier versions' behavior. You can keep the global toggle off while enabling selected scopes in either mode.
The package retains its existing JSON envelope (enabled, rich_value, schedule, and metadata). Read managed flags using FeatureValue::resolveValue() so those fields are interpreted correctly; native Pennant checks do not interpret this envelope or implement the helper's optional global fallback. For a model scope, pass \Laravel\Pennant\Feature::serializeScope($user) to the helper so custom keys and morph maps match.
Settings
Each feature has a Settings modal with three sections. All three can be configured together in a single save.
Schedule
Set a future date and time. The feature stays off until that moment. When the scheduled time arrives, the pennant:activate-scheduled command flips it on automatically.
Feature is OFF → schedule set for Jun 15 00:00 → command runs → feature turns ON
Variant Value
Instead of returning true or false, the feature returns a custom string.
Useful for A/B testing, UI themes, or API versioning.
Set the variant in Settings and read it in your code using the plugin helper:
use daacreators\PennantManager\Support\FeatureValue; $value = FeatureValue::resolveValue('theme'); // Returns the variant string if active → 'dark', 'v2', 'beta' // Returns true if active with no variant set // Returns false if the feature is inactive
You can also check a specific scope (e.g. a user):
$value = FeatureValue::resolveValue('theme', 'App\Models\User|42');
Percentage Rollout
Randomly activates the feature for a percentage of your users by creating individual scope rows for them. Only users who do not already have a scope row are eligible.
Apply immediately ON - scope rows are created the moment you save Settings.
Apply immediately OFF - the percentage and model are stored, and the rollout is applied by the scheduled command when the activation date arrives. Use this to combine a rollout with a launch date.
A typical launch flow:
Set 30% rollout + Jun 15 activation date + Apply immediately OFF
→ On Jun 15, the command picks 30% of users and activates for them automatically
Manage Scopes
Per-feature page for activating or removing the feature for individual users or teams.
Manual search - search by the configured column (e.g. email), select one or more records, and activate.
Bulk import - paste a list of values (one per line, matching the search column) to activate for many users at once without uploading a file.
Activate by Segment - activate for all records matching a column condition, e.g. all users where plan = pro or country = US. If segment_columns is configured for the model, a dropdown restricts which columns can be filtered on. Otherwise any column name can be entered freely.
The scopes table shows each activated record, their model type, and whether their scope is currently active or inactive.
Feature Discovery
Scans your configured discovery_paths for Feature::define() calls - both string-keyed and class-based - and merges results with any features listed manually in discovered_features. Opens a modal showing features found in code but not yet in the database, all pre-selected for one-click import.
// Both of these are detected automatically Feature::define('new-dashboard', fn (User $user) => false); Feature::define(BetaFeature::class, fn (User $user) => false);
Adoption Stats
A stats widget at the top of the feature list showing per-feature adoption - what percentage of each model type has that feature currently active.
Defining Features
Define features the standard Pennant way in your AppServiceProvider:
use Laravel\Pennant\Feature; public function boot(): void { Feature::define('new-dashboard', fn (User $user) => $user->isOnProPlan()); Feature::define(BetaFeature::class, fn (User $user) => false); }
After defining features, open the Discover Features action in the panel to import them into the database.
Scheduling
To activate scheduled features and apply deferred rollouts automatically, add the command to your scheduler:
// routes/console.php use Illuminate\Support\Facades\Schedule; Schedule::command('pennant:activate-scheduled')->everyMinute();
You can also run it manually at any time:
php artisan pennant:activate-scheduled
Support the Project
If this package saves you time or helps your business, consider supporting its development.
Contributing
Please see CONTRIBUTING for details.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
License
The MIT License (MIT). Please see License File for more information.


