Skip to content

Instantly share code, notes, and snippets.

@antonshell
Created September 15, 2021 13:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save antonshell/73cae2793c2b8597bca43c508834dfc0 to your computer and use it in GitHub Desktop.
Save antonshell/73cae2793c2b8597bca43c508834dfc0 to your computer and use it in GitHub Desktop.
Feature Flag Service - PHP Client
<?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;
}
}
<?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