Skip to content

Commit 37e7eaf

Browse files
committed
add ability to load flags from a file
1 parent 7543cf3 commit 37e7eaf

File tree

6 files changed

+222
-1
lines changed

6 files changed

+222
-1
lines changed

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ a Redis cache operating in your production environment. The ld-relay offers many
9191
'redis_port' => 6379
9292
]);
9393

94+
Using flag data from a file
95+
---------------------------
96+
97+
For testing purposes, the SDK can be made to read feature flag state from a file or files instead of connecting to LaunchDarkly. See [`FileDataFeatureRequester`](https://github.com/launchdarkly/node-client/blob/master/FileDataFeatureRequester.php) for more details.
98+
9499
Testing
95100
-------
96101

@@ -119,9 +124,9 @@ About LaunchDarkly
119124
* [JavaScript](http://docs.launchdarkly.com/docs/js-sdk-reference "LaunchDarkly JavaScript SDK")
120125
* [PHP](http://docs.launchdarkly.com/docs/php-sdk-reference "LaunchDarkly PHP SDK")
121126
* [Python](http://docs.launchdarkly.com/docs/python-sdk-reference "LaunchDarkly Python SDK")
122-
* [Python Twisted](http://docs.launchdarkly.com/docs/python-twisted-sdk-reference "LaunchDarkly Python Twisted SDK")
123127
* [Go](http://docs.launchdarkly.com/docs/go-sdk-reference "LaunchDarkly Go SDK")
124128
* [Node.JS](http://docs.launchdarkly.com/docs/node-sdk-reference "LaunchDarkly Node SDK")
129+
* [Electron](http://docs.launchdarkly.com/docs/electron-sdk-reference "LaunchDarkly Electron SDK")
125130
* [.NET](http://docs.launchdarkly.com/docs/dotnet-sdk-reference "LaunchDarkly .Net SDK")
126131
* [Ruby](http://docs.launchdarkly.com/docs/ruby-sdk-reference "LaunchDarkly Ruby SDK")
127132
* [iOS](http://docs.launchdarkly.com/docs/ios-sdk-reference "LaunchDarkly iOS SDK")
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<?php
2+
namespace LaunchDarkly;
3+
4+
use Psr\Log\LoggerInterface;
5+
6+
/**
7+
* This component allows you to use local files as a source of feature flag state. This would
8+
* typically be used in a test environment, to operate using a predetermined feature flag state
9+
* without an actual LaunchDarkly connection.
10+
* <p>
11+
* To use this component, create an instance of this class, passing the path(s) of your data
12+
* file(s). Then place the resulting object in your LaunchDarkly client configuration with the
13+
* key "feature_requester".
14+
* <pre>
15+
* $file_data = new FileDataFeatureRequester("./testData/flags.json");
16+
* $config = array("feature_requester" => $file_data, "send_events" => false);
17+
* $client = new LDClient("sdk_key", $config);
18+
* </pre>
19+
* <p>
20+
* This will cause the client <i>not</i> to connect to LaunchDarkly to get feature flags. (Note
21+
* that in this example, <code>send_events</core> is also set to false so that it will not
22+
* connect to LaunchDarkly to send analytics events either.)
23+
* <p>
24+
*/
25+
class FileDataFeatureRequester implements FeatureRequester
26+
{
27+
/** @var array */
28+
private $_filePaths;
29+
/** @var array */
30+
private $_flags;
31+
/** @var array */
32+
private $_segments;
33+
/** @var LoggerInterface */
34+
private $_logger;
35+
36+
public function __construct($filePaths, $options = array())
37+
{
38+
$this->_filePaths = is_array($filePaths) ? $filePaths : array($filePaths);
39+
$this->_options = $options;
40+
$this->_flags = array();
41+
$this->_segments = array();
42+
$this->_logger = isset($options['logger']) ? $options['logger'] : null;
43+
$this->readAllData();
44+
}
45+
46+
/**
47+
* Gets an individual feature flag
48+
*
49+
* @param $key string feature key
50+
* @return FeatureFlag|null The decoded FeatureFlag, or null if missing
51+
*/
52+
public function getFeature($key)
53+
{
54+
return isset($this->_flags[$key]) ? $this->_flags[$key] : null;
55+
}
56+
57+
/**
58+
* Gets an individual user segment
59+
*
60+
* @param $key string segment key
61+
* @return Segment|null The decoded Segment, or null if missing
62+
*/
63+
public function getSegment($key)
64+
{
65+
return isset($this->_segments[$key]) ? $this->_segments[$key] : null;
66+
}
67+
68+
/**
69+
* Gets all feature flags
70+
*
71+
* @return array()|null The decoded FeatureFlags, or null if missing
72+
*/
73+
public function getAllFeatures()
74+
{
75+
return $this->_flags;
76+
}
77+
78+
private function readAllData()
79+
{
80+
$flags = array();
81+
$segments = array();
82+
foreach ($this->_filePaths as $filePath) {
83+
$this->loadFile($filePath, $flags, $segments);
84+
}
85+
$this->_flags = $flags;
86+
$this->_segments = $segments;
87+
}
88+
89+
private function loadFile($filePath, &$flags, &$segments)
90+
{
91+
$content = file_get_contents($filePath);
92+
$data = json_decode($content, true);
93+
if ($data == null) {
94+
throw new \InvalidArgumentException("File is not valid JSON: " + $filePath);
95+
}
96+
if (isset($data['flags'])) {
97+
foreach ($data['flags'] as $key => $value) {
98+
$flag = FeatureFlag::decode($value);
99+
$this->tryToAdd($flags, $key, $flag, "feature flag");
100+
}
101+
}
102+
if (isset($data['flagValues'])) {
103+
foreach ($data['flagValues'] as $key => $value) {
104+
$flag = FeatureFlag::decode(array(
105+
"key" => $key,
106+
"version" => 1,
107+
"on" => false,
108+
"prerequisites" => array(),
109+
"salt" => "",
110+
"targets" => array(),
111+
"rules" => array(),
112+
"fallthrough" => array(),
113+
"offVariation" => 0,
114+
"variations" => array($value),
115+
"deleted" => false,
116+
"trackEvents" => false,
117+
"clientSide" => false
118+
));
119+
$this->tryToAdd($flags, $key, $flag, "feature flag");
120+
}
121+
}
122+
if (isset($data['segments'])) {
123+
foreach ($data['segments'] as $key => $value) {
124+
$segment = Segment::decode($value);
125+
$this->tryToAdd($segments, $key, $segment, "user segment");
126+
}
127+
}
128+
}
129+
130+
private function tryToAdd(&$array, $key, $item, $kind) {
131+
if (isset($array[$key])) {
132+
throw new \InvalidArgumentException("File data contains more than one " . $kind . " with key: " . $key);
133+
} else {
134+
$array[$key] = $item;
135+
}
136+
}
137+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
namespace LaunchDarkly\Tests;
3+
4+
use LaunchDarkly\FileDataFeatureRequester;
5+
use LaunchDarkly\LDUser;
6+
7+
class FileDataFeatureRequesterTest extends \PHPUnit_Framework_TestCase
8+
{
9+
public function testLoadsFile()
10+
{
11+
$fr = new FileDataFeatureRequester("./tests/filedata/all-properties.json");
12+
$flag1 = $fr->getFeature("flag1");
13+
$this->assertEquals("flag1", $flag1->getKey());
14+
$flag2 = $fr->getFeature("flag2");
15+
$this->assertEquals("flag2", $flag2->getKey());
16+
$seg1 = $fr->getSegment("seg1");
17+
$this->assertEquals("seg1", $seg1->getKey());
18+
}
19+
20+
public function testLoadsMultipleFiles()
21+
{
22+
$fr = new FileDataFeatureRequester(array("./tests/filedata/flag-only.json",
23+
"./tests/filedata/segment-only.json"));
24+
$flag1 = $fr->getFeature("flag1");
25+
$this->assertEquals("flag1", $flag1->getKey());
26+
$seg1 = $fr->getSegment("seg1");
27+
$this->assertEquals("seg1", $seg1->getKey());
28+
}
29+
30+
public function testShortcutFlagCanBeEvaluated()
31+
{
32+
$fr = new FileDataFeatureRequester("./tests/filedata/all-properties.json");
33+
$flag2 = $fr->getFeature("flag2");
34+
$this->assertEquals("flag2", $flag2->getKey());
35+
$result = $flag2->evaluate(new LDUser("user"), null);
36+
$this->assertEquals("value2", $result->getDetail()->getValue());
37+
}
38+
}

tests/filedata/all-properties.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"flags": {
3+
"flag1": {
4+
"key": "flag1",
5+
"on": true,
6+
"fallthrough": {
7+
"variation": 2
8+
},
9+
"variations": [ "fall", "off", "on" ]
10+
}
11+
},
12+
"flagValues": {
13+
"flag2": "value2"
14+
},
15+
"segments": {
16+
"seg1": {
17+
"key": "seg1",
18+
"include": ["user1"]
19+
}
20+
}
21+
}

tests/filedata/flag-only.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"flags": {
3+
"flag1": {
4+
"key": "flag1",
5+
"on": true,
6+
"fallthrough": {
7+
"variation": 2
8+
},
9+
"variations": [ "fall", "off", "on" ]
10+
}
11+
}
12+
}

tests/filedata/segment-only.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"segments": {
3+
"seg1": {
4+
"key": "seg1",
5+
"include": ["user1"]
6+
}
7+
}
8+
}

0 commit comments

Comments
 (0)