Skip to main content

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.