Created
September 15, 2021 13:29
-
-
Save antonshell/73cae2793c2b8597bca43c508834dfc0 to your computer and use it in GitHub Desktop.
Feature Flag Service - PHP Client
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// src/Service/FeatureFlagService.php | |
declare(strict_types=1); | |
namespace App\Service; | |
use GuzzleHttp\ClientInterface; | |
use Symfony\Component\HttpFoundation\Request; | |
use Symfony\Component\HttpFoundation\Response; | |
class FeatureFlagService | |
{ | |
private const FEATURE_PRODUCT_ORDERING = 'product-ordering'; | |
private const FEATURE_DEMO = 'demo-feature'; | |
private const ERROR_CANT_READ_FEATURE_VALUE = 'Error while reading value for feature %s. Code: %s, Content: %s.'; | |
private const ENV_PROD = 'prod'; | |
private const ENV_STAGE = 'stage'; | |
private const ENV_DEV = 'dev'; | |
private const ENV_TEST = 'test'; | |
private array $featuresValues = []; | |
public function __construct( | |
private string $baseUrl, | |
private string $environment, | |
private string $readKey, | |
private ClientInterface $client, | |
) { | |
} | |
public function isProductOrderingEnabled(): bool | |
{ | |
return $this->getFeatureValue(self::FEATURE_PRODUCT_ORDERING); | |
} | |
public function isDemoFeatureEnabled(): bool | |
{ | |
return $this->getFeatureValue(self::FEATURE_DEMO); | |
} | |
private function getFeatureValue(string $feature): bool | |
{ | |
// cashing feature values | |
if (isset($this->featuresValues[$feature])) { | |
return $this->featuresValues[$feature]; | |
} | |
// normalize environment | |
if (!in_array($this->environment, [self::ENV_PROD, self::ENV_STAGE, self::ENV_DEV, self::ENV_TEST])) { | |
$this->environment = self::ENV_DEV; | |
} | |
// send request to feature-flags service | |
$url = sprintf('%s/%s/%s', $this->baseUrl, $feature, $this->environment); | |
$response = $this->client->request(Request::METHOD_GET, $url, [ | |
'headers' => [ | |
'Authorization' => sprintf('bearer %s', $this->readKey), | |
], | |
]); | |
// handle bad responses | |
$content = $response->getBody()->getContents(); | |
$contentArray = json_decode($content, true); | |
if (Response::HTTP_OK !== $response->getStatusCode() || !isset($contentArray['enabled'])) { | |
$message = sprintf( | |
self::ERROR_CANT_READ_FEATURE_VALUE, | |
$feature, | |
$response->getStatusCode(), | |
$content | |
); | |
throw new \Exception($message); | |
} | |
$value = (bool) $contentArray['enabled']; | |
$this->featuresValues[$feature] = $value; | |
return $value; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// tests/Unit/Service/FeatureFlagServiceTest.php | |
declare(strict_types=1); | |
namespace App\Tests\Unit\Service; | |
use App\Service\FeatureFlagService; | |
use GuzzleHttp\Client; | |
use GuzzleHttp\Psr7\Response; | |
use GuzzleHttp\Psr7\Utils; | |
use PHPUnit\Framework\TestCase; | |
class FeatureFlagServiceTest extends TestCase | |
{ | |
private const BASE_URL = 'https://feature-flags.antonshell.me/feature/antonshell/demo'; | |
private const ENVIRONMENT = 'prod'; | |
private const READ_KEY = 'test_read_key'; | |
private const WRONG_ENVIRONMENT = 'wrong_env'; | |
private Client $client; | |
protected function setUp(): void | |
{ | |
$this->client | |
= $this->getMockBuilder(Client::class) | |
->disableOriginalConstructor() | |
->getMock() | |
; | |
} | |
protected function tearDown(): void | |
{ | |
unset($this->client); | |
} | |
public function testIsProductOrderingEnabled(): void | |
{ | |
$stream = Utils::streamFor(json_encode([ | |
'status' => 'ok', | |
'feature' => 'product-ordering', | |
'environment' => 'prod', | |
'enabled' => true, | |
], JSON_THROW_ON_ERROR)); | |
$response = new Response(200, ['Content-Type' => 'application/json'], $stream); | |
$this->client | |
->expects(self::once()) | |
->method('request') | |
->with('GET', 'https://feature-flags.antonshell.me/feature/antonshell/demo/product-ordering/prod', [ | |
'headers' => [ | |
'Authorization' => 'bearer test_read_key', | |
], | |
]) | |
->willReturn($response) | |
; | |
self::assertTrue($this->getFeatureFlagService()->isProductOrderingEnabled()); | |
} | |
public function testIsDemoFeatureEnabled(): void | |
{ | |
$stream = Utils::streamFor(json_encode([ | |
'status' => 'ok', | |
'feature' => 'demo-feature', | |
'environment' => 'prod', | |
'enabled' => true, | |
], JSON_THROW_ON_ERROR)); | |
$response = new Response(200, ['Content-Type' => 'application/json'], $stream); | |
$this->client | |
->expects(self::once()) | |
->method('request') | |
->with('GET', 'https://feature-flags.antonshell.me/feature/antonshell/demo/demo-feature/prod', [ | |
'headers' => [ | |
'Authorization' => 'bearer test_read_key', | |
], | |
]) | |
->willReturn($response) | |
; | |
self::assertTrue($this->getFeatureFlagService()->isDemoFeatureEnabled()); | |
} | |
public function testGetFeatureValueForWrongEnvironment(): void | |
{ | |
$stream = Utils::streamFor(json_encode([ | |
'status' => 'ok', | |
'feature' => 'demo-feature', | |
'environment' => self::WRONG_ENVIRONMENT, | |
'enabled' => true, | |
], JSON_THROW_ON_ERROR)); | |
$response = new Response(200, ['Content-Type' => 'application/json'], $stream); | |
$this->client | |
->expects(self::once()) | |
->method('request') | |
->with('GET', 'https://feature-flags.antonshell.me/feature/antonshell/demo/demo-feature/dev', [ | |
'headers' => [ | |
'Authorization' => 'bearer test_read_key', | |
], | |
]) | |
->willReturn($response) | |
; | |
self::assertTrue($this->getFeatureFlagService(self::WRONG_ENVIRONMENT)->isDemoFeatureEnabled()); | |
} | |
public function testGetFeatureValueError(): void | |
{ | |
$stream = Utils::streamFor(json_encode([ | |
'status' => 403, | |
'message' => 'Invalid access token provided', | |
], JSON_THROW_ON_ERROR)); | |
$response = new Response(403, ['Content-Type' => 'application/json'], $stream); | |
$this->client | |
->expects(self::once()) | |
->method('request') | |
->with('GET', 'https://feature-flags.antonshell.me/feature/antonshell/demo/demo-feature/prod', [ | |
'headers' => [ | |
'Authorization' => 'bearer test_read_key', | |
], | |
]) | |
->willReturn($response) | |
; | |
$this->expectException(\Exception::class); | |
$this->expectExceptionMessage('Error while reading value for feature demo-feature. Code: 403, Content: {"status":403,"message":"Invalid access token provided"}.'); | |
$this->getFeatureFlagService()->isDemoFeatureEnabled(); | |
} | |
public function testGetFeatureValueCaching(): void | |
{ | |
$featureFlagService = $this->getFeatureFlagService(); | |
$reflection = new \ReflectionClass($featureFlagService); | |
$property = $reflection->getProperty('featuresValues'); | |
$property->setAccessible(true); | |
$property->setValue($featureFlagService, ['demo-feature' => true]); | |
self::assertTrue($featureFlagService->isDemoFeatureEnabled()); | |
} | |
private function getFeatureFlagService(string $environment = self::ENVIRONMENT): FeatureFlagService | |
{ | |
return new FeatureFlagService( | |
self::BASE_URL, | |
$environment, | |
self::READ_KEY, | |
$this->client, | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment