Skip to main content

Laravel Integration

Learn how to use Toggly Feature Management in Laravel applications.

Installation​

Install the Laravel integration (pulls core automatically):

composer require toggly/laravel

Laravel package auto-discovery registers Toggly\Laravel\ServiceProvider and the Toggly facade. If auto-discovery is disabled, register them manually in config/app.php.

Compatibility​

Laravel 8.0 or later remains supported. Current packed-package host checks use Laravel 10.3.3, 11.6.1, 12.12.2, and 13.10.1. PHP 8.5 is supported; use a PHP version that your Laravel release supports. The package continues to retain its PHP 7.4 declared floor for compatible retained hosts.

The SDK needs a PSR-18 HTTP client and PSR-17 request factory. Bind your application's chosen implementations in Laravel's container before resolving FeatureManager; the SDK does not select an HTTP implementation for you.

Configuration​

Publish the configuration file:

php artisan vendor:publish --tag=toggly-config

This creates config/toggly.php. Configure your settings in .env:

TOGGLY_APP_KEY=your-app-key
TOGGLY_ENVIRONMENT=Production
TOGGLY_BASE_URL=https://definitions.toggly.io
TOGGLY_USE_SIGNED_DEFINITIONS=true
TOGGLY_REFRESH_INTERVAL=300

Using the Facade​

The Toggly facade provides easy access to feature management:

use Toggly\Laravel\Facades\Toggly;

if (Toggly::isEnabled('my-feature')) {
// Feature is enabled
}

Dependency Injection​

Inject FeatureManager directly into your controllers or services:

use Toggly\FeatureManagement\Core\FeatureManager;

class MyController extends Controller
{
private FeatureManager $featureManager;

public function __construct(FeatureManager $featureManager)
{
$this->featureManager = $featureManager;
}

public function index()
{
if ($this->featureManager->isEnabled('new-dashboard')) {
return view('dashboard.new');
}

return view('dashboard.old');
}
}

Middleware​

Route-Based Middleware​

Use the FeatureGateMiddleware to protect routes based on feature flags:

Register Middleware​

For Laravel 11+ (in bootstrap/app.php):

->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'feature' => \Toggly\Laravel\Middleware\FeatureGateMiddleware::class,
]);
})

For Laravel 10 and below (in app/Http/Kernel.php):

protected $routeMiddleware = [
// ... other middleware
'feature' => \Toggly\Laravel\Middleware\FeatureGateMiddleware::class,
];

Use in Routes​

// routes/web.php
Route::middleware(['feature:my-feature'])->group(function () {
Route::get('/new-feature', [NewFeatureController::class, 'index']);
});

Middleware Options​

FeatureGateMiddleware takes the feature name and an optional redirect URL. If the feature is off and no redirect is given, it abort(404).

// Abort with 404 if feature is disabled (default)
Route::middleware(['feature:my-feature'])->group(function () {
// ...
});

// Redirect if feature is disabled (second middleware parameter = URL)
Route::middleware(['feature:my-feature,/old-page'])->group(function () {
// ...
});

Attribute-Based Middleware​

For a more native Laravel experience, use PHP 8 attributes with the FeatureGateAttributeMiddleware (see Native Laravel Patterns below).

Task Scheduling​

Set up scheduled tasks to refresh features and send statistics:

In app/Console/Kernel.php:

protected function schedule(Schedule $schedule)
{
// Refresh feature definitions every 5 minutes
$schedule->call(function () {
app(\Toggly\FeatureManagement\Core\FeatureProvider::class)->refreshFeatures();
})->everyFiveMinutes();

// Send usage statistics every minute
$schedule->call(function () {
app(\Toggly\FeatureManagement\Core\UsageStatsProvider::class)->sendStats();
})->everyMinute();

// Send metrics every minute
$schedule->call(function () {
app(\Toggly\FeatureManagement\Core\MetricsService::class)->sendMetrics();
})->everyMinute();
}

State Change Handlers​

Register callbacks to be notified when features change state:

use Toggly\FeatureManagement\Core\FeatureStateService;

$featureStateService = app(FeatureStateService::class);

$featureStateService->whenFeatureTurnsOn('my-feature', function () {
// Feature was enabled
Log::info('My feature was enabled!');
});

$featureStateService->whenFeatureTurnsOff('my-feature', function () {
// Feature was disabled
Log::info('My feature was disabled!');
});

Custom Metrics​

Record custom metrics for experiments:

use Toggly\FeatureManagement\Core\MetricsService;

$metricsService = app(MetricsService::class);

// Record a measurement
$metricsService->measure('checkout-time', 2.5);
$metricsService->observe('button-click', 1);
$metricsService->incrementCounter('page-views', 1);

Feature evaluation context (core path)​

Laravel helpers (Toggly facade, Blade, middleware) evaluate through PHP core FeatureManager. Segment and UserClaims filters are Local in core — not via the old Laravel filter classes.

Blade and middleware derive legacy identity (user id), groups, and IP when available. They do not populate request or claims.

The Toggly facade proxies FeatureManager directly. Without an explicit context argument, Toggly::isEnabled('x') supplies none of those keys — targeting and percentage filters will not see identity from the facade alone. Pass context as the second argument when those filters matter.

Segment HTTP filters and UserClaims fail closed unless you pass that context explicitly (for example via HttpRequestMapper + auth-derived claims below).

Pass full context when those filters matter:

use Toggly\FeatureManagement\Core\FeatureManager;
use Toggly\FeatureManagement\Core\HttpRequestMapper;

// HeaderBag::all() is array<string, string[]> — mapper expects scalar strings
$headers = [];
foreach ($request->headers->all() as $name => $values) {
$headers[$name] = is_array($values) ? ($values[0] ?? '') : (string) $values;
}

$context = HttpRequestMapper::mergeIntoContext(
$headers,
[
'identity' => (string) auth()->id(),
'groups' => auth()->user()?->roles?->pluck('name')->all() ?? [],
'claims' => [
// From your auth / JWT layer — not from untrusted client headers
'role' => auth()->user()?->role ?? '',
],
]
);

if (app(FeatureManager::class)->isEnabled('MobileCheckout', $context)) {
// ...
}
Context keyUsed by
identityPercentage, Targeting, segment percentage gates
groupsTargeting
claimsUserClaims (Claim + Value)
requestHTTP segment filters (userAgent / acceptLanguage / country)

HttpRequestMapper maps user-agent, accept-language, and country headers (cf-ipcountry → x-vercel-ip-country → cloudfront-viewer-country). It does not invent identity, groups, or claims.

See Core library and the SDK × filter matrix.

Legacy Laravel Filters (deprecated)​

Classes under Toggly\Laravel\Filters\ (BrowserFamilyFilter, BrowserLanguageFilter, CountryFilter, DeviceTypeFilter, OSFilter, UserClaimsFilter) are legacy. They are not wired into core evaluation and should not be treated as the primary path for segment / UserClaims filters.

Prefer core FeatureManager + claims / request (or HttpRequestMapper) as shown above.

Native Laravel Patterns​

The Laravel SDK provides several native patterns that make feature flags feel natural to Laravel developers. These patterns allow you to use feature flags in a way that feels idiomatic to Laravel, using PHP 8 attributes, Blade components, and directives.

info

These native patterns are automatically available once you've installed and configured the Laravel SDK. No additional setup is required beyond the standard installation.

PHP 8 Attributes (FeatureGate)​

Use PHP 8 attributes to gate controllers and methods. This is the most declarative way to protect routes and actions.

Basic Usage:

use Toggly\Laravel\Attributes\FeatureGate;

// Gate an entire controller
#[FeatureGate('my-feature')]
class MyController extends Controller
{
public function index()
{
// All actions in this controller require 'my-feature' to be enabled
}

// Gate a specific action (overrides controller-level gate)
#[FeatureGate('another-feature')]
public function show()
{
// This specific action requires 'another-feature'
}
}

Controller-Level vs Method-Level:

When you apply #[FeatureGate] to a controller class, all actions in that controller are protected. You can override this at the method level by applying the attribute to individual methods.

Multiple Features​

You can require multiple features with Any (default) or All requirement:

// Any of these features must be enabled (default behavior)
#[FeatureGate(['feature1', 'feature2'], requirement: 'Any')]
class MyController extends Controller
{
// Controller is accessible if feature1 OR feature2 is enabled
}

// All features must be enabled
#[FeatureGate(['feature1', 'feature2'], requirement: 'All')]
public function index()
{
// This action requires BOTH feature1 AND feature2 to be enabled
}

Pass multiple features as an array. A single string is one feature name (commas are not split):

#[FeatureGate(['feature1', 'feature2'], requirement: 'Any')]

Custom Error Handling​

By default, when a feature is disabled, the middleware returns a 404 response. You can customize this behavior:

// Redirect to a specific route if feature is disabled
#[FeatureGate('my-feature', redirectTo: '/old-page')]
public function index()
{
// If 'my-feature' is disabled, users are redirected to /old-page
}

// Return a custom status code (default is 404)
#[FeatureGate('my-feature', statusCode: 403)]
public function index()
{
// If 'my-feature' is disabled, returns 403 Forbidden
}

// Combine with requirement
#[FeatureGate(['feature1', 'feature2'], requirement: 'All', statusCode: 403)]
public function index() { }

Feature Usage Tracking​

Use the FeatureUsage attribute to mark controllers/methods as actively using features for statistics. This is different from FeatureGate - it marks usage for reporting purposes but doesn't gate access.

Important: Always place #[FeatureUsage] after #[FeatureGate] to ensure correct reporting.

use Toggly\Laravel\Attributes\FeatureGate;
use Toggly\Laravel\Attributes\FeatureUsage;

#[FeatureGate('my-feature')]
#[FeatureUsage('my-feature')] // Must come AFTER FeatureGate
class MyController extends Controller
{
// Usage is tracked when this controller is accessed
// Only if the FeatureGate allows access
}

// You can also track usage on specific methods
#[FeatureGate('new-dashboard')]
#[FeatureUsage('new-dashboard')]
public function dashboard()
{
// Usage tracked for this specific action
}

Multiple Features:

#[FeatureGate(['feature1', 'feature2'])]
#[FeatureUsage(['feature1', 'feature2'])]
class MyController extends Controller { }
warning

The FeatureUsage attribute should be declared after the FeatureGate attribute. If declared before, usage will be counted even if the action isn't allowed by the FeatureGate flag, resulting in incorrect reporting.

info

Feature usage statistics are automatically sent to Toggly when features are evaluated. The FeatureUsage attribute helps you explicitly mark which controllers/methods represent active usage of features for better analytics.

Attribute-Based Middleware​

To enable automatic feature gating based on attributes, register the FeatureGateAttributeMiddleware. Once registered, all controllers and methods with #[FeatureGate] attributes will be automatically gated - no need to manually add middleware to routes.

For Laravel 11+ (in bootstrap/app.php):

->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
\Toggly\Laravel\Middleware\FeatureGateAttributeMiddleware::class,
]);

// Or apply to API routes
$middleware->api(append: [
\Toggly\Laravel\Middleware\FeatureGateAttributeMiddleware::class,
]);
})

For Laravel 10 and below (in app/Http/Kernel.php):

protected $middlewareGroups = [
'web' => [
// ... other middleware
\Toggly\Laravel\Middleware\FeatureGateAttributeMiddleware::class,
],

'api' => [
// ... other middleware
\Toggly\Laravel\Middleware\FeatureGateAttributeMiddleware::class,
],
];

How It Works:

The middleware automatically:

  1. Inspects the controller and method for #[FeatureGate] attributes
  2. Evaluates the feature flags with the current request context
  3. Blocks access if features are not enabled (returns 404 or redirects)
  4. Tracks feature usage for #[FeatureUsage] attributes

Example:

// No need to add middleware to routes - it's automatic!
#[FeatureGate('premium-features')]
class PremiumController extends Controller
{
#[FeatureGate('advanced-analytics')]
public function analytics()
{
// Automatically protected by both attributes
}
}

Blade Components​

Use the <x-feature> Blade component to conditionally render content. This component is automatically registered and ready to use.

Basic Usage:

<x-feature name="my-feature">
<div>This is shown when 'my-feature' is enabled</div>
</x-feature>

Multiple Features:

{{-- Show if ANY feature is enabled (default) --}}
<x-feature name="feature1,feature2" requirement="any">
<div>Shown if feature1 OR feature2 is enabled</div>
</x-feature>

{{-- Show if ALL features are enabled --}}
<x-feature name="feature1,feature2" requirement="all">
<div>Shown only if BOTH feature1 AND feature2 are enabled</div>
</x-feature>

Real-World Example:

<div class="dashboard">
<h1>Dashboard</h1>

{{-- Show new analytics only if feature is enabled --}}
<x-feature name="new-analytics">
<div class="analytics-panel">
<h2>Advanced Analytics</h2>
<!-- New analytics content -->
</div>
</x-feature>

{{-- Show beta features if any are enabled --}}
<x-feature name="beta-feature-1,beta-feature-2" requirement="any">
<div class="beta-banner">
<p>Beta features available!</p>
</div>
</x-feature>
</div>

Component Attributes:

  • name (required): Feature name(s) as string or comma-separated string
  • requirement (optional): "any" (default) or "all"

The component automatically builds context from identity, groups, and IP when available. It does not map HTTP headers into request or populate claims — segment filters and UserClaims need the explicit mapping shown in Feature evaluation context (or FeatureManager with a full context array).

Blade Directives​

The SDK automatically registers Blade directives for feature flags. These directives work like standard Blade @if statements and are perfect for conditional rendering.

Basic Usage:

@feature('my-feature')
<div>This is shown when the feature is enabled</div>
@endfeature

Multiple Features:

{{-- Show if ANY feature is enabled (default) --}}
@feature('feature1,feature2')
<div>Shown if feature1 OR feature2 is enabled</div>
@endfeature

{{-- Show if ALL features are enabled --}}
@feature('feature1,feature2', 'all')
<div>Shown only if BOTH features are enabled</div>
@endfeature

Opposite Condition:

@unlessfeature('my-feature')
<div>Shown when feature is disabled</div>
@endunlessfeature

Nested with Other Directives:

@feature('new-checkout')
<div class="checkout-v2">
@auth
<p>Welcome, {{ auth()->user()->name }}!</p>
@endauth
</div>
@else
<div class="checkout-v1">
<!-- Old checkout -->
</div>
@endfeature

Real-World Example:

<nav>
<a href="/">Home</a>
<a href="/products">Products</a>

@feature('new-dashboard')
<a href="/dashboard">New Dashboard</a>
@else
<a href="/old-dashboard">Dashboard</a>
@endfeature

@feature('premium-features')
<a href="/premium">Premium</a>
@endfeature
</nav>

Available Directives:

  • @feature($feature, $requirement = 'any') - Show content if feature(s) enabled
  • @unlessfeature($feature) - Show content if feature(s) disabled

If Statements​

You can also use feature flags in standard PHP if statements using the facade or dependency injection:

Using the Facade:

use Toggly\Laravel\Facades\Toggly;

if (Toggly::isEnabled('my-feature')) {
// Feature is enabled
return view('new-feature');
}

return view('old-feature');

With Context:

$context = [
'identity' => (string) auth()->id(),
'traits' => [
'plan' => auth()->user()->plan,
],
];

if (Toggly::isEnabled('premium-feature', $context)) {
// Feature enabled for this user
}

Dependency Injection​

The SDK integrates seamlessly with Laravel's service container. Prefer dependency injection over facades for better testability:

In Controllers:

use Toggly\FeatureManagement\Core\FeatureManager;

class MyController extends Controller
{
public function __construct(
private FeatureManager $featureManager
) {}

public function index()
{
if ($this->featureManager->isEnabled('my-feature')) {
return view('new-view');
}

return view('old-view');
}
}

In Services:

use Toggly\FeatureManagement\Core\FeatureManager;

class MyService
{
public function __construct(
private FeatureManager $featureManager
) {}

public function processOrder($order)
{
if ($this->featureManager->isEnabled('new-checkout', [
'userId' => $order->user_id,
'orderTotal' => $order->total,
])) {
return $this->processWithNewCheckout($order);
}

return $this->processWithOldCheckout($order);
}
}

In Form Requests:

use Toggly\FeatureManagement\Core\FeatureManager;

class StoreOrderRequest extends FormRequest
{
public function rules()
{
$rules = [
'items' => 'required|array',
];

// Add validation rules based on feature flags
if (app(FeatureManager::class)->isEnabled('new-validation')) {
$rules['items.*.quantity'] = 'required|integer|min:1|max:10';
}

return $rules;
}
}

Best Practices​

  1. Use Environment Variables: Store your App Key in .env, never commit it
  2. Cache Features: Features are automatically cached, but ensure your cache driver is configured
  3. Handle Errors Gracefully: The SDK falls back to cached values on errors
  4. Schedule Tasks: Set up scheduled tasks for refreshing features and sending stats
  5. Use Dependency Injection: Prefer dependency injection over facades for better testability
  6. Use Attributes for Route Protection: Use #[FeatureGate] attributes instead of manual middleware for cleaner, more maintainable code
  7. Track Feature Usage: Use #[FeatureUsage] attributes to get accurate usage statistics
  8. Organize Feature Names: Use consistent naming conventions (e.g., kebab-case) and consider creating a constants class or enum for feature names
  9. Test Feature Flags: Write tests that verify behavior with features enabled and disabled
  10. Document Feature Flags: Keep track of what each feature flag controls and when it can be removed

Complete Example​

Here's a complete example showing multiple patterns working together:

Controller:

use Toggly\Laravel\Attributes\FeatureGate;
use Toggly\Laravel\Attributes\FeatureUsage;
use Toggly\FeatureManagement\Core\FeatureManager;

#[FeatureGate('new-dashboard')]
#[FeatureUsage('new-dashboard')]
class DashboardController extends Controller
{
public function __construct(
private FeatureManager $featureManager
) {}

public function index()
{
$showAnalytics = $this->featureManager->isEnabled('analytics', [
'userId' => auth()->id(),
]);

return view('dashboard', [
'showAnalytics' => $showAnalytics,
]);
}
}

Blade View:

@extends('layouts.app')

@section('content')
<div class="dashboard">
<h1>Dashboard</h1>

<x-feature name="analytics">
<div class="analytics-panel">
<h2>Analytics</h2>
<!-- Analytics content -->
</div>
</x-feature>

@feature('notifications')
<div class="notifications">
<!-- Notifications -->
</div>
@endfeature
</div>
@endsection

Routes:

// No middleware needed - attributes handle it!
Route::get('/dashboard', [DashboardController::class, 'index']);