Laravel Integration
Segment membership
Create the list in the dashboard and attach it to a Targeting filter. Sync members with a Backend application key. Evaluation uses the published list after the next definitions refresh. Flow · HTTP API
Use the core client from a job or controller. Laravel does not add a second HTTP stack.
Requires a Backend application key. Do not call this from a browser, mobile app, or other public client.
use Toggly\FeatureManagement\SegmentMembershipClient;
$segments = new SegmentMembershipClient(
$httpClient,
$requestFactory,
$streamFactory,
config('toggly.app_key')
);
$segments->addSegmentMembers('Beta Testers', ['user-123']);
Learn how to use Toggly Feature Management in Laravel applications.
Installation
Install the Laravel integration (pulls core automatically):
composer require toggly/laravel guzzlehttp/guzzle guzzlehttp/psr7
Laravel package auto-discovery registers the SDK service provider. If discovery
is disabled, add only Toggly\Laravel\ServiceProvider::class to your existing
bootstrap/providers.php provider list:
<?php
return [
App\Providers\AppServiceProvider::class,
Toggly\Laravel\ServiceProvider::class,
];
Keep your other application providers in this list. Use the imported facade
use Toggly\Laravel\Facades\Toggly; shown below; a facade does not belong in
bootstrap/providers.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
Host bootstrap
The SDK resolves a PSR-18 HTTP client and PSR-17 request factory from Laravel's
container. Add these bindings to app/Providers/AppServiceProvider.php. The
boot example performs an initial refresh before controllers or Blade evaluate
flags. It is a simple request-per-process setup; shared snapshots can support a
scheduled refresh strategy once you define your application's cold-start policy.
<?php
namespace App\Providers;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\HttpFactory;
use Illuminate\Support\ServiceProvider;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Toggly\FeatureManagement\Core\{FeatureProvider, UsageStatsProvider, MetricsService};
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(ClientInterface::class, fn () => new Client());
$this->app->singleton(RequestFactoryInterface::class, fn () => new HttpFactory());
}
public function boot(): void
{
if (!config('toggly.app_key')) {
return;
}
$provider = $this->app->make(FeatureProvider::class);
$provider->refreshFeatures();
$this->app->terminating(function () use ($provider): void {
$this->app->make(UsageStatsProvider::class)->flush();
$this->app->make(MetricsService::class)->flush();
$provider->shutdown();
});
}
}
Use your existing provider class if it already has registration or boot logic.
Do not replace unrelated bindings. Configure a working Laravel cache store;
for a local first run, Laravel's array cache avoids a database dependency but
does not persist snapshots between processes.
Create new-dashboard in Toggly, set it AlwaysOn, and render the new view in the
controller example below. Change it to AlwaysOff and make a new request: the
original view should render after refresh. Targeting rules additionally need
request context; the facade does not infer the signed-in user.
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
For a shared persistent snapshot, add the refresh task to routes/console.php:
use Illuminate\Support\Facades\Schedule;
use Toggly\FeatureManagement\Core\FeatureProvider;
Schedule::call(function (): void {
app(FeatureProvider::class)->refreshFeatures();
})->everyFiveMinutes();
Run Laravel's scheduler using your host's normal schedule:run cron entry or
schedule:work process. TOGGLY_REFRESH_INTERVAL alone creates no timer. A
scheduled process can update shared snapshots, but it cannot drain usage or
metrics held in a different worker's memory. Flush those in their owning
request or worker, as shown in host bootstrap.
Persistent workers
The adapter registers a singleton telemetry context provider that captures an Illuminate Request and retains usage deduplication state. Sharing a cache does not reset that state for a new request. Per-call evaluation context is necessary for targeting, but does not by itself establish telemetry isolation in Octane or concurrent workers. Validate request ownership before using this adapter in such hosts; this guide's bootstrap is for request-per-process operation.
Long-running hosts also own periodic refresh, same-worker telemetry flushing,
provider tick() for WebSockets, and shutdown() on termination. See
the core worker lifecycle.
State Change Handlers
Register callbacks to be notified when features change state:
use Toggly\FeatureManagement\Contracts\FeatureStateServiceInterface;
$featureStateService = app(FeatureStateServiceInterface::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' => auth()->check() ? (string) auth()->id() : null,
'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.
Blade directives and components are registered by the SDK service provider. PHP attributes require the attribute middleware registration below before they gate routes.
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
}
// Add a second gate to this action
#[FeatureGate('another-feature')]
public function show()
{
// This action requires both class and method gates
}
}
Controller-Level vs Method-Level:
When you apply #[FeatureGate] to a controller class, all actions in that controller are protected. A method-level gate adds another requirement: both class and method gates must allow the action.
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.
The middleware evaluates class and method gates first, then usage attributes. Declaration order does not change this sequence.
use Toggly\Laravel\Attributes\FeatureGate;
use Toggly\Laravel\Attributes\FeatureUsage;
#[FeatureGate('my-feature')]
#[FeatureUsage('my-feature')] // Evaluated after both gates allow access
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 { }
FeatureUsage performs an additional isEnabled evaluation after the gates
allow access; it does not call a separate explicit used-count operation.
Evaluations collect statistics in memory. They are sent during flushing, not
automatically over the network on every check.
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)
- Evaluates
#[FeatureUsage]flags after the gates allow access
Example:
// With FeatureGateAttributeMiddleware registered for this route group:
#[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' => auth()->check() ? (string) auth()->id() : null,
'claims' => [
'role' => auth()->user()?->role ?? '',
],
];
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', [
'identity' => (string) $order->user_id,
])) {
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: Schedule definition refreshes; flush statistics in the request or worker that collected them. A separate scheduled process cannot drain another process's in-memory counters.
- 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', [
'identity' => auth()->check() ? (string) auth()->id() : null,
]);
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:
// The web group must include FeatureGateAttributeMiddleware as shown above.
Route::get('/dashboard', [DashboardController::class, 'index']);
More examples
Browse the Samples catalog. For
variants, use an initially configured, identity-scoped FeatureProvider;
Toggly::getVariant is not a facade API. PHP has no ContextProperty or entity
registration API, so adding plan or orderTotal to a context array does not
create an entity rule. See advanced usage.