Advanced Usage
These examples build on the variables in the
core entry point: $settings, $http, $state,
$provider, $usage and $features.
State and definition callbacks
Subscribe on the same state service injected into the provider. Callbacks take
no arguments; capture application values with use when needed. Save the
returned IDs so a worker can unregister listeners when their owner is released.
$onId = $state->whenFeatureTurnsOn('new-dashboard', static function (): void {
error_log('new-dashboard turned on');
});
$offId = $state->whenFeatureTurnsOff('new-dashboard', static function (): void {
error_log('new-dashboard turned off');
});
$definitionsId = $state->whenDefinitionsChange(static function (): void {
error_log('Feature definitions changed');
});
$provider->refreshFeatures();
$state->unregisterFeatureStateChange('new-dashboard', $onId);
$state->unregisterFeatureStateChange('new-dashboard', $offId);
$state->unregisterDefinitionsChange($definitionsId);
An on/off subscription can invoke its callback immediately if that state is already known. These are provider state notifications, not per-user targeting notifications or authorization events.
Custom metric sources
Each registry method accepts one callback and returns a registration ID.
Measurement and counter callbacks return maps of metric key to numeric value.
Observation callbacks return maps of metric key to [count, value] pairs.
use Toggly\FeatureManagement\Core\{MetricsRegistryService, MetricsService};
$registry = new MetricsRegistryService();
$measurementId = $registry->registerMeasurements(static fn (): array => [
'queue-depth' => 3.0,
]);
$observationId = $registry->registerObservations(static fn (): array => [
'checkout-time' => [2, 1.5],
]);
$counterId = $registry->registerCounters(static fn (): array => [
'completed-orders' => 2.0,
]);
$metrics = new MetricsService($settings, $http, $provider, $registry);
$metrics->sendMetrics();
$registry->unregisterMetrics($measurementId);
$registry->unregisterMetrics($observationId);
$registry->unregisterMetrics($counterId);
The numbers above are illustrative fixed values; replace them with values owned by your application. Callbacks remain registered until unregistered, so ensure your data source defines whether subsequent reads are current values or new increments. Flush the metrics service in the process that collected them.
Authorization for secured features
The SDK calls isAllowed($featureKey) for a secured feature. Bind authorization
to the current authenticated request; evaluation context is not forwarded into
this interface. A flag is an additional condition, not a replacement for your
application's normal authorization checks.
use Toggly\FeatureManagement\Contracts\FeatureAuthorizationServiceInterface;
use Toggly\FeatureManagement\Core\FeatureManager;
final class RequestFeatureAuthorization implements FeatureAuthorizationServiceInterface
{
public function __construct(private array $allowedFeatureKeys) {}
public function isAllowed(string $featureKey): bool
{
return in_array($featureKey, $this->allowedFeatureKeys, true);
}
}
// Supply only keys authorized by your server-side policy for this request.
$authorization = new RequestFeatureAuthorization(['premium-content']);
$features = new FeatureManager($provider, $usage, $provider, $authorization);
$enabled = $features->isEnabled('premium-content', ['identity' => 'alice']);
Variants
A variant is a named configuration chosen by the definitions service for one
identity. Build an isolated provider with enable_variants and identity set
before its first refresh. This example intentionally has no snapshot, so it
cannot load another identity's persisted variants.
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\HttpFactory;
use Toggly\FeatureManagement\Config\TogglySettings;
use Toggly\FeatureManagement\Core\{FeatureProvider, FeatureStateService};
use Toggly\FeatureManagement\Http\TogglyHttpClient;
$variantSettings = new TogglySettings([
'app_key' => getenv('TOGGLY_APP_KEY') ?: '',
'environment' => 'Production',
'base_url' => getenv('TOGGLY_BASE_URL') ?: 'https://definitions.toggly.io/',
'use_signed_definitions' => true,
'enable_variants' => true,
'identity' => 'alice',
'enable_live_updates' => false,
]);
$variantHttp = new TogglyHttpClient(new Client(), new HttpFactory(), $variantSettings->getBaseUrl());
$variantProvider = new FeatureProvider($variantSettings, $variantHttp, new FeatureStateService());
try {
$variantProvider->refreshFeatures();
$variant = $variantProvider->getVariant('pricing-test');
$name = $variant['name'] ?? 'default';
$configuration = $variant['configurationValue'] ?? null;
$value = $variantProvider->getVariantValue('pricing-test');
} finally {
$variantProvider->shutdown();
}
getVariant returns {name, configurationValue} or null when there is no
enabled variant. Neither FeatureManager nor the Laravel facade exposes this
method. Ordinary isEnabled context does not retarget variants; use the correct
provider identity scope from the start.
Long-running hosts
Your application owns polling and telemetry schedules. This bounded CLI loop
illustrates the lifecycle; replace the loop condition with your worker's stop
signal and perform application work between iterations. It uses $metrics
from the metric setup above and core objects configured for this worker.
$nextRefresh = 0;
$nextFlush = 0;
$stopAt = time() + 60;
try {
while (time() < $stopAt) {
$now = time();
if ($now >= $nextRefresh) {
$provider->refreshFeatures();
$nextRefresh = $now + $settings->refreshInterval;
}
$provider->tick();
if ($now >= $nextFlush) {
$usage->flush();
$metrics->flush();
$nextFlush = $now + 60;
}
usleep(100000);
}
} finally {
$usage->flush();
$metrics->flush();
$provider->shutdown();
}
tick() drives WebSocket processing; it does not replace the explicit refresh
schedule. Enable live updates in the settings before constructing the provider
when the host supports them. No separate WebSocket object needs injection.
For workers handling different users, pass evaluation context per call and create request-local telemetry and authorization objects. Do not cache a user-specific evaluation solely by flag key. In Laravel, read the persistent-worker limitations before sharing the adapter's singleton services across requests.
Testing your integration
Use deterministic definition responses through your PSR HTTP client and execute
the real manager. Verify enabled and disabled branches, unknown flags, the
first refresh, two different user contexts, snapshot restart behavior and
shutdown flushing. A subclass of FeatureManager still requires its real
constructor dependencies unless you supply a constructor of your own. For
application unit tests, inject the narrow feature-check interface your
application uses instead of creating a partially initialized SDK manager.