Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -230,4 +230,4 @@ PASSPORT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
-----END PUBLIC KEY-----"

FEATURE_FLAG_API_TOKEN=alovelytoken
FEATURE_FLAGGING_CONFIG_URL=https://raw.githubusercontent.com/HDRUK/hdruk-feature-configurations/refs/heads/main/dev/features.json
FEATURE_FLAGGING_CONFIG_URL=https://storage.googleapis.com/hdr-gw-feature-configurations-storage-bucket-prod/dev/features.json
103 changes: 103 additions & 0 deletions app/FeatureFlags/FeatureFlagManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php

namespace App\FeatureFlags;

use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Http;

class FeatureFlagManager
{
protected Filesystem $storage;
protected array $features = [];

public function __construct()
{
//$this->storage = app('filesystem')->disk('gcs.feature_flags');
$this->loadFeatures();
}

protected function loadFeatures(): void
{
Log::info('Loading feature flags...');

$this->features = cache()->remember('feature_flags.json', now()->addMinutes(30), function () {
if (app()->environment('local')) {
Log::info('Local environment detected. Using HTTP Call, no cache.');
return [
'SDEConciergeServiceEnquiry' => ['enabled' => true],
'Aliases' => ['enabled' => true],
];
// $url = env('FEATURE_FLAGGING_CONFIG_URL');
// $res = Http::get($url);
// if ($res->successful()) {
// return $res->json();
// }

// logger()->error('Failed to fetch feature flags from URL', ['url' => $url]);
// return [];
}

try {
try {
$json = $this->storage->get('features.json');
Log::info('Successfully fetched features.json from GCS.');
} catch (\Throwable $e) {
Log::error('Error accessing GCS bucket for features.json: ' . $e->getMessage());
return [];
}

$features = json_decode($json, true);

if (json_last_error() !== JSON_ERROR_NONE) {
Log::error('Failed to decode features.json: ' . json_last_error_msg());
return [];
}

Log::info('Feature flags loaded and parsed successfully.', ['features' => $features]);

return $features;

} catch (\Throwable $e) {
Log::error('Unexpected error loading features.json: ' . $e->getMessage());
return [];
}
});
}



public function reload(): void
{
Log::info('Reloading feature flags: Clearing cache and reloading...');
cache()->forget('feature_flags.json');
$this->loadFeatures();
}

public function getEnabledFeatures(): array
{
$this->loadFeatures();

$enabledFeatures = array_filter($this->features, function ($feature) {
return is_array($feature) && ($feature['enabled'] ?? false) === true;
});

Log::info('Fetching all enabled feature flags.', ['enabled_features' => array_keys($enabledFeatures)]);

return $enabledFeatures;
}

public function resolve(string $feature, mixed $scope = null): mixed
{
$this->loadFeatures();

$value = $this->features[$feature] ?? false;
Log::info('Resolving feature flag.', [
'feature' => $feature,
'resolved_value' => $value,
'scope' => $scope,
]);

return $value;
}
}
42 changes: 8 additions & 34 deletions app/Http/Controllers/Api/V1/FeatureFlagController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@

use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
use App\Http\Controllers\Controller;
use Laravel\Pennant\Feature;
use App\Services\FeatureFlagManager;
use App\FeatureFlags\FeatureFlagManager;

class FeatureFlagController extends Controller
{
Expand Down Expand Up @@ -51,7 +49,7 @@ class FeatureFlagController extends Controller
* )
* )
*/
public function index(Request $request, FeatureFlagManager $flagManager): JsonResponse
public function reload(FeatureFlagManager $resolver): JsonResponse
{
$featureFlagToken = env('FEATURE_FLAG_API_TOKEN');
$authHeader = $request->header('Authorization');
Expand All @@ -67,37 +65,13 @@ public function index(Request $request, FeatureFlagManager $flagManager): JsonRe
Log::warning('Invalid API token', ['provided' => $providedToken]);
return response()->json(['message' => 'Unauthorized: Invalid token.'], 401);
}
Cache::forget('getAllFlags');
Cache::forget('feature_flags');

$resolver->reload();

$url = env('FEATURE_FLAGGING_CONFIG_URL');
return response()->json([
'message' => 'Feature flags reloaded from GCS.'
]);

if (app()->environment('testing') || !$url) {
return response()->json(['message' => 'Feature flagging disabled in this environment.'], 200);
}

$res = Http::get($url);

if (!$res->successful()) {
Log::error('Failed to fetch feature flags from GitHub', ['url' => $url]);
return response()->json(['message' => 'Failed to fetch feature flags.'], 500);
}

$featureFlags = $res->json();




if (!is_array($featureFlags)) {
return response()->json(['message' => 'Invalid feature flag format.'], 422);
}

Log::info("Using feature flags from Bucket: " . print_r($featureFlags, true));

$flagManager->define($featureFlags);

return response()->json(['message' => 'Feature flags defined successfully.'], 200);
}
/**
* @OA\Get(
Expand All @@ -115,9 +89,9 @@ public function index(Request $request, FeatureFlagManager $flagManager): JsonRe
* )
* )
*/
public function getEnabledFeatures(Request $request, FeatureFlagManager $flagManager): JsonResponse
public function getEnabledFeatures(FeatureFlagManager $resolver): JsonResponse
{
$allFlags = $flagManager->getAllFlags();
$allFlags = $resolver->getEnabledFeatures();

return response()->json($allFlags, 200);
}
Expand Down
46 changes: 11 additions & 35 deletions app/Providers/FeatureServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,23 @@

namespace App\Providers;

use Illuminate\Support\Facades\Http;
use Illuminate\Support\ServiceProvider;
use App\Services\FeatureFlagManager;
use Illuminate\Support\Facades\Cache;
use Laravel\Pennant\Feature;
use App\FeatureFlags\FeatureFlagManager;

class FeatureServiceProvider extends ServiceProvider
{
public function boot()
public function register(): void
{
$this->app->booted(function () {
logger()->info('Starting features');
$url = env('FEATURE_FLAGGING_CONFIG_URL');

if (app()->environment('testing') || !$url) {
return;
}

$featureFlags = Cache::remember('feature_flags', now()->addMinutes(10), function () use ($url) {
logger()->info('Calling that Bucket');
$res = Http::retry(3, 5000, function ($exception, $requestNumber) use ($url) {
logger()->warning('Retrying feature flag fetch', [
'url' => $url,
'attempt' => $requestNumber,
'error' => $exception->getMessage(),
]);
})->get($url);

if (!$res->successful()) {
logger()->error('Failed to fetch feature flags', ['url' => $url, 'status' => $res->status()]);
}

return $res->successful() ? $res->json() : [];
});

if (is_array($featureFlags) && !empty($featureFlags)) {
app(FeatureFlagManager::class)->define($featureFlags);
} else {
logger()->warning('No feature flags were defined - empty or failed response.', ['url' => $url]);
}
$this->app->singleton(FeatureFlagManager::class, function ($app) {
return new FeatureFlagManager();
});
}


public function boot(): void
{
Feature::resolveScopeUsing(
$this->app->make(FeatureFlagManager::class)
);
}
}
59 changes: 0 additions & 59 deletions app/Services/FeatureFlagManager.php

This file was deleted.

11 changes: 11 additions & 0 deletions config/filesystems.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,17 @@
],

'gcs' => [
'feature_flags' => [
'driver' => 'gcs',
'key_file_path' => env('GOOGLE_APPLICATION_CREDENTIALS'),
'project_id' => env('GOOGLE_CLOUD_PROJECT_ID'),
'bucket' => env('GOOGLE_CLOUD_FEATURE_FLAGS_BUCKET'),
'path_prefix' => env('GOOGLE_CLOUD_FEATURE_FLAGS_PATH_PREFIX', ''),
'storage_api_uri' => env('GOOGLE_CLOUD_STORAGE_API_URI', null),
'apiEndpoint' => env('GOOGLE_CLOUD_STORAGE_API_ENDPOINT', null),
'visibility' => 'public',
'throw' => false,
],
'unscanned' => [
'driver' => 'gcs',
'key_file_path' => env('GOOGLE_APPLICATION_CREDENTIALS'),
Expand Down