208 lines
6.1 KiB
PHP
208 lines
6.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Uwil\ArcadiaGenesisPhpClient\Lib;
|
|
|
|
use GuzzleHttp\Exception\ClientException;
|
|
use GuzzleHttp\Exception\ConnectException;
|
|
use GuzzleHttp\Exception\ServerException;
|
|
use Psr\Http\Client\ClientInterface;
|
|
use Psr\Http\Message\RequestInterface;
|
|
use Psr\Http\Message\ResponseInterface;
|
|
use Uwil\ArcadiaGenesisPhpClient\Enum\ArcadiaGenesisApiErrorCode;
|
|
use Uwil\ArcadiaGenesisPhpClient\Exception\ApiCallFailed;
|
|
use Uwil\ArcadiaGenesisPhpClient\Exception\ApiErrorException;
|
|
use Uwil\ArcadiaGenesisPhpClient\Exception\InvalidApiResponse;
|
|
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiQueryBuilder\TableQuery;
|
|
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiResponse\ApiResponse;
|
|
use Uwil\ArcadiaGenesisPhpClient\Lib\Psr7\RequestFactory;
|
|
use Uwil\ArcadiaGenesisPhpClient\Utils\Utils;
|
|
|
|
class ApiCaller
|
|
{
|
|
public function __construct(
|
|
private readonly ClientInterface $client,
|
|
private readonly ApiConfig $apiConfig,
|
|
private readonly RequestFactory $requestFactory
|
|
) {
|
|
}
|
|
|
|
public function callApi(string $queryPath, TableQuery $query): ApiResponse
|
|
{
|
|
$uri = Utils::appendPathTo(
|
|
$this->apiConfig->endpoint,
|
|
$queryPath
|
|
);
|
|
|
|
$request = $this->requestFactory->buildRequest(
|
|
HttpMethod::GET,
|
|
$uri,
|
|
$query->toArray()
|
|
);
|
|
|
|
return $this->callWithRetry($request);
|
|
}
|
|
|
|
protected function callWithRetry(RequestInterface $request): ApiResponse
|
|
{
|
|
$maxAttempts = $this->apiConfig->maxRetries + 1;
|
|
$lastException = new \RuntimeException("Max attempts '{$maxAttempts}' reached on api call");
|
|
|
|
for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
|
|
try {
|
|
return $this->call($request);
|
|
} catch (ApiCallFailed $e) {
|
|
$lastException = $e;
|
|
|
|
if ($attempt >= $maxAttempts) {
|
|
break;
|
|
}
|
|
|
|
$this->delay($attempt);
|
|
} catch (ApiErrorException $e) {
|
|
if ($e->apiError !== ArcadiaGenesisApiErrorCode::ERR_TOO_MANY_REQUESTS) {
|
|
throw $e;
|
|
}
|
|
|
|
$lastException = $e;
|
|
|
|
if ($attempt >= $maxAttempts) {
|
|
break;
|
|
}
|
|
|
|
$this->delay($attempt);
|
|
}
|
|
}
|
|
|
|
throw $lastException;
|
|
}
|
|
|
|
protected function call(RequestInterface $request): ApiResponse
|
|
{
|
|
try {
|
|
$response = $this->client->sendRequest($request);
|
|
|
|
$this->assertResponseHasExpectedHeaders($response);
|
|
|
|
$body = $this->parseResponseBody($response);
|
|
$body = $this->assertApiResponseFormIsAsExpected($body);
|
|
|
|
if ($this->apiResponseIsError($body)) {
|
|
throw ApiErrorException::fromResponseError($body['error']);
|
|
}
|
|
|
|
return ApiResponse::fromBody($body);
|
|
} catch (ConnectException $e) {
|
|
throw new ApiCallFailed(
|
|
'Failed to connect to Api backend: ' . $e->getMessage(),
|
|
$e->getCode(),
|
|
$e
|
|
);
|
|
} catch (ClientException $e) {
|
|
throw new ApiCallFailed(
|
|
'Failed to call Api backend seeing a ClientException',
|
|
$e->getCode(),
|
|
$e
|
|
);
|
|
} catch (ServerException $e) {
|
|
$response = $e->getResponse();
|
|
|
|
$body = $this->parseResponseBody($response);
|
|
|
|
if ($this->apiResponseIsError($body)) {
|
|
throw ApiErrorException::fromResponseError($body['error']);
|
|
}
|
|
|
|
throw new ApiErrorException(
|
|
ArcadiaGenesisApiErrorCode::UNKNOWN,
|
|
$e
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @throws \Uwil\ArcadiaGenesisPhpClient\Exception\InvalidApiResponse
|
|
*/
|
|
protected function parseResponseBody(ResponseInterface $response): mixed
|
|
{
|
|
$bodyContent = $response->getBody()->getContents();
|
|
|
|
if (! json_validate($bodyContent)) {
|
|
$jsonErrorMessage = json_last_error_msg();
|
|
throw new InvalidApiResponse(
|
|
"Invalid Json message '{$jsonErrorMessage}'"
|
|
);
|
|
}
|
|
|
|
return json_decode($bodyContent, true);
|
|
}
|
|
|
|
/**
|
|
* @throws \Uwil\ArcadiaGenesisPhpClient\Exception\InvalidApiResponse
|
|
*/
|
|
protected function assertResponseHasExpectedHeaders(ResponseInterface $response): void
|
|
{
|
|
$headerIsJsonContent = in_array(
|
|
$response->getHeaders()['Content-Type'][0] ?? '',
|
|
[
|
|
'application/json;charset=UTF-8',
|
|
'application/json',
|
|
],
|
|
true
|
|
);
|
|
|
|
if (! $headerIsJsonContent) {
|
|
throw new InvalidApiResponse(
|
|
'Expected Json header in Content-Type'
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return array<string,mixed>
|
|
*
|
|
* @throws \Uwil\ArcadiaGenesisPhpClient\Exception\InvalidApiResponse
|
|
*/
|
|
protected function assertApiResponseFormIsAsExpected(mixed $body): array
|
|
{
|
|
$e = new InvalidApiResponse(
|
|
'Body content shape cannot be recognized'
|
|
);
|
|
|
|
if (! is_array($body) || ! array_key_exists('success', $body)) {
|
|
throw $e;
|
|
}
|
|
|
|
if (! $this->apiResponseIsError($body) && ! $this->apiResponseContainsData($body)) {
|
|
throw $e;
|
|
}
|
|
|
|
return $body;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $responseData
|
|
*/
|
|
protected function apiResponseIsError(array $responseData): bool
|
|
{
|
|
return array_key_exists('error', $responseData)
|
|
&& is_array($responseData['error'])
|
|
&& ! empty($responseData['error']);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $responseData
|
|
*/
|
|
protected function apiResponseContainsData(array $responseData): bool
|
|
{
|
|
return array_key_exists('data', $responseData)
|
|
&& is_array($responseData['data']);
|
|
}
|
|
|
|
protected function delay(int $attempt): void
|
|
{
|
|
$delayMs = $this->apiConfig->retryDelayMs * $attempt;
|
|
usleep($delayMs * 1000);
|
|
}
|
|
}
|