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 key | Used by |
|---|---|
identity | Percentage, Targeting, segment percentage gates |
groups | Targeting |
claims | UserClaims (Claim + Value) |
request | HTTP 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.
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 { }
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.
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:
- Inspects the controller and method for
#[FeatureGate]attributes - Evaluates the feature flags with the current request context
- Blocks access if features are not enabled (returns 404 or redirects)
- 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 stringrequirement(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
- Use Environment Variables: Store your App Key in
.env, never commit it - Cache Features: Features are automatically cached, but ensure your cache driver is configured
- Handle Errors Gracefully: The SDK falls back to cached values on errors
- Schedule Tasks: Set up scheduled tasks for refreshing features and sending stats
- Use Dependency Injection: Prefer dependency injection over facades for better testability
- Use Attributes for Route Protection: Use
#[FeatureGate]attributes instead of manual middleware for cleaner, more maintainable code - Track Feature Usage: Use
#[FeatureUsage]attributes to get accurate usage statistics - Organize Feature Names: Use consistent naming conventions (e.g.,
kebab-case) and consider creating a constants class or enum for feature names - Test Feature Flags: Write tests that verify behavior with features enabled and disabled
- 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']);