diff --git a/CHANGELOG.md b/CHANGELOG.md index 81ff9ad6..2c93216c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ * add [crm item support](https://github.com/mesilov/bitrix24-php-sdk/issues/330) * add enum `DealStageSemanticId` * add Duplicate search support for `Bitrix24\SDK\Services\CRM\Duplicates\Service\Duplicate` +* add `x-request-id` [header support](https://github.com/mesilov/bitrix24-php-sdk/issues/354) ### Changed diff --git a/src/Core/ApiClient.php b/src/Core/ApiClient.php index ac7387e0..9f2f6125 100644 --- a/src/Core/ApiClient.php +++ b/src/Core/ApiClient.php @@ -5,9 +5,11 @@ namespace Bitrix24\SDK\Core; use Bitrix24\SDK\Core\Contracts\ApiClientInterface; +use Bitrix24\SDK\Core\Credentials\Credentials; use Bitrix24\SDK\Core\Exceptions\InvalidArgumentException; use Bitrix24\SDK\Core\Exceptions\TransportException; use Bitrix24\SDK\Core\Response\DTO\RenewedAccessToken; +use Bitrix24\SDK\Infrastructure\HttpClient\RequestId\RequestIdGeneratorInterface; use Fig\Http\Message\StatusCodeInterface; use Psr\Log\LoggerInterface; use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; @@ -18,7 +20,8 @@ class ApiClient implements ApiClientInterface { protected HttpClientInterface $client; protected LoggerInterface $logger; - protected Credentials\Credentials $credentials; + protected Credentials $credentials; + protected RequestIdGeneratorInterface $requestIdGenerator; /** * @const string */ @@ -32,14 +35,20 @@ class ApiClient implements ApiClientInterface /** * ApiClient constructor. * - * @param Credentials\Credentials $credentials + * @param Credentials $credentials * @param HttpClientInterface $client + * @param RequestIdGeneratorInterface $requestIdGenerator * @param LoggerInterface $logger */ - public function __construct(Credentials\Credentials $credentials, HttpClientInterface $client, LoggerInterface $logger) + public function __construct( + Credentials $credentials, + HttpClientInterface $client, + RequestIdGeneratorInterface $requestIdGenerator, + LoggerInterface $logger) { $this->credentials = $credentials; $this->client = $client; + $this->requestIdGenerator = $requestIdGenerator; $this->logger = $logger; $this->logger->debug( 'ApiClient.init', @@ -64,9 +73,9 @@ protected function getDefaultHeaders(): array } /** - * @return Credentials\Credentials + * @return Credentials */ - public function getCredentials(): Credentials\Credentials + public function getCredentials(): Credentials { return $this->credentials; } @@ -80,7 +89,10 @@ public function getCredentials(): Credentials\Credentials */ public function getNewAccessToken(): RenewedAccessToken { - $this->logger->debug('getNewAccessToken.start'); + $requestId = $this->requestIdGenerator->getRequestId(); + $this->logger->debug('getNewAccessToken.start', [ + 'requestId' => $requestId + ]); if ($this->getCredentials()->getApplicationProfile() === null) { throw new InvalidArgumentException('application profile not set'); } @@ -103,14 +115,21 @@ public function getNewAccessToken(): RenewedAccessToken ); $requestOptions = [ - 'headers' => $this->getDefaultHeaders(), + 'headers' => array_merge( + $this->getDefaultHeaders(), + [ + $this->requestIdGenerator->getHeaderFieldName() => $requestId + ] + ), ]; $response = $this->client->request($method, $url, $requestOptions); $responseData = $response->toArray(false); if ($response->getStatusCode() === StatusCodeInterface::STATUS_OK) { $newAccessToken = RenewedAccessToken::initFromArray($responseData); - $this->logger->debug('getNewAccessToken.finish'); + $this->logger->debug('getNewAccessToken.finish', [ + 'requestId' => $requestId + ]); return $newAccessToken; } if ($response->getStatusCode() === StatusCodeInterface::STATUS_BAD_REQUEST) { @@ -129,12 +148,14 @@ public function getNewAccessToken(): RenewedAccessToken */ public function getResponse(string $apiMethod, array $parameters = []): ResponseInterface { + $requestId = $this->requestIdGenerator->getRequestId(); $this->logger->info( 'getResponse.start', [ 'apiMethod' => $apiMethod, 'domainUrl' => $this->credentials->getDomainUrl(), 'parameters' => $parameters, + 'requestId' => $requestId ] ); @@ -150,9 +171,15 @@ public function getResponse(string $apiMethod, array $parameters = []): Response $parameters['auth'] = $this->getCredentials()->getAccessToken()->getAccessToken(); } + $requestOptions = [ 'json' => $parameters, - 'headers' => $this->getDefaultHeaders(), + 'headers' => array_merge( + $this->getDefaultHeaders(), + [ + $this->requestIdGenerator->getHeaderFieldName() => $requestId + ] + ), // disable redirects, try to catch portal change domain name event 'max_redirects' => 0, ]; @@ -163,6 +190,7 @@ public function getResponse(string $apiMethod, array $parameters = []): Response [ 'apiMethod' => $apiMethod, 'responseInfo' => $response->getInfo(), + 'requestId' => $requestId ] ); diff --git a/src/Core/CoreBuilder.php b/src/Core/CoreBuilder.php index 8e5b99d8..8f0d7960 100644 --- a/src/Core/CoreBuilder.php +++ b/src/Core/CoreBuilder.php @@ -9,6 +9,8 @@ use Bitrix24\SDK\Core\Credentials\Credentials; use Bitrix24\SDK\Core\Credentials\WebhookUrl; use Bitrix24\SDK\Core\Exceptions\InvalidArgumentException; +use Bitrix24\SDK\Infrastructure\HttpClient\RequestId\DefaultRequestIdGenerator; +use Bitrix24\SDK\Infrastructure\HttpClient\RequestId\RequestIdGeneratorInterface; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use Symfony\Component\EventDispatcher\EventDispatcher; @@ -23,12 +25,13 @@ */ class CoreBuilder { - protected ?ApiClientInterface $apiClient; - protected HttpClientInterface $httpClient; - protected EventDispatcherInterface $eventDispatcher; - protected LoggerInterface $logger; - protected ?Credentials $credentials; - protected ApiLevelErrorHandler $apiLevelErrorHandler; + private ?ApiClientInterface $apiClient; + private HttpClientInterface $httpClient; + private EventDispatcherInterface $eventDispatcher; + private LoggerInterface $logger; + private ?Credentials $credentials; + private ApiLevelErrorHandler $apiLevelErrorHandler; + private RequestIdGeneratorInterface $requestIdGenerator; /** * CoreBuilder constructor. @@ -46,6 +49,12 @@ public function __construct() $this->credentials = null; $this->apiClient = null; $this->apiLevelErrorHandler = new ApiLevelErrorHandler($this->logger); + $this->requestIdGenerator = new DefaultRequestIdGenerator(); + } + + public function withRequestIdGenerator(RequestIdGeneratorInterface $requestIdGenerator): void + { + $this->requestIdGenerator = $requestIdGenerator; } /** @@ -101,6 +110,7 @@ public function build(): CoreInterface $this->apiClient = new ApiClient( $this->credentials, $this->httpClient, + $this->requestIdGenerator, $this->logger ); } diff --git a/src/Infrastructure/HttpClient/RequestId/DefaultRequestIdGenerator.php b/src/Infrastructure/HttpClient/RequestId/DefaultRequestIdGenerator.php new file mode 100644 index 00000000..d0332e8b --- /dev/null +++ b/src/Infrastructure/HttpClient/RequestId/DefaultRequestIdGenerator.php @@ -0,0 +1,50 @@ +<?php + +declare(strict_types=1); + +namespace Bitrix24\SDK\Infrastructure\HttpClient\RequestId; + +use Symfony\Component\Uid\Uuid; + +class DefaultRequestIdGenerator implements RequestIdGeneratorInterface +{ + private const DEFAULT_REQUEST_ID_FIELD_NAME = 'X-Request-ID'; + private const KEY_NAME_VARIANTS = [ + 'REQUEST_ID', + 'HTTP_X_REQUEST_ID', + 'UNIQUE_ID' + ]; + + private function generate(): string + { + return Uuid::v7()->toRfc4122(); + } + + private function findExists(): ?string + { + $candidate = null; + foreach(self::KEY_NAME_VARIANTS as $key) + { + if(!empty($_SERVER[$key])) + { + $candidate = $_SERVER[$key]; + break; + } + } + return $candidate; + } + + public function getRequestId(): string + { + $reqId = $this->findExists(); + if ($reqId === null) { + $reqId = $this->generate(); + } + return $reqId; + } + + public function getHeaderFieldName(): string + { + return self::DEFAULT_REQUEST_ID_FIELD_NAME; + } +} \ No newline at end of file diff --git a/src/Infrastructure/HttpClient/RequestId/RequestIdGeneratorInterface.php b/src/Infrastructure/HttpClient/RequestId/RequestIdGeneratorInterface.php new file mode 100644 index 00000000..60dd9f0f --- /dev/null +++ b/src/Infrastructure/HttpClient/RequestId/RequestIdGeneratorInterface.php @@ -0,0 +1,12 @@ +<?php + +declare(strict_types=1); + +namespace Bitrix24\SDK\Infrastructure\HttpClient\RequestId; + +interface RequestIdGeneratorInterface +{ + public function getRequestId(): string; + + public function getHeaderFieldName(): string; +} \ No newline at end of file diff --git a/tests/Unit/Infrastructure/HttpClient/RequestId/DefaultRequestIdGeneratorTest.php b/tests/Unit/Infrastructure/HttpClient/RequestId/DefaultRequestIdGeneratorTest.php new file mode 100644 index 00000000..fab333b8 --- /dev/null +++ b/tests/Unit/Infrastructure/HttpClient/RequestId/DefaultRequestIdGeneratorTest.php @@ -0,0 +1,44 @@ +<?php + +declare(strict_types=1); + +namespace Bitrix24\SDK\Tests\Unit\Infrastructure\HttpClient\RequestId; + +use Bitrix24\SDK\Infrastructure\HttpClient\RequestId\DefaultRequestIdGenerator; +use Generator; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Uid\Uuid; + +class DefaultRequestIdGeneratorTest extends TestCase +{ + /** + * @param $requestIdKey + * @param $requestId + * @return void + * @dataProvider requestIdKeyDataProvider + * @covers \Bitrix24\SDK\Infrastructure\HttpClient\RequestId\DefaultRequestIdGenerator::getRequestId + */ + public function testExistsRequestId($requestIdKey, $requestId): void + { + $_SERVER[$requestIdKey] = $requestId; + $gen = new DefaultRequestIdGenerator(); + $this->assertEquals($requestId, $gen->getRequestId()); + unset($_SERVER[$requestIdKey]); + } + + public function requestIdKeyDataProvider(): Generator + { + yield 'REQUEST_ID' => [ + 'REQUEST_ID', + Uuid::v7()->toRfc4122() + ]; + yield 'HTTP_X_REQUEST_ID' => [ + 'HTTP_X_REQUEST_ID', + Uuid::v7()->toRfc4122() + ]; + yield 'UNIQUE_ID' => [ + 'UNIQUE_ID', + Uuid::v7()->toRfc4122() + ]; + } +} diff --git a/tests/Unit/Stubs/NullCore.php b/tests/Unit/Stubs/NullCore.php index 209c064b..13d85e00 100644 --- a/tests/Unit/Stubs/NullCore.php +++ b/tests/Unit/Stubs/NullCore.php @@ -12,15 +12,11 @@ use Bitrix24\SDK\Core\Credentials\Credentials; use Bitrix24\SDK\Core\Credentials\WebhookUrl; use Bitrix24\SDK\Core\Response\Response; +use Bitrix24\SDK\Infrastructure\HttpClient\RequestId\DefaultRequestIdGenerator; use Psr\Log\NullLogger; use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Response\MockResponse; -/** - * Class NullCore - * - * @package Bitrix24\SDK\Tests\Unit\Stubs - */ class NullCore implements CoreInterface { /** @@ -37,6 +33,10 @@ public function call(string $apiMethod, array $parameters = []): Response public function getApiClient(): ApiClientInterface { - return new ApiClient(Credentials::createFromWebhook(new WebhookUrl('')), new MockHttpClient(), new NullLogger()); + return new ApiClient( + Credentials::createFromWebhook(new WebhookUrl('')), + new MockHttpClient(), + new DefaultRequestIdGenerator(), + new NullLogger()); } } \ No newline at end of file