Skip to main content

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.

Server only

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 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

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:

  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. 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 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' => 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​

  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: 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.
  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', [
'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.