Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
FeatureFlagService
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
3 / 3
6
100.00% covered (success)
100.00%
1 / 1
 enabled
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 all
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 resolveFlags
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace App\Http\Services;
4
5use App\Http\Models\RemoteConfig;
6use Illuminate\Support\Facades\Cache;
7
8/**
9 * Thin wrapper around {@see RemoteConfig::feature_flags} that gives the rest
10 * of the codebase a single choke-point for boolean rollout toggles.
11 *
12 * Storage
13 * -------
14 * Flags live inside the single-row `remote_configs.feature_flags` document,
15 * alongside the existing `disable_old_linkedin_plugin` and
16 * `notification_daily_cap` entries. A flag is addressed by a namespaced
17 * string key (e.g. `roleplay.corporate_personas`) which becomes a plain
18 * array key in the stored document.
19 *
20 * Resolution order
21 * ----------------
22 *  1. RemoteConfig row (MongoDB, cached for 60 s by the underlying
23 *     {@see RemoteConfigService}, matching the existing consumer cache).
24 *  2. `config('features.<key>')` fallback — lets operators ship a default
25 *     without a live DB edit (e.g. in the `config/features.php` file).
26 *  3. Hard-coded `$default` supplied by the caller (last resort).
27 *
28 * This service is intentionally stateless and side-effect free — writing
29 * flags still goes through the existing RemoteConfig CMC-FE flow.
30 */
31class FeatureFlagService
32{
33    /**
34     * Explicit list of known flag keys.
35     *
36     * Kept in sync with the `config/features.php` file so both layers surface
37     * the same surface area. New flags should be registered here so they are
38     * discoverable by operators and test harnesses.
39     */
40    public const ROLEPLAY_CORPORATE_PERSONAS = 'roleplay.corporate_personas';
41
42    /**
43     * Cache key for the resolved feature_flags array — mirrors the
44     * RemoteConfig consumer cache so we avoid a second round-trip on the
45     * hot admin path.
46     */
47    private const CACHE_TTL_SECONDS = 60;
48
49    /**
50     * Return whether the given flag is enabled.
51     *
52     * @param  string  $key  Dot-namespaced flag key (e.g. `roleplay.corporate_personas`)
53     * @param  bool  $default  Default when neither the RemoteConfig nor the config/features.php layer supplies a value
54     * @return bool True when the flag is enabled
55     */
56    public function enabled(string $key, bool $default = false): bool
57    {
58        $flags = $this->resolveFlags();
59
60        // Support both the nested / dotted form and the flat form as a
61        // convenience — the stored document uses the flat key.
62        if (array_key_exists($key, $flags)) {
63            return (bool) $flags[$key];
64        }
65
66        $configValue = config('features.'.$key);
67        if ($configValue !== null) {
68            return (bool) $configValue;
69        }
70
71        return $default;
72    }
73
74    /**
75     * Raw flag map (for debugging / admin surfaces).
76     *
77     * @return array<string, mixed>
78     */
79    public function all(): array
80    {
81        return $this->resolveFlags();
82    }
83
84    /**
85     * Load the feature_flags array from the single-row RemoteConfig.
86     *
87     * @return array<string, mixed>
88     */
89    private function resolveFlags(): array
90    {
91        return Cache::remember('feature_flags:resolved', self::CACHE_TTL_SECONDS, function () {
92            $config = RemoteConfig::query()->first();
93
94            return is_array($config?->feature_flags) ? $config->feature_flags : [];
95        });
96    }
97}