Published
- 12 min read
Unleash: Feature flags in PHP
Feature flags (also known as feature toggles) let you enable or disable specific features or code paths at runtime without deploying new code. In this article, we'll explore Unleash, my favourite open-source tool for managing feature flags efficiently.
If you're unsure where you could (or why you should) use feature flags in your project, this section is for you, otherwise feel free to skip this part.
What are feature flags
Feature flags are runtime switches that enable or disable specific code paths dynamically. You might already be using them without realizing it! If your system allows enabling or disabling functionality via database settings (e.g., toggling registrations, comments, or user uploads), you're already using a basic form of feature flags. But these self-built options are rarely as thought-out as dedicated feature flagging systems.
Dedicated feature flagging systems
Dedicated feature flagging systems provide a standardized way to manage feature toggles and unlock additional use cases, such as:
- Gradually roll out features to a subset of users, such as internal users or beta testers.
- Makes it possible to do a gradual rollout to test out the reactions without deploying a feature to everyone
- Enable features based on the region of the user (like GDPR, CCPA)
- Create experimental features without maintaining separate branches
- A/B test multiple versions of a new feature
- Implement a kill switch to turn off some parts of the code in case of emergency (attack, data corruption...)
- Replace your built-in permission system
- Create toggleable features that are only needed in certain cases (for example, enable a high verbosity logging if you run into issues)
- Rollback features if they're broken
- and many more
Unleash
Disclaimer: I originally wrote the open-source Unleash PHP SDK, which was later adopted as the official Unleash SDK. While I’m paid to maintain it, this article is not sponsored (and I'm not an employee of Unleash). I’m writing it for the same reasons I originally created the SDK: I love how Unleash is implemented and think more people should use it!
Unleash is one such system. Unleash offers both a paid plan and a self-hosted open-source version. While the open-source version lacks some premium features, since the release of the constraints feature to the OSS version it's feature-complete for my needs.
What makes Unleash unique is the way the feature evaluation is handled: everything happens locally, meaning your app does not leak any data to Unleash. Your application also avoids performance overhead from unnecessary HTTP requests. Usually these systems do the evaluation on the server and just return a yes/no response. With Unleash, you instead get the whole configuration as a simple JSON and the SDK does evaluation locally (to the point that you could even use the SDK without Unleash at all, you can simply provide a static JSON). Furthermore, the features are cached locally for half a minute or so, thus the only I/O overhead Unleash adds is 2 http requests a minute. And another cool feature is that they support pretty much every major programming language. Now that my fanboying is over, let's go over Unleash in PHP!
Unleash in PHP
Installing the SDK is straightforward, simply run composer require unleash/client
. The documentation can be found at Packagist or GitHub. It supports PHP versions as old as 7.2. Afterwards you create an instance of the Unleash object that you will use throughout your code:
$unleash = UnleashBuilder::create()
->withAppName('Some app name')
->withAppUrl('https://my-unleash-server.com/api/')
->withInstanceId('Some instance id')
->build();
The app name and instance ID are used to identify clients. The app URL is the Unleash server endpoint, which you can find in the settings page.
Once you've set up the Unleash object, using it is extremely simple:
if ($unleash->isEnabled('new-product-page')) {
// do one thing
} else if ($unleash->isEnabled('semi-new-product-page')) {
// do other thing
} else {
// do yet another thing
}
If you do A/B testing, you can configure variants like this:
$topMenuVariant = $unleash->getVariant('top-menu');
if (!$topMenuVariant->isEnabled()) {
// todo the user does not have access to the feature at all
} else {
$payload = $topMenuVariant->getPayload();
// let's assume the payload is a JSON
assert($payload->getType() === VariantPayloadType::JSON);
$payloadData = $payload->fromJson();
// todo display the menu based on the received payload
}
Configuring the features
All of the above must be configured somewhere and that place is the Unleash UI. You can test out their official demo (just put whatever email in there, it doesn't even have to be real, there's no confirmation) if you don't want to install Unleash locally.
Each feature has multiple environments, by default a development
and production
one (I think in the open source version you cannot create more, though I successfully did so by fiddling directly with the database) and each environment must have one or more strategies (unless the environment is disabled). Strategies is what controls whether the feature is enabled for a user or not. I'll go briefly over the simple strategies and then write a bit more about the complex ones (and custom ones).
- Standard - simple yes/no strategy, no configuration, just enabled or disabled
- User IDs - enable the feature for specific user IDs
- IPs and Hosts - enable the feature for specific IP addresses and hostnames respectively
Unleash doesn’t automatically know your app’s user IDs—you need to provide them via an Unleash context:
$context = new UnleashContext(currentUserId: '123');
if ($unleash->isEnabled('some-feature', $context)) {
// todo
}
Or more likely, if you don't want to pass around a manually created context all the time, just create a provider that will create the default context:
final class MyContextProvider implements UnleashContextProvider
{
public function getContext(): Context
{
$context = new UnleashContext();
$context->setCurrentUserId('user id from my app');
return $context;
}
}
$unleash = UnleashBuilder::create()
->withAppName('Some app name')
->withAppUrl('https://my-unleash-server.com/api/')
->withInstanceId('Some instance id')
->withContextProvider(new MyContextProvider())
->build();
if ($unleash->isEnabled('some-feature')) {
// todo
}
The Gradual rollout strategy
This powerful strategy allows you to roll out features to a percentage of users based on a chosen context field (e.g., user ID, IP address, or any custom attribute). With the help of constraints you can configure very complex access scenarios thanks to the many operators that are available (various string, array, date, numeric, version and string operators) for each of your context fields. So in short, you create arbitrary fields in your context provider which you can then validate with any of the supported operators.
This is sort of becoming the catch-all default strategy because it can do everything the others can with the help of constraints. If you want to emulate the Standard strategy, just make it always available to 100% of your users. Emulating User IDs strategy can be done by having it available to 100% of your userbase and adding a constraint that the userId must be one of the specified values. And so on.
Custom strategies
Need even more flexibility? You can create custom strategies! Here’s a real-world example from one of my projects:
<?php
namespace App\Service\Unleash;
use InvalidArgumentException;
use Unleash\Client\Configuration\Context;
use Unleash\Client\DTO\Strategy;
use Unleash\Client\Strategy\AbstractStrategyHandler;
use Override;
final class AccountIdUnleashStrategy extends AbstractStrategyHandler
{
public const string CONTEXT_NAME = 'currentAccountId';
#[Override]
public function getStrategyName(): string
{
return 'accountId';
}
#[Override]
public function isEnabled(Strategy $strategy, Context $context): bool
{
$allowedAccountIds = $this->findParameter('accountIds', $strategy);
if (!$allowedAccountIds) {
return false;
}
try {
$currentCompanyAccountId = $context->getCustomProperty(self::CONTEXT_NAME);
} catch (InvalidArgumentException) {
return false;
}
$allowedAccountIds = array_map('trim', explode(',', $allowedAccountIds));
$enabled = in_array($currentCompanyAccountId, $allowedAccountIds, true);
if (!$enabled) {
return false;
}
return $this->validateConstraints($strategy, $context);
}
}
Then simply register it:
$unleash = UnleashBuilder::create()
->withAppName('Some app name')
->withAppUrl('https://my-unleash-server.com/api/')
->withInstanceId('Some instance id')
->withContextProvider(new MyContextProvider())
->withStrategy(new AccountIdUnleashStrategy())
->build();
The strategy is then simply created in Unleash where you add an accountIds
field of type list
and mark it as required. Note that this strategy could also be defined using a Gradual rollout strategy with constraints, but I think having a custom one like that provides a better developer experience.
One downside to custom strategies is that if you use them in different projects, you need to create them in each project and the behavior must be the same (meaning the same context fields and the same implementation even across languages).
Unleash in Symfony
The Unleash Symfony bundle handles most of the configuration for you and offers additional features, such as:
#[IsEnabled]
attribute for controller routes- Automatic user ID if the Symfony Security component is configured
- Automatic integration with the Symfony http request object, like fetching the remote IP from it instead of from the $_SERVER array
- Automatic environment context value based on the kernel environment
- Custom context properties configured either as static values, as Expression Language expressions or provided via an event listener
- Twig functions, tags, tests and filters
- Automatically registered custom strategies, you simply implement them and Unleash knows about them
- and more
Additional notes
There are many other Unleash features I haven’t covered, such as the frontend proxy (which handles evaluation and prevents client-side state leakage). Some advanced features are better suited for official documentation rather than a blog post.