3
3
namespace Drenso \Shared \FeatureFlags ;
4
4
5
5
use Symfony \Component \DependencyInjection \Exception \EnvNotFoundException ;
6
+ use Symfony \Contracts \Cache \CacheInterface ;
6
7
7
8
class FeatureFlags implements FeatureFlagsInterface
8
9
{
10
+ private const CACHE_KEY_MTIME = 'drenso.feature_flags.mtime ' ;
11
+ private const CACHE_KEY_CONFIG = 'drenso.feature_flags.configuration ' ;
12
+
9
13
private ?array $ resolvedConfiguration = null ;
10
14
11
- public function __construct (private readonly string $ configuration , private readonly string $ configurationOverride )
15
+ public function __construct (
16
+ private readonly string $ configuration ,
17
+ private readonly string $ configurationOverride ,
18
+ private readonly bool $ jsonCommentParserEnabled ,
19
+ private readonly ?CacheInterface $ appCache = null )
12
20
{
13
21
}
14
22
@@ -31,26 +39,68 @@ public function getFlagValue(string $flag): bool
31
39
private function resolve (): void
32
40
{
33
41
if (null !== $ this ->resolvedConfiguration ) {
42
+ // Configuration already resolved, direct return
34
43
return ;
35
44
}
36
45
37
46
if (!file_exists ($ this ->configuration ) || !is_readable ($ this ->configuration )) {
38
47
throw new EnvNotFoundException (sprintf ('Could not find features file %s ' , $ this ->configuration ));
39
48
}
40
49
50
+ $ overrideAvailable = true ;
41
51
if (!$ this ->configurationOverride
42
52
|| !file_exists ($ this ->configurationOverride )
43
53
|| !is_readable ($ this ->configurationOverride )) {
44
54
// No need to throw, as it is an override. Do set an empty default value here.
45
- $ configurationOverride = '{} ' ;
46
- } else {
47
- $ configurationOverride = file_get_contents ($ this ->configurationOverride ) ?? '{} ' ;
55
+ $ overrideAvailable = false ;
56
+ }
57
+
58
+ if (!$ this ->appCache ) {
59
+ $ this ->resolvedConfiguration = $ this ->parseConfiguration ($ overrideAvailable );
60
+
61
+ return ;
48
62
}
49
63
50
- $ configuration = file_get_contents ($ this ->configuration ) ?? '{} ' ;
51
- $ this ->resolvedConfiguration = array_merge (
64
+ // Validate modify times
65
+ $ currentMTime = max (
66
+ filemtime ($ this ->configuration ),
67
+ $ overrideAvailable ? filemtime ($ this ->configurationOverride ) : 0 ,
68
+ );
69
+
70
+ $ cachedMTime = $ this ->appCache ->get (self ::CACHE_KEY_MTIME , static fn (): int => 0 );
71
+ if ($ cachedMTime < $ currentMTime ) {
72
+ // Remove cached values as one of the files has been modified
73
+ $ this ->appCache ->delete (self ::CACHE_KEY_MTIME );
74
+ $ this ->appCache ->delete (self ::CACHE_KEY_CONFIG );
75
+ }
76
+
77
+ // Populate the cache
78
+ $ this ->resolvedConfiguration = $ this ->appCache
79
+ ->get (self ::CACHE_KEY_CONFIG , fn (): array => $ this ->parseConfiguration ($ overrideAvailable ));
80
+ $ this ->appCache ->get (self ::CACHE_KEY_MTIME , static fn (): int => $ currentMTime );
81
+ }
82
+
83
+ private function parseConfiguration (bool $ overrideAvailable ): array
84
+ {
85
+ $ configuration = file_get_contents ($ this ->configuration );
86
+ $ configurationOverride = $ overrideAvailable ? file_get_contents ($ this ->configurationOverride ) : false ;
87
+
88
+ $ configuration = $ configuration ? $ this ->filterComments ($ configuration ) : '{} ' ;
89
+ $ configurationOverride = $ configurationOverride ? $ this ->filterComments ($ configurationOverride ) : '{} ' ;
90
+
91
+ return array_merge (
52
92
json_decode ($ configuration , true , flags: JSON_THROW_ON_ERROR ),
53
93
json_decode ($ configurationOverride , true , flags: JSON_THROW_ON_ERROR ),
54
94
);
55
95
}
96
+
97
+ private function filterComments (string $ data ): string
98
+ {
99
+ if (!$ this ->jsonCommentParserEnabled ) {
100
+ return $ data ;
101
+ }
102
+
103
+ // Regex from https://stackoverflow.com/a/43439966
104
+ return preg_replace ('~ (" (?: \\\\. | [^"])*+ ") | // \V*+ | /\* .*? \*/ ~xs ' , '$1 ' , $ data );
105
+ }
56
106
}
0 commit comments