Skip to content

Commit

Permalink
fix(php): add error handler for json serde errors. add throws in the
Browse files Browse the repository at this point in the history
annotations

Signed-off-by: Roman Dmytrenko <[email protected]>
  • Loading branch information
erka committed Oct 7, 2024
1 parent bdff02b commit af35fa3
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 14 deletions.
3 changes: 2 additions & 1 deletion flipt-php/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"homepage": "https://flipt.io",
"require": {
"php": ">=8.0",
"guzzlehttp/guzzle": "^7"
"guzzlehttp/guzzle": "^7",
"psr/log": "^1.0|^2.0|^3.0"
},
"repositories": [
{
Expand Down
114 changes: 106 additions & 8 deletions flipt-php/src/Flipt/Client/FliptClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
use Flipt\Models\VariantEvaluationResult;
use Flipt\Models\DefaultBooleanEvaluationResult;
use Flipt\Models\DefaultVariantEvaluationResult;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;


final class FliptClient
Expand All @@ -16,42 +18,106 @@ final class FliptClient
protected string $namespace;
protected string $entityId;
protected array $context;
protected LoggerInterface $logger;


/**
* @param array<string, string> $context
*/
public function __construct(string|Client $host, string $namespace = "default", array $context = [], string $entityId = '', AuthenticationStrategy $authentication = null)
{
$this->authentication = $authentication;
$this->namespace = $namespace;
$this->context = $context;
$this->entityId = $entityId;
$this->client = (is_string($host)) ? new Client(['base_uri' => $host]) : $host;
$this->logger = new NullLogger();
}

/**
* Set logger to use
*/
public function setLogger(LoggerInterface $logger) {
$this->logger = $logger;
}

/**
* Returns the boolean evaluation result
*
* @param array<string, string> $context
*
* @throws \JsonException if request or response includes invalid json data
* @throws \Psr\Http\Client\ClientExceptionInterface if network or request error occurs
*/
public function boolean(string $name, $context = [], $entityId = NULL, $reference = ""): BooleanEvaluationResult
public function boolean(string $name, ?array $context = [], ?string $entityId = null, ?string $reference = ""): BooleanEvaluationResult
{
$response = $this->apiRequest('/evaluate/v1/boolean', $this->mergeRequestParams($name, $context, $entityId, $reference));
return new DefaultBooleanEvaluationResult($response['flagKey'], $response['enabled'], $response['reason'], $response['requestDurationMillis'], $response['requestId'], $response['timestamp']);
}


/**
* Returns the bool result or default
*
* @param string $name - the flag key
* @param bool $fallback - default value in case of error
*
* @param array<string, string> $context
*/
public function booleanValue(string $name, bool $fallback, ?array $context = [], ?string $entityId = null, ?string $reference = ""): bool
{
try {
return $this->boolean($name, $context, $entityId, $reference)->getEnabled();
} catch (\Throwable $e) {
$this->logger->error($e->getMessage());
}
return $fallback;
}

/**
* Returns the variant evaluation result
*
* @param array<string,string> $context
*
* @throws \JsonException if request or response includes invalid json data
* @throws \Psr\Http\Client\ClientExceptionInterface if network or request error occurs
*/
public function variant(string $name, $context = [], $entityId = NULL, $reference = ""): VariantEvaluationResult
public function variant(string $name, ?array $context = [], ?string $entityId = null, ?string $reference = ""): VariantEvaluationResult
{
$response = $this->apiRequest('/evaluate/v1/variant', $this->mergeRequestParams($name, $context, $entityId, $reference));
return new DefaultVariantEvaluationResult($response['flagKey'], $response['match'], $response['reason'], $response['requestDurationMillis'], $response['requestId'], $response['timestamp'], $response['segmentKeys'], $response['variantKey'], $response['variantAttachment']);
}


/**
* Returns the variant evaluation variantKey or default
*
* @param string $name - the flag key
* @param string $fallback - default value in case of error
*
* @param array<string,string> $context
*/
public function variantValue(string $name, string $fallback, ?array $context = [], ?string $entityId = null, ?string $reference = ""): string
{
try {
return $this->variant($name, $context, $entityId, $reference)->getVariantKey();
} catch (\Throwable $e) {
$this->logger->error($e->getMessage());
}
return $fallback;
}

/**
* Batch return evaluation requests
*
* @param array<string> $names
* @param array<string,string> $context
*
* @return array<mixed>
*
* @throws \JsonException if request or response includes invalid json data
* @throws \Psr\Http\Client\ClientExceptionInterface if network or request error occurs
*/
public function batch(array $names, $context = [], $entityId = NULL, $reference = ""): array
public function batch(array $names, $context = [], ?string $entityId = null, ?string $reference = ""): array
{

$response = $this->apiRequest('/evaluate/v1/batch', [
Expand Down Expand Up @@ -81,8 +147,12 @@ public function batch(array $names, $context = [], $entityId = NULL, $reference
}, $response['responses']);
}


protected function mergeRequestParams(string $name, $context = [], $entityId = NULL, $reference = "")
/**
* @param array<string,string> $context
*
* @return array<string,mixed>
*/
protected function mergeRequestParams(string $name, $context = [], ?string $entityId = null, ?string $reference = "")
{
return [
'context' => array_merge($this->context, $context),
Expand All @@ -97,6 +167,13 @@ protected function mergeRequestParams(string $name, $context = [], $entityId = N

/**
* Helper function to perform a guzzle request with the correct headers and body
*
* @param array<string,mixed> $body
*
* @return array<string,mixed>
*
* @throws \JsonException if request or response includes invalid json data
* @throws \Psr\Http\Client\ClientExceptionInterface if network or request error occurs
*/
protected function apiRequest(string $path, array $body = [], string $method = 'POST')
{
Expand All @@ -112,15 +189,17 @@ protected function apiRequest(string $path, array $body = [], string $method = '
// execute request
$response = $this->client->request($method, $path, [
'headers' => $headers,
'body' => json_encode($body, JSON_FORCE_OBJECT),
'body' => json_encode($body, JSON_FORCE_OBJECT | JSON_THROW_ON_ERROR),
]);

return json_decode($response->getBody(), true);
return json_decode($response->getBody(), true, 512, JSON_THROW_ON_ERROR);
}


/**
* Create a new client with a different namespace
*
* @return FliptClient
*/
public function withNamespace(string $namespace)
{
Expand All @@ -129,6 +208,10 @@ public function withNamespace(string $namespace)

/**
* Create a new client with a different context
*
* @param array<string,string> $context
*
* @return FliptClient
*/
public function withContext(array $context)
{
Expand All @@ -137,6 +220,8 @@ public function withContext(array $context)

/**
* Create a new client with a different authentication strategy
*
* @return FliptClient
*/
public function withAuthentication(AuthenticationStrategy $authentication)
{
Expand All @@ -146,11 +231,17 @@ public function withAuthentication(AuthenticationStrategy $authentication)

interface AuthenticationStrategy
{
/**
* @param array<string, mixed> $headers
*
* @return array<string, mixed>
*/
public function authenticate(array $headers);
}

/**
* Authenticate with a client token
*
* @see https://www.flipt.io/docs/authentication/methods#static-token
*/
class ClientTokenAuthentication implements AuthenticationStrategy
Expand All @@ -162,6 +253,9 @@ public function __construct(string $token)
$this->token = $token;
}

/**
* @inheritDoc
*/
public function authenticate(array $headers)
{
$headers['Authorization'] = 'Bearer ' . $this->token;
Expand All @@ -171,6 +265,7 @@ public function authenticate(array $headers)

/**
* Authenticate with a JWT token
*
* @see https://www.flipt.io/docs/authentication/methods#json-web-tokens
*/
class JWTAuthentication implements AuthenticationStrategy
Expand All @@ -182,6 +277,9 @@ public function __construct(string $token)
$this->token = $token;
}

/**
* @inheritDoc
*/
public function authenticate(array $headers)
{
$headers['Authorization'] = 'JWT ' . $this->token;
Expand Down
6 changes: 6 additions & 0 deletions flipt-php/src/Flipt/Models/DefaultVariantEvaluationResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ final class DefaultVariantEvaluationResult implements VariantEvaluationResult
public ?string $variantKey;
public ?string $variantAttachment;

/**
* @param array<string> $segmentKeys
*/
public function __construct(
string $flagKey,
bool $match,
Expand Down Expand Up @@ -70,6 +73,9 @@ public function getTimestamp(): string
return $this->timestamp;
}

/**
* @return array<string>
*/
public function getSegmentKeys(): ?array
{
return $this->segmentKeys;
Expand Down
3 changes: 3 additions & 0 deletions flipt-php/src/Flipt/Models/VariantEvaluationResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ public function getReason(): string;
public function getRequestDurationMillis(): float;
public function getRequestId(): string;
public function getTimestamp(): string;
/**
* @return array<string>
*/
public function getSegmentKeys(): ?array;
public function getVariantKey(): ?string;
public function getVariantAttachment(): ?string;
Expand Down
62 changes: 57 additions & 5 deletions flipt-php/tests/FliptClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,20 @@
use PHPUnit\Framework\TestCase;
use Flipt\Client\FliptClient;
use Flipt\Models\ResponseReasons;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Client;
use Psr\Http\Client\ClientExceptionInterface;

final class FliptClientTest extends TestCase
{

protected array $history;
protected FliptClient $apiClient;

public function setUp(): void
{

$fliptUrl = getenv('FLIPT_URL');

if (!$fliptUrl) {
Expand All @@ -24,7 +28,6 @@ public function setUp(): void
$this->fail('FLIPT_AUTH_TOKEN environment variable not set');
}


$this->apiClient = new FliptClient($fliptUrl, authentication: new Flipt\Client\ClientTokenAuthentication($authToken));
}

Expand All @@ -36,18 +39,67 @@ public function testBoolean(): void
$this->assertTrue($result->getEnabled());
$this->assertEquals($result->getReason(), ResponseReasons::MATCH_EVALUATION_REASON);
$this->assertEquals('flag_boolean', $result->getFlagKey());

$value = $this->apiClient->boolval('flag_boolean', false, ['fizz' => 'buzz'], 'entity');
$this->assertTrue($value);
}


public function testVariant(): void
{
$result = $this->apiClient->variant('flag1', ['fizz' => 'buzz'], 'entity');


$this->assertTrue($result->getMatch());
$this->assertEquals($result->getReason(), ResponseReasons::MATCH_EVALUATION_REASON);
$this->assertEquals('flag1', $result->getFlagKey());
$this->assertEquals($result->getSegmentKeys(), ['segment1']);
$this->assertEquals($result->getVariantKey(), 'variant1');

$value = $this->apiClient->variantValue('flag1', 'fallback', ['fizz' => 'buzz'], 'entity');
$this->assertEquals($value, 'variant1');
}

public function testCommunicationError(): void
{
$mock = new MockHandler([
new RequestException('Error Communicating with Server', new Request('POST', '/evaluate/v1/boolean'))
]);
$handlerStack = HandlerStack::create($mock);
$client = new Client(['handler' => $handlerStack]);
$apiClient = new FliptClient($client);

$this->expectException(ClientExceptionInterface::class);
$this->expectExceptionMessage('Error Communicating with Server');
$apiClient->boolean('flag1', ['fizz' => 'buzz'], 'entity');
}

public function testInvalidJsonData(): void
{
$mock = new MockHandler([
new Response(200, [], '{"Hello": "invalid json')
]);
$handlerStack = HandlerStack::create($mock);
$client = new Client(['handler' => $handlerStack]);
$apiClient = new FliptClient($client);

$this->expectException(JsonException::class);
$this->expectExceptionMessage('Control character error, possibly incorrectly encoded');
$apiClient->boolean('flag1', ['fizz' => 'buzz'], 'entity');
}

public function testFallback(): void
{
$mock = new MockHandler([
new Response(200, [], '{"Hello": "invalid json')
]);
$handlerStack = HandlerStack::create($mock);
$client = new Client(['handler' => $handlerStack]);
$apiClient = new FliptClient($client);

$result = $apiClient->booleanValue('flag-b', true, ['fizz' => 'buzz'], 'entity', '');
$this->assertTrue($result);

$result = $apiClient->variantValue('flag-e', 'variant-a', ['fizz' => 'buzz'], 'entity', '');
$this->assertEquals($result, 'variant-a');
}
}

0 comments on commit af35fa3

Please sign in to comment.