Skip to content
Pro Analyzer — Available with ShieldCI Pro

Pennant Configuration Analyzer ​

Analyzer IDCategorySeverityTime To Fix
pennant-config🏅 Best PracticesMedium10 minutes

What This Checks ​

Validates Laravel Pennant feature flag configuration, driver settings, and usage patterns. Checks for:

  • Unpublished config/pennant.php - driver configuration cannot be verified
  • Explicit use of the array driver - feature flag state is not persisted between requests
  • Stale feature flag references - flags checked via Feature::active(), inactive(), when(), unless(), value(), and bulk methods (allAreActive(), someAreActive(), allAreInactive(), someAreInactive()) that have no corresponding definition
  • Feature definitions without a type hint on the scope parameter (including PHP 8 union types such as User|null)
  • Feature definitions with empty closures or bare return; statements - no default value when the store is unavailable
  • Class-based features in app/Features/ (with a resolve() method) are recognised as definitions, including those using the #[Name('...')] attribute

Severity note: Missing scope type hints raise a Low severity issue; all other findings are Medium.

Why It Matters ​

  • State Persistence: The array driver resets all feature flag state on every request, making gradual rollouts and A/B tests meaningless in production
  • Auditability: Without a published config file the active driver cannot be reviewed or controlled via source code
  • Stale Flags: Feature checks without a matching definition always resolve to false, permanently hiding features with no visible error
  • Type Safety: Unvalidated scope parameters can cause runtime errors when the wrong scope type is passed to a feature closure
  • Reliability: Feature definitions that return nothing behave unpredictably when the flag store is unavailable

How to Fix ​

Quick Fix (2 minutes) ​

Publish the Pennant configuration and migration files, then run the migrations:

bash
php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider"
php artisan migrate

Proper Fix (10 minutes) ​

1. Use the database driver for production:

php
// config/pennant.php
'default' => env('PENNANT_STORE', 'database'),

'stores' => [
    'database' => [
        'driver' => 'database',
        'connection' => null,
        'table' => 'features',
    ],
],

2. Define all referenced feature flags:

Before (❌):

php
// Flag is checked but never defined
if (Feature::active('new-dashboard')) { ... }

After (✅) - closure-based:

php
// app/Providers/AppServiceProvider.php
Feature::define('new-dashboard', function (User $scope): bool {
    return $scope->is_beta_tester;
});

After (✅) - class-based:

php
// app/Features/NewDashboard.php
class NewDashboard
{
    public function resolve(User $scope): bool
    {
        return $scope->is_beta_tester;
    }
}

3. Add type hints to scope parameters (nullable union types are also valid):

Before (❌):

php
Feature::define('guest-banner', function ($scope) {
    return $scope === null;
});

After (✅):

php
Feature::define('guest-banner', function (User|null $scope): bool {
    return $scope === null;
});

4. Provide a return value in every feature definition:

Before (❌):

php
Feature::define('maintenance-banner', function (User $scope) {});

After (✅):

php
Feature::define('maintenance-banner', function (User $scope): bool {
    return false; // safe default when store is unavailable
});

5. Clean up stale flags:

bash
# Remove stored values for a specific feature
php artisan pennant:purge new-dashboard

# Purge all stored feature values
php artisan pennant:purge

References ​