Skip to content

Commit 2ea5c52

Browse files
authored
Merge pull request #32 from launchdarkly/eb/ch26319/file-data-source
add ability to load flags from a file
2 parents 529ec43 + 3887732 commit 2ea5c52

File tree

6 files changed

+245
-1
lines changed

6 files changed

+245
-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/php-client/blob/master/FileDataFeatureRequester.php) and ["Reading flags from a file"](https://docs.launchdarkly.com/docs/reading-flags-from-a-file).
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: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<?php
2+
namespace LaunchDarkly;
3+
4+
/**
5+
* This component allows you to use local files as a source of feature flag state. This would
6+
* typically be used in a test environment, to operate using a predetermined feature flag state
7+
* without an actual LaunchDarkly connection.
8+
* <p>
9+
* To use this component, create an instance of this class, passing the path(s) of your data
10+
* file(s). Then place the resulting object in your LaunchDarkly client configuration with the
11+
* key "feature_requester".
12+
* <pre>
13+
* $file_data = new FileDataFeatureRequester("./testData/flags.json");
14+
* $config = array("feature_requester" => $file_data, "send_events" => false);
15+
* $client = new LDClient("sdk_key", $config);
16+
* </pre>
17+
* <p>
18+
* This will cause the client <i>not</i> to connect to LaunchDarkly to get feature flags. (Note
19+
* that in this example, <code>send_events</core> is also set to false so that it will not
20+
* connect to LaunchDarkly to send analytics events either.)
21+
* <p>
22+
*/
23+
class FileDataFeatureRequester implements FeatureRequester
24+
{
25+
/** @var array */
26+
private $_filePaths;
27+
/** @var array */
28+
private $_flags;
29+
/** @var array */
30+
private $_segments;
31+
32+
public function __construct($filePaths)
33+
{
34+
$this->_filePaths = is_array($filePaths) ? $filePaths : array($filePaths);
35+
$this->_flags = array();
36+
$this->_segments = array();
37+
$this->readAllData();
38+
}
39+
40+
/**
41+
* Gets an individual feature flag
42+
*
43+
* @param $key string feature key
44+
* @return FeatureFlag|null The decoded FeatureFlag, or null if missing
45+
*/
46+
public function getFeature($key)
47+
{
48+
return isset($this->_flags[$key]) ? $this->_flags[$key] : null;
49+
}
50+
51+
/**
52+
* Gets an individual user segment
53+
*
54+
* @param $key string segment key
55+
* @return Segment|null The decoded Segment, or null if missing
56+
*/
57+
public function getSegment($key)
58+
{
59+
return isset($this->_segments[$key]) ? $this->_segments[$key] : null;
60+
}
61+
62+
/**
63+
* Gets all feature flags
64+
*
65+
* @return array()|null The decoded FeatureFlags, or null if missing
66+
*/
67+
public function getAllFeatures()
68+
{
69+
return $this->_flags;
70+
}
71+
72+
private function readAllData()
73+
{
74+
$flags = array();
75+
$segments = array();
76+
foreach ($this->_filePaths as $filePath) {
77+
$this->loadFile($filePath, $flags, $segments);
78+
}
79+
$this->_flags = $flags;
80+
$this->_segments = $segments;
81+
}
82+
83+
private function loadFile($filePath, &$flags, &$segments)
84+
{
85+
$content = file_get_contents($filePath);
86+
$data = json_decode($content, true);
87+
if ($data == null) {
88+
throw new \InvalidArgumentException("File is not valid JSON: " + $filePath);
89+
}
90+
if (isset($data['flags'])) {
91+
foreach ($data['flags'] as $key => $value) {
92+
$flag = FeatureFlag::decode($value);
93+
$this->tryToAdd($flags, $key, $flag, "feature flag");
94+
}
95+
}
96+
if (isset($data['flagValues'])) {
97+
foreach ($data['flagValues'] as $key => $value) {
98+
$flag = FeatureFlag::decode(array(
99+
"key" => $key,
100+
"version" => 1,
101+
"on" => false,
102+
"prerequisites" => array(),
103+
"salt" => "",
104+
"targets" => array(),
105+
"rules" => array(),
106+
"fallthrough" => array(),
107+
"offVariation" => 0,
108+
"variations" => array($value),
109+
"deleted" => false,
110+
"trackEvents" => false,
111+
"clientSide" => false
112+
));
113+
$this->tryToAdd($flags, $key, $flag, "feature flag");
114+
}
115+
}
116+
if (isset($data['segments'])) {
117+
foreach ($data['segments'] as $key => $value) {
118+
$segment = Segment::decode($value);
119+
$this->tryToAdd($segments, $key, $segment, "user segment");
120+
}
121+
}
122+
}
123+
124+
private function tryToAdd(&$array, $key, $item, $kind)
125+
{
126+
if (isset($array[$key])) {
127+
throw new \InvalidArgumentException("File data contains more than one " . $kind . " with key: " . $key);
128+
} else {
129+
$array[$key] = $item;
130+
}
131+
}
132+
}
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: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"flags": {
3+
"flag1": {
4+
"key": "flag1",
5+
"version": 1,
6+
"deleted": false,
7+
"on": true,
8+
"prerequisites": [],
9+
"targets": [],
10+
"rules": [],
11+
"fallthrough": {
12+
"variation": 2
13+
},
14+
"variations": [ "fall", "off", "on" ],
15+
"offVariation": 0,
16+
"salt": "",
17+
"trackEvents": false,
18+
"clientSide": false
19+
}
20+
},
21+
"flagValues": {
22+
"flag2": "value2"
23+
},
24+
"segments": {
25+
"seg1": {
26+
"key": "seg1",
27+
"version": 1,
28+
"deleted": false,
29+
"included": ["user1"],
30+
"excluded": [],
31+
"rules": [],
32+
"salt": ""
33+
}
34+
}
35+
}

tests/filedata/flag-only.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+
"version": 1,
6+
"deleted": false,
7+
"on": true,
8+
"prerequisites": [],
9+
"targets": [],
10+
"rules": [],
11+
"fallthrough": {
12+
"variation": 2
13+
},
14+
"variations": [ "fall", "off", "on" ],
15+
"offVariation": 0,
16+
"salt": "",
17+
"trackEvents": false,
18+
"clientSide": false
19+
}
20+
}
21+
}

tests/filedata/segment-only.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"segments": {
3+
"seg1": {
4+
"key": "seg1",
5+
"version": 1,
6+
"deleted": false,
7+
"included": ["user1"],
8+
"excluded": [],
9+
"rules": [],
10+
"salt": ""
11+
}
12+
}
13+
}

0 commit comments

Comments
 (0)