qandidate/toggle
Qandidate Toggle is a PHP library for managing feature toggles (feature flags) in your applications. Define toggles, activate them conditionally with strategies, and roll features out safely without redeploying code.
Install the core library (and companion packages only if you need them):
composer require qandidate/toggle
The mental model is three objects:
Toggle — something that can answer isActive().ToggleProvider — the registry that hands toggles back by name (InMemoryProvider, MongoDB, YAML, or your own).ToggleManager — the façade you inject into application code.Minimal wiring:
use Qandidate\Toggle\ToggleManager;
use Qandidate\Toggle\ToggleFactory;
use Qandidate\Toggle\InMemoryProvider;
$provider = new InMemoryProvider();
$provider->add('new-checkout', ToggleFactory::create('new-checkout', [
'strategy' => 'operator',
'operator' => 'OneOf',
'conditions' => [
['name' => 'userId', 'operator' => 'Equals', 'value' => 42],
],
]));
$manager = new ToggleManager($provider);
$manager->isActive('new-checkout');
Where to look first, in this order:
README.md — the strategies section is the whole API surface in one page.src/Qandidate/Toggle/ToggleFactory.php — tells you exactly which strategy / operator strings are legal.src/Qandidate/Toggle/Operator/ — the condition vocabulary (Equals, InSet, Percentage, AllOf, AnyOf, OneOf, NoneOf, …).src/Qandidate/Toggle/Context.php and ContextValue.php — how runtime data reaches your conditions.First real use case: a release toggle. Wrap one new code path behind a simple strategy toggle (always on/off) so you can deploy dark and flip it from config without a redeploy. Don't start with targeting rules — get the plumbing (provider → manager → injected service) working first.
Inject the narrow interface, not the manager. If your code only calls isActive(), depend on Qandidate\Toggle\Toggle or a tiny FeatureFlags wrapper around the manager. That keeps tests trivial and lets you swap in a static stub.
Strategy taxonomy you'll actually use:
| Need | Definition |
|---|---|
| Kill switch / always on | ['strategy' => 'simple'] (or flip the value on the manager) |
| Internal users only | operator => 'OneOf', condition userId InSet [1,2,3] |
| Gradual rollout | operator => 'Percentage' (or an operator with a Percentage condition) |
| Per-tenant beta | condition on tenantId with InSet |
| Composite rules | nest AllOf / AnyOf operators as conditions |
Build a context factory per-request. Don't sprinkle new Context(...) through controllers. Add one service that reads the current user/tenant/request and returns a Context. Every toggle then gets the same population of values, and you only extend one class when a new dimension appears.
Cache the context, not just the toggle. Operators are cheap to evaluate; provider round-trips (Mongo, HTTP) are not. A request-scoped memoizer that keys on (toggleName, contextHash) removes almost all the cost in loops and templates.
Remote/centralized flags. Use qandidate/toggle-client when flags must be managed by a separate service rather than shipped in config. Treat every remote call as fallible — wrap it in a local default so a timeout degrades to "feature off" rather than a 500.
Symfony / framework integration. qandidate/toggle-bundle wires the manager as a service and gives you config/YAML-driven definitions. Register your context provider there and the rest of the app only sees an injected Toggle.
Rollout workflow that works:
InSet of internal user IDs; watch metrics for a day.Percentage, ramp 5 → 25 → 100.Testing pattern. Seed an InMemoryProvider with the toggle forced on and off, and assert both code paths in the same test file. Keep a small helper (withToggles(['new-checkout' => false])) so tests read as intent, not as object graph construction.
Custom behaviour. Two extension points cover most needs: implement Toggle for entirely bespoke logic, or implement the operator interface for a new condition type (e.g. DuringBusinessHours). Prefer the operator — it composes with everything else and stays declarative.
Missing toggles will bite you. Providers typically throw when asked for a name that doesn't exist. Decide once, globally: either register a catch-all default or wrap the lookup in a try/catch that logs and returns false. Silent 500s on an unknown flag are the most common production incident with this class of library.
Operator semantics are easy to invert. AllOf = every condition matches, AnyOf = at least one, OneOf = exactly one, NoneOf = none. Getting AnyOf where you meant OneOf (or vice versa) is the classic "the toggle is on for everyone" bug. Write a unit test per operator you introduce.
Missing context values are not an exception — they're a silent false. If you condition on tenantId and the context factory doesn't populate it on background jobs, the toggle is permanently off. Add an assertion or a test that the context factory produces the full key set on every entry point (HTTP, CLI, queue worker).
Percentage needs a stable input. Bucketing is only consistent if it's derived from something stable per user (an ID), not from a request ID, timestamp, or randomly ordered array. If users flap in and out of a rollout, look at what you're hashing.
No caching, no invalidation. The core library evaluates the tree on every call. If you're checking a flag inside a loop or a hot rendering path, memoize it yourself. There's no built-in push/invalidation mechanism either — remote providers are poll-on-read.
Provider registration signatures vary. InMemoryProvider, the YAML provider, and third-party providers each have their own constructor/add shape. Confirm against the specific provider's source rather than assuming $provider->add($name, $toggle).
Static state and containers. Constructing a ToggleManager (with a Mongo/HTTP provider behind it) per call is a real performance trap. Register it as a singleton in the container and inject it.
Debugging a toggle that "doesn't work": dump the context object first, then the toggle definition, then evaluate the operator tree by hand. Ninety percent of the time the definition is right and the context is missing a key.
Toggles are technical debt with a half-life. Name them with a domain prefix (checkout.new-flow), keep a removal ticket in the definition's comment or config, and delete the branch when you hit 100%. A repo full of stale toggles is worse than no toggles.
Pin the version and check maintenance activity. This is a mature, low-churn library with a small ecosystem — read the changelog before bumping, and be prepared to vendor or fork if you lean on it heavily. The core is small enough that forking is a realistic option.
How can I help you explore Laravel packages today?