Feature Control System
README
What is it and why do I need it?
FCS (Feature Control System) is a simple library for feature-toggling and can be used to define list of features with simple and complex conditions and allowing to define environment settings and tags to resolve the status of each feature.
For example you can define features for specific users, specific active cookies, development environments, tags or even all of them combined in some logic pattern.
All your features can be managed from one place and easily updated to wider audience, rolled back to previous environment or completely removed.
FCS can be especially useful in Continues Integration workflow, where multiple changes are merged in short time to the mainline and not all of them are ready to be deployed to production.
It can be also used to limit some feature to very specific conditions, e.g. due to market regulations or conditions of some promotions.
It can be also useful for A/B testing and when enabling new features only for some segment of users to confirm new code working fine in real environment without risking issues for all the users.
How to configure list of features?
In case of small projects, you can simply keep configuration in array in some PHP or JSON file.
In case of bigger projects, it's better to load it from redis or other cache provider, to distribute the list of features and their flags among different code repositories.
In case of redis integration, it's best to prepare some simple deployment script in backoffice to easily update flags for every feature, ideally with basic user interface. This way it can be used in emergency by anyone in your team with permissions, including PM or QA or even marketing team on handling particular temporary promotions (offering access to only some flags for different users).
FCS
Configuration methods:
Example of code to configure some settings:
use \FeatureControlSystem\FCS;
FCS::Instance()->setTag('env', 'preproduction');
FCS::Instance()->setTag('username', 'john123');
FCS::Instance()->setTag('lang', 'en');
// set flags manually
FCS::Instance()->setFlags([
'my-feature' => ['flag' => FCS::FLAG_ACTIVE],
'my-feature2' => ['flag' => FCS::FLAG_TAG, 'name' => 'username', 'value' => 'john123']
]);
// or load flags from redis cache
$redis = new \Redis();
// ...
FCS::Instance()->loadFlags($redis, 'flags');
- setTag
Set a tag with a name and value
FCS::Instance()->setTag(string $name, string $value);
// e.g. FCS::Instance()->setTag('lang', 'en')
- setFlags
Register a list of features with their flags, that can be used to determine if feature is active or not
FCS::Instance()->setFlags(array $flags);
// e.g. FCS::Instance()->setFlags([
'my-feature' => ['flag' => FCS::FLAG_ACTIVE],
'my-feature2' => ['flag' => FCS::FLAG_TAG, 'name' => 'username', 'value' => 'john123'],
'my-feature3' => ['flag' => FCS::FLAG_TAG, 'name' => 'lang', 'value' => ['pl', 'de']]
])
- loadFlags
Register a list of features with their flags loaded from redis cache; data in redis needs to be encoded with JSON and stored under simple key
First argument can be either instance of Predis client or Redis PHP extension
FCS::Instance()->loadFlags(object $redis, string $key);
// e.g. FCS::Instance()->loadFlags($redis, 'flags')
Getter methods:
- feature
Check if feature is active and use returned boolean to enable some code
if (<bool> = FCS::Instance()->feature(string $name)) {
// code is enabled
}
// e.g. FCS::Instance()->feature('my-feature') === true
- getBodyCssClasses
Get string with list of CSS classes that can be appended to body element in your frontend
Returned CSS classes have "fcs-" prefix
<string> = FCS::Instance()->getBodyCssClasses();
// e.g. FCS::Instance()->getBodyCssClasses() === 'fcs-my-feature fcs-my-feature2 fcs-my-feature100'
- getActiveFeatures
Get array of names of features that can be for example injected to JS to enable some dynamic code on frontend
<array:string> = FCS::Instance()->getActiveFeatures();
// e.g. FCS::Instance()->getActiveFeatures() === ['my-feature', 'my-feature2', 'my-feature100']
Available flags:
use \FeatureControlSystem\FCS;
FCS::FLAG_INACTIVE = 0
FCS::FLAG_ACTIVE = 1
FCS::FLAG_COOKIE = 2
FCS::FLAG_TAG = 3
FCS::FLAG_MULTI_AND = 4
FCS::FLAG_MULTI_OR = 5
- FLAG_INACTIVE
Inactive flag means that feature is disabled for everyone
FCS::Instance()->setFlags([
'my-feature' => ['flag' => FCS::FLAG_INACTIVE] // FCS::FLAG_INACTIVE = 0
]);
if (FCS::Instance()->feature('my-feature')) { // false
// no one can see it
}
- FLAG_ACTIVE
Active flag means that feature is enabled for everyone
FCS::Instance()->setFlags([
'my-feature' => ['flag' => FCS::FLAG_ACTIVE] // FCS::FLAG_ACTIVE = 1
]);
if (FCS::Instance()->feature('my-feature')) { // true
// always working
}
- FLAG_COOKIE
Cookie flag means that feature is enabled for everyone who make request to the server with active cookie matching feature by its name
Cookies need to have "fcs-" prefix
Note: slash (/) in name of feature is automatically turned into double dash (--)
FCS::Instance()->setFlags([
'my-feature' => ['flag' => FCS::FLAG_COOKIE] // FCS::FLAG_COOKIE = 2
]);
$_COOKIE['fcs-my-feature'] = true;
if (FCS::Instance()->feature('my-feature')) { // true, if fcs-my-feature cookie exists
// working for anyone with enabled cookie "fcs-my-feature" in this case
}
- FLAG_TAG
Tag flag means that feature is enabled for a tag defined by name and for single value (string) or for list of values (array:string)
FCS::Instance()->setFlags([
'my-feature' => ['flag' => FCS::FLAG_TAG, 'name' => 'lang', 'value' => 'en'], // FCS::FLAG_TAG = 3
'my-feature2' => ['flag' => FCS::FLAG_TAG, 'name' => 'lang', 'value' => ['pl', 'de']] // FCS::FLAG_TAG = 3
]);
FCS::Instance()->setTag('lang', 'en');
if (FCS::Instance()->feature('my-feature')) { // true
// working for "lang" tag if its value is "en"
}
if (FCS::Instance()->feature('my-feature2')) { // false
// working for "lang" tag if its value is "pl" or "de"
}
- FLAG_MULTI_AND
Multi-and flag allows to define group of multiple flags and means that feature is enabled if all of the subflags meet their conditions
FCS::Instance()->setFlags([
'my-feature' => ['flag' => FCS::FLAG_MULTI_AND, 'flags' => [ // FCS::FLAG_MULTI_AND = 4
['flag' => FCS::FLAG_TAG, 'name' => 'domain', 'value' => 'domain.com'], // FCS::FLAG_TAG = 3
['flag' => FCS::FLAG_TAG, 'name' => 'lang', 'value' => 'en'] // FCS::FLAG_TAG = 3
]],
'my-feature2' => ['flag' => FCS::FLAG_MULTI_AND, 'flags' => [ // FCS::FLAG_MULTI_AND = 4
['flag' => FCS::FLAG_TAG, 'name' => 'domain', 'value' => 'domain.com'], // FCS::FLAG_TAG = 3
['flag' => FCS::FLAG_TAG, 'name' => 'lang', 'value' => 'pl'] // FCS::FLAG_TAG = 3
]]
]);
FCS::Instance()->setTag('domain', 'domain.com');
FCS::Instance()->setTag('lang', 'en');
if (FCS::Instance()->feature('my-feature')) { // true
// available only if domain tag is equal "domain.com" AND lang tag is "en"
}
if (FCS::Instance()->feature('my-feature2')) { // false
// available only if domain tag is equal "domain.com" AND lang tag is "pl"
}
- FLAG_MULTI_OR
Multi-or flag allows to define group of multiple flags and means that feature is enabled if any of the subflags meets its condition
FCS::Instance()->setFlags([
'my-feature' => ['flag' => FCS::FLAG_MULTI_OR, 'flags' => [ // FCS::FLAG_MULTI_OR = 5
['flag' => FCS::FLAG_TAG, 'name' => 'domain', 'value' => 'domain.com'], // FCS::FLAG_TAG = 3
['flag' => FCS::FLAG_TAG, 'name' => 'domain', 'value' => 'domain2.com'] // FCS::FLAG_TAG = 3
]],
'my-feature2' => ['flag' => FCS::FLAG_MULTI_OR, 'flags' => [ // FCS::FLAG_MULTI_OR = 5
['flag' => FCS::FLAG_TAG, 'name' => 'domain', 'value' => 'domain3.com'], // FCS::FLAG_TAG = 3
['flag' => FCS::FLAG_TAG, 'name' => 'domain', 'value' => 'domain4.com] // FCS::FLAG_TAG = 3
]]
]);
FCS::Instance()->setTag('domain', 'domain.com');
if (FCS::Instance()->feature('my-feature')) { // true
// available only if domain tag is equal "domain.com" OR "domain2.com"
}
if (FCS::Instance()->feature('my-feature2')) { // false
// available only if domain tag is equal "domain3.com" OR "domain4.com"
}
Examples:
use \FeatureControlSystem\FCS;
// Define initial settings for your environment + project + user, usually set just once at the beginning
FCS::Instance()->setTag('env', 'preproduction');
FCS::Instance()->setTag('username', 'test123');
FCS::Instance()->setTag('IP', $_SERVER['REMOTE_ADDR']);
FCS::Instance()->setTag('role', 'user');
FCS::Instance()->setTag('country', 'UK');
FCS::Instance()->setTag('product', 'product1');
FCS::Instance()->setTag('domain', 'domain.com');
FCS::Instance()->setTag('lang', 'en');
// Sometimes you can define also extra tags later
FCS::Instance()->setTag('section', 'promotions');
FCS::Instance()->setTag('promotion', '1');
// Define list of features from array (loaded from configuration file or cache provider e.g. redis)
FCS::Instance()->setFlags([
'experiment' => ['flag' => FCS::FLAG_INACTIVE],
'blue-background' => ['flag' => FCS::FLAG_TAG, 'name' => 'country', 'value' => ['UK', 'DE']],
'xmas-promo' => ['flag' => FCS::FLAG_MULTI_AND, 'flags' => [
['flag' => FCS::FLAG_TAG, 'name' => 'domain', 'value' => 'domain.com'],
['flag' => FCS::FLAG_TAG, 'name' => 'lang', 'value' => 'pl']
]],
'chat' => ['flag' => FCS::FLAG_MULTI_OR, 'flags' => [
['flag' => FCS::FLAG_COOKIE],
['flag' => FCS::FLAG_TAG, 'name' => 'env', 'value' => ['test123', 'test456']]
]],
'auth' => ['flag' => FCS::FLAG_ACTIVE],
'new-registration' => ['flag' => FCS::FLAG_TAG, 'name' => 'env', 'value' => 'preproduction'],
'my-carousel' => ['flag' => FCS::FLAG_COOKIE],
'new-bonus' => ['flag' => FCS::FLAG_TAG, 'name' => 'env', 'value' => ['test123', 'test456']]
]);
// Get list of features to inject to your frontend to JS variable
$activeFeatures = FCS::Instance()->getActiveFeatures(); // ['blue-background', 'chat', 'auth', 'new-registration', 'new-bonus']
// Get CSS classes to append them to <body> element
$cssClasses = FCS::Instance()->getBodyCssClasses(); // 'fcs-blue-background fcs-chat fcs-auth fcs-new-registration fcs-new-bonus'
// Activate features in PHP
if (FCS::Instance()->feature('auth')) { // true
// active feature
}
if (FCS::Instance()->feature('experiment')) { // false
// inactive feature
}
$_COOKIE['fcs-my-carousel'] = 1;
if (FCS::Instance()->feature('my-carousel')) { // true
// feature active if $_COOKIE['fcs-my-carousel'] cookie is enabled
}
if (FCS::Instance()->feature('new-bonus')) { // true
// feature active for this user when logged in (test123)
} else {
// old feature for everyone else
}
if (FCS::Instance()->feature('new-registration')) { // true
// feature active in preproduction environment
}
if (FCS::Instance()->feature('xmas-promo')) { // false
// feature active if both tags domain AND lang meet the current settings
}
if (FCS::Instance()->feature('chat')) { // true
// feature active if cookie is enabled ($_COOKIE['fcs-chat']) OR if one of listed users is logged in
}
License
MIT License