Skip to content

Commit bebd1a8

Browse files
Add JSONC and cache support to feature flag service
1 parent 53f5a0b commit bebd1a8

File tree

4 files changed

+68
-11
lines changed

4 files changed

+68
-11
lines changed

config/packages/drenso_shared.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ drenso_shared:
4545
enabled: true # Enable the feature flags service and attributes (automatically true when you specify any of the options below and omit this line)
4646
configuration_file: '' # The JSON file location where your flags are configured, for example '%env(resolve:FEATURES_FILE)%'
4747
configuration_local_file: '' # Not required, but can be used to override flags in the main configuration file outside of version control
48+
json_comment_parser_enabled: true # JSONC supported by default
4849
gravatar:
4950
enabled: true # Enable the GravatarHelper for injection (automatically true when you specify any of the options below and omit this line)
5051
fallback_style: 'mp' # Define a fallback style for accounts without a gravatar, see https://en.gravatar.com/site/implement/images/#default-image

src/DependencyInjection/Configuration.php

+3
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,9 @@ private function configureServices(ArrayNodeDefinition $node): void
270270
->scalarNode('configuration_local_file')
271271
->defaultNull()
272272
->end() // configuration_local_file
273+
->booleanNode('json_comment_parser_enabled')
274+
->defaultTrue()
275+
->end() // json_comment_parser_enabled
273276
->scalarNode('custom_handler')
274277
->defaultNull()
275278
->end() // custom_handler

src/DependencyInjection/DrensoSharedExtension.php

+8-5
Original file line numberDiff line numberDiff line change
@@ -290,15 +290,18 @@ private function configureServices(ContainerBuilder $container, array $config, b
290290
{
291291
$services = $config['services'];
292292

293-
if ($services['feature_flags']['enabled']) {
294-
if (!$services['feature_flags']['custom_handler']) {
293+
$featureFlags = $services['feature_flags'];
294+
if ($featureFlags['enabled']) {
295+
if (!$featureFlags['custom_handler']) {
295296
$container
296297
->register(FeatureFlagsInterface::class, FeatureFlags::class)
297-
->setArgument('$configuration', $services['feature_flags']['configuration_file'])
298-
->setArgument('$configurationOverride', $services['feature_flags']['configuration_local_file'] ?? '')
298+
->setArgument('$configuration', $featureFlags['configuration_file'])
299+
->setArgument('$configurationOverride', $featureFlags['configuration_local_file'] ?? '')
300+
->setArgument('$jsonCommentParserEnabled', $featureFlags['json_comment_parser_enabled'])
301+
->setArgument('$appCache', new Reference(CacheInterface::class, ContainerInterface::NULL_ON_INVALID_REFERENCE))
299302
->setPublic($public);
300303
} else {
301-
$container->setAlias(FeatureFlagsInterface::class, $services['feature_flags']['custom_handler']);
304+
$container->setAlias(FeatureFlagsInterface::class, $featureFlags['custom_handler']);
302305
}
303306

304307
$container

src/FeatureFlags/FeatureFlags.php

+56-6
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,20 @@
33
namespace Drenso\Shared\FeatureFlags;
44

55
use Symfony\Component\DependencyInjection\Exception\EnvNotFoundException;
6+
use Symfony\Contracts\Cache\CacheInterface;
67

78
class FeatureFlags implements FeatureFlagsInterface
89
{
10+
private const CACHE_KEY_MTIME = 'drenso.feature_flags.mtime';
11+
private const CACHE_KEY_CONFIG = 'drenso.feature_flags.configuration';
12+
913
private ?array $resolvedConfiguration = null;
1014

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)
1220
{
1321
}
1422

@@ -31,26 +39,68 @@ public function getFlagValue(string $flag): bool
3139
private function resolve(): void
3240
{
3341
if (null !== $this->resolvedConfiguration) {
42+
// Configuration already resolved, direct return
3443
return;
3544
}
3645

3746
if (!file_exists($this->configuration) || !is_readable($this->configuration)) {
3847
throw new EnvNotFoundException(sprintf('Could not find features file %s', $this->configuration));
3948
}
4049

50+
$overrideAvailable = true;
4151
if (!$this->configurationOverride
4252
|| !file_exists($this->configurationOverride)
4353
|| !is_readable($this->configurationOverride)) {
4454
// 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;
4862
}
4963

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(
5292
json_decode($configuration, true, flags: JSON_THROW_ON_ERROR),
5393
json_decode($configurationOverride, true, flags: JSON_THROW_ON_ERROR),
5494
);
5595
}
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+
}
56106
}

0 commit comments

Comments
 (0)