Core Library Usage
A feature flag chooses between two code paths without redeploying your application.
FeatureProvider loads definitions; FeatureManager evaluates those definitions
for the context you supply. Keep the original behavior as the disabled branch.
Installation
Run this in the directory containing your application's composer.json:
composer require toggly/feature-management-php guzzlehttp/guzzle guzzlehttp/psr7
First flag: a complete PHP entry point
Create first-flag.php beside vendor/. Configure new-dashboard in your
Toggly app and choose the same environment as this script. Set TOGGLY_APP_KEY
in your application's environment, then run php first-flag.php.
<?php
require __DIR__ . '/vendor/autoload.php';
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\HttpFactory;
use Toggly\FeatureManagement\Config\TogglySettings;
use Toggly\FeatureManagement\Core\{FeatureManager, FeatureProvider, FeatureStateService, UsageStatsProvider};
use Toggly\FeatureManagement\Http\TogglyHttpClient;
$appKey = getenv('TOGGLY_APP_KEY');
if ($appKey === false || $appKey === '') {
throw new RuntimeException('Set TOGGLY_APP_KEY before running this example.');
}
$settings = new TogglySettings([
'app_key' => $appKey,
'environment' => 'Production',
'base_url' => getenv('TOGGLY_BASE_URL') ?: 'https://definitions.toggly.io/',
'use_signed_definitions' => true,
'enable_live_updates' => false,
'on_error' => static function (string $message, ?Throwable $error): void {
error_log($message);
},
]);
$http = new TogglyHttpClient(new Client(), new HttpFactory(), $settings->getBaseUrl());
$state = new FeatureStateService();
$provider = new FeatureProvider($settings, $http, $state);
$usage = new UsageStatsProvider($settings, $http);
$features = new FeatureManager($provider, $usage, $provider);
try {
// Construction does not fetch definitions. Load before the first decision.
$provider->refreshFeatures();
$context = ['identity' => 'demo-reader'];
echo $features->isEnabled('new-dashboard', $context)
? "New dashboard\n"
: "Original dashboard\n";
} finally {
$usage->flush();
$provider->shutdown();
}
With an AlwaysOn rule the output is New dashboard; with an AlwaysOff rule it
is Original dashboard. Change the rule and rerun the script to fetch the new
state. A missing flag is disabled by default. A failed refresh is reported via
on_error; keep a useful disabled path when no usable definition is available.
The demonstration identity is fixed. In a web application derive it from the
authenticated request before its first check.
Per-evaluation targeting
Pass context as the second argument every time the decision depends on a user. These checks reuse definitions without changing the provider's identity:
$alice = $features->isEnabled('beta-access', ['identity' => 'alice']);
$bob = $features->isEnabled('beta-access', ['identity' => 'bob']);
$context = [
'identity' => 'alice',
'groups' => ['beta-testers'],
'claims' => ['role' => 'editor'],
'request' => [
'userAgent' => 'Mozilla/5.0',
'acceptLanguage' => 'en-US',
'country' => 'US',
],
];
$enabled = $features->isEnabled('premium-content', $context);
Identity supports Targeting and percentage rules, groups support Targeting,
claims supports UserClaims, and request supports BrowserFamily,
BrowserLanguage, Country, DeviceType and OS filters. Legacy userId and
user_id are identity fallbacks; prefer identity in new code. Use
server-authenticated claims and trusted proxy headers, not arbitrary client
claims about membership or role.
HttpRequestMapper::mergeIntoContext($headers, $context) can map scalar
user-agent, accept-language and country headers into request. It does not
create user identity or claims. See the Laravel example.
PHP does not implement a ContextProperty filter, entity registration, or
per-call entity objects. Adding Order, plan, or orderTotal to an array
does not make those properties evaluable. See the filter matrix.
Telemetry context is a separate responsibility
The optional FeatureContextProviderInterface passed to UsageStatsProvider
provides telemetry identifiers and tracks checks already counted in a request.
It does not supply targeting context to FeatureManager. Its four methods are
accessedInRequest, accessedInRequestWithContext, getContextIdentifier and
getContextIdentifierWithContext.
Keep deduplication state in a request-lifetime object, not a session that spans requests. Construct a new telemetry context and usage provider for each request in a persistent host; flush that usage provider before releasing it. Definitions can be shared for ordinary local evaluation, while request context and authorization must retain their own lifetime.
Snapshot injection
Core has no snapshot provider by default. Select one before construction:
use Toggly\FeatureManagement\Storage\SnapshotProviders\FileSnapshotProvider;
use Toggly\FeatureManagement\Storage\SnapshotSettings;
$snapshots = new FileSnapshotProvider(
sys_get_temp_dir() . '/toggly-production',
new SnapshotSettings(['document_name' => 'toggly_features'])
);
$provider = new FeatureProvider($settings, $http, $state, $snapshots);
$features = new FeatureManager($provider, $usage, $provider);
$provider->refreshFeatures();
The constructor attempts to load a saved snapshot; refreshFeatures() then
fetches current definitions. There is no setSnapshotProvider method. Use
separate storage namespaces for different apps and environments. Protect the
snapshot directory and give the PHP process permission to read and write it.
See Snapshot providers for other storage choices.
Metrics and authorization
Metrics also need the feature provider so they can associate metrics with experiments:
use Toggly\FeatureManagement\Core\{MetricsRegistryService, MetricsService};
$registry = new MetricsRegistryService();
$metrics = new MetricsService($settings, $http, $provider, $registry);
$metrics->measure('checkout-time', 2.5);
$metrics->observe('button-click', 1);
$metrics->incrementCounter('page-views', 1);
$metrics->flush();
For secured features, supply a FeatureAuthorizationServiceInterface as the
fourth FeatureManager argument. It implements isAllowed(string $featureKey): bool.
There is no context parameter: the application must give the authorization
service its request-local authenticated principal. See Advanced usage.
Refresh and shutdown
refresh_interval describes the interval your host should use; it does not
start a timer. Call refreshFeatures() explicitly at startup when there is no
usable snapshot, and periodically in a scheduler or worker. In a short PHP
request, a shared snapshot can avoid a network fetch on every page if a separate
scheduled process keeps that snapshot current.
Long-running hosts must call tick() to drive WebSocket work and still schedule
refreshFeatures() for polling. tick() does not implement the polling timer.
Call shutdown() when the worker ends and flush usage and metrics from the same
process that accumulated them. See Configuration and
Advanced usage.
The provider accepts settings, HTTP client, state service, optional snapshot, and optional PSR logger—in that order. It manages its WebSocket internally. Signed refresh rejects invalid signatures; do not retry by silently disabling signature verification. Keep the last usable state and observe refresh errors.
Continue learning
Read the bootstrap above from settings → transport → provider → manager → evaluation. Then add request context, snapshot storage, and host scheduling in that order. The Samples catalog contains application examples across the SDK family.