Skip to content
Merged
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
26 changes: 22 additions & 4 deletions src/API/Endpoints/BaseEndpoint.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,13 @@ protected function buildQueryString(array $filters): string
*/
protected function rest_create(array $body, array $filters): BaseResource
{
$headers = $this->extractIdempotencyHeaders($filters);

$result = $this->client->performHttpCall(
self::REST_CREATE,
$this->getResourcePath() . $this->buildQueryString($filters),
$this->parseRequestBody($body)
$this->parseRequestBody($body),
$headers
);

return ResourceFactory::createResourceFromApiResult($result, $this->getResourceObject());
Expand All @@ -92,17 +95,20 @@ protected function rest_create(array $body, array $filters): BaseResource
* @return null|BaseResource
* @throws \Vatly\API\Exceptions\ApiException
*/
protected function rest_update(string $id, array $body = []): ?BaseResource
protected function rest_update(string $id, array $body = [], array $filters = []): ?BaseResource
{
if (empty($id)) {
throw new ApiException("Invalid resource id.");
}

$headers = $this->extractIdempotencyHeaders($filters);

$id = urlencode($id);
$result = $this->client->performHttpCall(
self::REST_UPDATE,
"{$this->getResourcePath()}/{$id}",
$this->parseRequestBody($body)
"{$this->getResourcePath()}/{$id}" . $this->buildQueryString($filters),
$this->parseRequestBody($body),
$headers
);

if ($result == null) {
Expand Down Expand Up @@ -200,6 +206,18 @@ public function getResourcePath(): string
return $this->resourcePath;
}

private function extractIdempotencyHeaders(array &$filters): array
{
$headers = [];

if (isset($filters['idempotencyKey'])) {
$headers['Idempotency-Key'] = $filters['idempotencyKey'];
unset($filters['idempotencyKey']);
}

return $headers;
}

/**
* @param array $body
* @return null|string
Expand Down
4 changes: 2 additions & 2 deletions src/API/Endpoints/SubscriptionEndpoint.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,11 @@ protected function getResourcePageObject(int $count, PaginationLinks $links): Ba
/**
* @throws ApiException
*/
public function update(string $subscriptionId, array $data = []): ?BaseResource
public function update(string $subscriptionId, array $data = [], array $filters = []): ?BaseResource
{
$this->validateSubscriptionId($subscriptionId);

return $this->rest_update($subscriptionId, $data);
return $this->rest_update($subscriptionId, $data, $filters);
}

/**
Expand Down
13 changes: 13 additions & 0 deletions src/API/HttpClient/Idempotency/DefaultIdempotencyKeyGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Vatly\API\HttpClient\Idempotency;

class DefaultIdempotencyKeyGenerator implements IdempotencyKeyGeneratorContract
{
public function generate(): string
{
return substr(base64_encode(random_bytes(24)), 0, 16);
}
}
22 changes: 22 additions & 0 deletions src/API/HttpClient/Idempotency/FakeIdempotencyKeyGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace Vatly\API\HttpClient\Idempotency;

class FakeIdempotencyKeyGenerator implements IdempotencyKeyGeneratorContract
{
protected string $fakeKey = 'fake-idempotency-key';

public function setFakeKey(string $key): self
{
$this->fakeKey = $key;

return $this;
}

public function generate(): string
{
return $this->fakeKey;
}
}
10 changes: 10 additions & 0 deletions src/API/HttpClient/Idempotency/IdempotencyKeyGeneratorContract.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Vatly\API\HttpClient\Idempotency;

interface IdempotencyKeyGeneratorContract
{
public function generate(): string;
}
52 changes: 52 additions & 0 deletions src/API/Traits/HandlesIdempotency.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

declare(strict_types=1);

namespace Vatly\API\Traits;

use Vatly\API\HttpClient\Idempotency\IdempotencyKeyGeneratorContract;

trait HandlesIdempotency
{
protected ?IdempotencyKeyGeneratorContract $idempotencyKeyGenerator = null;

protected ?string $idempotencyKey = null;

public function setIdempotencyKey(string $idempotencyKey): self
{
$this->idempotencyKey = $idempotencyKey;

return $this;
}

public function getIdempotencyKey(): ?string
{
return $this->idempotencyKey;
}

public function resetIdempotencyKey(): self
{
$this->idempotencyKey = null;

return $this;
}

public function setIdempotencyKeyGenerator(IdempotencyKeyGeneratorContract $generator): self
{
$this->idempotencyKeyGenerator = $generator;

return $this;
}

public function getIdempotencyKeyGenerator(): ?IdempotencyKeyGeneratorContract
{
return $this->idempotencyKeyGenerator;
}

public function clearIdempotencyKeyGenerator(): self
{
$this->idempotencyKeyGenerator = null;

return $this;
}
}
31 changes: 27 additions & 4 deletions src/API/VatlyApiClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@
use Vatly\API\HttpClient\DefaultHttpClientFactory;
use Vatly\API\HttpClient\HttpClientFactoryInterface;
use Vatly\API\HttpClient\HttpClientInterface;
use Vatly\API\HttpClient\Idempotency\DefaultIdempotencyKeyGenerator;
use Vatly\API\Traits\HandlesIdempotency;

class VatlyApiClient
{
use HandlesIdempotency;
/**
* The version of this client.
*/
Expand Down Expand Up @@ -103,6 +106,8 @@ public function __construct(
$this->httpClient = (new DefaultHttpClientFactory)->make();
}

$this->idempotencyKeyGenerator = new DefaultIdempotencyKeyGenerator();

$this->initializeVersionString();
$this->initializeEndpoints();
}
Expand Down Expand Up @@ -190,11 +195,11 @@ public function setApiKey(string $apiKey): VatlyApiClient
* @return \stdClass
* @throws ApiException
*/
public function performHttpCall(string $httpMethod, string $apiMethod, ?string $httpBody = null): ?object
public function performHttpCall(string $httpMethod, string $apiMethod, ?string $httpBody = null, array $requestHeaders = []): ?object
{
$url = $this->apiEndpoint . "/" . self::API_VERSION . "/" . $apiMethod;

return $this->performHttpCallToFullUrl($httpMethod, $url, $httpBody);
return $this->performHttpCallToFullUrl($httpMethod, $url, $httpBody, $requestHeaders);
}

/**
Expand All @@ -206,7 +211,7 @@ public function performHttpCall(string $httpMethod, string $apiMethod, ?string $
* @return object|null
* @throws \Vatly\API\Exceptions\ApiException
*/
public function performHttpCallToFullUrl(string $httpMethod, string $url, ?string $httpBody = null): ?object
public function performHttpCallToFullUrl(string $httpMethod, string $url, ?string $httpBody = null, array $requestHeaders = []): ?object
{
if (empty($this->apiKey)) {
throw new ApiException("You have not set an API key. Please use setApiKey() to set the API key.");
Expand All @@ -226,7 +231,25 @@ public function performHttpCallToFullUrl(string $httpMethod, string $url, ?strin
$headers['X-Vatly-Client-Info'] = php_uname();
}

return $this->httpClient->send($httpMethod, $url, $headers, $httpBody);
if (in_array($httpMethod, [self::HTTP_POST, self::HTTP_PATCH])) {
$idempotencyKey = $this->idempotencyKey;

if ($idempotencyKey === null && $this->idempotencyKeyGenerator !== null) {
$idempotencyKey = $this->idempotencyKeyGenerator->generate();
}

if ($idempotencyKey !== null) {
$headers['Idempotency-Key'] = $idempotencyKey;
}
}

$headers = array_merge($headers, $requestHeaders);

$response = $this->httpClient->send($httpMethod, $url, $headers, $httpBody);

$this->resetIdempotencyKey();

return $response;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace Vatly\Tests\API\HttpClient\Idempotency;

use PHPUnit\Framework\TestCase;
use Vatly\API\HttpClient\Idempotency\DefaultIdempotencyKeyGenerator;

class DefaultIdempotencyKeyGeneratorTest extends TestCase
{
/** @test */
public function generates_a_16_character_string()
{
$generator = new DefaultIdempotencyKeyGenerator();

$key = $generator->generate();

$this->assertIsString($key);
$this->assertEquals(16, strlen($key));
}

/** @test */
public function generates_unique_keys()
{
$generator = new DefaultIdempotencyKeyGenerator();

$key1 = $generator->generate();
$key2 = $generator->generate();

$this->assertNotEquals($key1, $key2);
}
}
11 changes: 11 additions & 0 deletions tests/API/HttpClient/SpyHttpClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ private function recordSend(
'User-Agent',
'Content-Type',
'X-Vatly-Client-Info',
'Idempotency-Key',
]);
}, ARRAY_FILTER_USE_KEY);

Expand All @@ -173,10 +174,20 @@ private function recordSend(
'httpMethod' => $httpMethod,
'url' => $url,
'headers' => $sanitizedHeaders,
'allHeaders' => $headers,
'httpBody' => $httpBody,
];
}

public function lastSentHeaders(): array
{
if (empty($this->recordedSends)) {
return [];
}

return end($this->recordedSends)['allHeaders'];
}

public function supportsDebugging(): bool
{
return false;
Expand Down
Loading