diff --git a/composer.json b/composer.json index 8162e47..fab3400 100644 --- a/composer.json +++ b/composer.json @@ -44,7 +44,8 @@ "guzzlehttp/guzzle": "^7.9.2", "ext-json": "*", "ext-mbstring": "*", - "psr/log": "^1.1|^2.0|^3.0" + "psr/log": "^1.1|^2.0|^3.0", + "psr/http-client": "^1.0" }, "require-dev": { "laravel/pint": "^1.20.0", diff --git a/src/Client/ArtemeonHttpClient.php b/src/Client/ArtemeonHttpClient.php index 031e1af..08c5dfc 100644 --- a/src/Client/ArtemeonHttpClient.php +++ b/src/Client/ArtemeonHttpClient.php @@ -24,11 +24,7 @@ use Artemeon\HttpClient\Exception\Request\TransferException; use Artemeon\HttpClient\Exception\RuntimeException; use Artemeon\HttpClient\Http\Header\Fields\UserAgent; -use Artemeon\HttpClient\Http\Header\Header; use Artemeon\HttpClient\Http\Header\HeaderField; -use Artemeon\HttpClient\Http\Header\Headers; -use Artemeon\HttpClient\Http\Request; -use Artemeon\HttpClient\Http\Response; use GuzzleHttp\Client as GuzzleClient; use GuzzleHttp\Exception\BadResponseException as GuzzleBadResponseException; use GuzzleHttp\Exception\ClientException as GuzzleClientException; @@ -39,7 +35,8 @@ use GuzzleHttp\Exception\TooManyRedirectsException as GuzzleTooManyRedirectsException; use GuzzleHttp\Exception\TransferException as GuzzleTransferException; use Override; -use Psr\Http\Message\ResponseInterface as GuzzleResponse; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; /** * HttpClient implementation with guzzle. @@ -53,7 +50,7 @@ public function __construct( } #[Override] - final public function send(Request $request, ?ClientOptions $clientOptions = null): Response + final public function send(RequestInterface $request, ?ClientOptions $clientOptions = null): ResponseInterface { if ($clientOptions instanceof ClientOptions) { $guzzleOptions = $this->clientOptionsConverter->toGuzzleOptionsArray($clientOptions); @@ -70,6 +67,11 @@ final public function send(Request $request, ?ClientOptions $clientOptions = nul return $this->doSend($request, $guzzleOptions); } + public function sendRequest(RequestInterface $request): ResponseInterface + { + return $this->doSend($request, []); + } + /** * Send request and transform Guzzle exception to Artemeon exceptions * Map Guzzle exceptions -> HttpClient exceptions: @@ -89,10 +91,10 @@ final public function send(Request $request, ?ClientOptions $clientOptions = nul * * @throws HttpClientException */ - private function doSend(Request $request, array $guzzleOptions): Response + private function doSend(RequestInterface $request, array $guzzleOptions): ResponseInterface { try { - $response = $this->guzzleClient->send($request, $guzzleOptions); + return $this->guzzleClient->send($request, $guzzleOptions); } catch (GuzzleClientException $previous) { throw ClientResponseException::fromResponse($this->getResponseFromGuzzleException($previous), $request, $previous->getMessage(), $previous); } catch (GuzzleServerException $previous) { @@ -112,39 +114,17 @@ private function doSend(Request $request, array $guzzleOptions): Response } catch (GuzzleException $previous) { throw RuntimeException::fromGuzzleException($previous); } - - return $this->convertGuzzleResponse($response); } /** * Checks the Guzzle exception for a response object and converts it to a Artemeon response object. */ - private function getResponseFromGuzzleException(GuzzleRequestException $guzzleRequestException): ?Response + private function getResponseFromGuzzleException(GuzzleRequestException $guzzleRequestException): ?ResponseInterface { if (!$guzzleRequestException->hasResponse()) { return null; } - return $this->convertGuzzleResponse($guzzleRequestException->getResponse()); - } - - /** - * Converts a GuzzleResponse object to our Response object. - */ - private function convertGuzzleResponse(?GuzzleResponse $guzzleResponse): Response - { - $headers = Headers::create(); - - foreach (array_keys($guzzleResponse->getHeaders()) as $headerField) { - $headers->add(Header::fromArray($headerField, $guzzleResponse->getHeader($headerField))); - } - - return new Response( - $guzzleResponse->getStatusCode(), - $guzzleResponse->getProtocolVersion(), - $guzzleResponse->getBody(), - $headers, - $guzzleResponse->getReasonPhrase(), - ); + return $guzzleRequestException->getResponse(); } } diff --git a/src/Client/Decorator/ClientOptionsModifier/HttpClientWithModifiedOptions.php b/src/Client/Decorator/ClientOptionsModifier/HttpClientWithModifiedOptions.php index 12dadc1..ad3236c 100644 --- a/src/Client/Decorator/ClientOptionsModifier/HttpClientWithModifiedOptions.php +++ b/src/Client/Decorator/ClientOptionsModifier/HttpClientWithModifiedOptions.php @@ -8,9 +8,9 @@ use Artemeon\HttpClient\Client\HttpClient; use Artemeon\HttpClient\Client\Options\ClientOptions; use Artemeon\HttpClient\Client\Options\ClientOptionsModifier; -use Artemeon\HttpClient\Http\Request; -use Artemeon\HttpClient\Http\Response; use Override; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; final class HttpClientWithModifiedOptions extends HttpClientDecorator { @@ -20,11 +20,16 @@ public function __construct(HttpClient $httpClient, private readonly ClientOptio } #[Override] - public function send(Request $request, ?ClientOptions $clientOptions = null): Response + public function send(RequestInterface $request, ?ClientOptions $clientOptions = null): ResponseInterface { return $this->httpClient->send($request, $this->modified($clientOptions)); } + public function sendRequest(RequestInterface $request): ResponseInterface + { + return $this->send($request); + } + private function modified(?ClientOptions $clientOptions): ClientOptions { return $this->clientOptionsModifier->modify($clientOptions ?? ClientOptions::fromDefaults()); diff --git a/src/Client/Decorator/HttpClientDecorator.php b/src/Client/Decorator/HttpClientDecorator.php index 8d41ccc..63e989e 100644 --- a/src/Client/Decorator/HttpClientDecorator.php +++ b/src/Client/Decorator/HttpClientDecorator.php @@ -14,10 +14,6 @@ namespace Artemeon\HttpClient\Client\Decorator; use Artemeon\HttpClient\Client\HttpClient; -use Artemeon\HttpClient\Client\Options\ClientOptions; -use Artemeon\HttpClient\Http\Request; -use Artemeon\HttpClient\Http\Response; -use Override; /** * Abstract base class for the decorator pattern. @@ -30,10 +26,4 @@ abstract class HttpClientDecorator implements HttpClient public function __construct(protected HttpClient $httpClient) { } - - /** - * @inheritDoc - */ - #[Override] - abstract public function send(Request $request, ?ClientOptions $clientOptions = null): Response; } diff --git a/src/Client/Decorator/Logger/LoggerDecorator.php b/src/Client/Decorator/Logger/LoggerDecorator.php index d187b2f..4619dcd 100644 --- a/src/Client/Decorator/Logger/LoggerDecorator.php +++ b/src/Client/Decorator/Logger/LoggerDecorator.php @@ -19,9 +19,9 @@ use Artemeon\HttpClient\Exception\HttpClientException; use Artemeon\HttpClient\Exception\Request\Http\ClientResponseException; use Artemeon\HttpClient\Exception\Request\Http\ServerResponseException; -use Artemeon\HttpClient\Http\Request; -use Artemeon\HttpClient\Http\Response; use Override; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; use Psr\Log\LoggerInterface; /** @@ -38,7 +38,7 @@ public function __construct(HttpClient $httpClient, private readonly LoggerInter * @inheritDoc */ #[Override] - public function send(Request $request, ?ClientOptions $clientOptions = null): Response + public function send(RequestInterface $request, ?ClientOptions $clientOptions = null): ResponseInterface { try { return $this->httpClient->send($request, $clientOptions); @@ -52,4 +52,9 @@ public function send(Request $request, ?ClientOptions $clientOptions = null): Re throw $exception; } } + + public function sendRequest(RequestInterface $request): ResponseInterface + { + return $this->send($request); + } } diff --git a/src/Client/Decorator/OAuth2/ClientCredentialsDecorator.php b/src/Client/Decorator/OAuth2/ClientCredentialsDecorator.php index 3ce4700..1d3c381 100644 --- a/src/Client/Decorator/OAuth2/ClientCredentialsDecorator.php +++ b/src/Client/Decorator/OAuth2/ClientCredentialsDecorator.php @@ -30,10 +30,11 @@ use Artemeon\HttpClient\Http\Header\Headers; use Artemeon\HttpClient\Http\MediaType; use Artemeon\HttpClient\Http\Request; -use Artemeon\HttpClient\Http\Response; use Artemeon\HttpClient\Http\Uri; use Exception; use Override; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; /** * Http client decorator to add transparent access tokens to requests. Fetches the 'Access Token' from @@ -45,12 +46,12 @@ class ClientCredentialsDecorator extends HttpClientDecorator * ClientCredentialsDecorator constructor. * * @param HttpClient $httpClient The http client to decorate - * @param Request $accessTokenRequest The http request object + * @param RequestInterface $accessTokenRequest The http request object * @param AccessTokenCache $accessTokenCache Cache strategy to store the access token */ public function __construct( HttpClient $httpClient, - private readonly Request $accessTokenRequest, + private readonly RequestInterface $accessTokenRequest, private readonly AccessTokenCache $accessTokenCache, ) { parent::__construct($httpClient); @@ -99,7 +100,7 @@ public static function fromClientCredentials( * @inheritDoc */ #[Override] - public function send(Request $request, ?ClientOptions $clientOptions = null): Response + public function send(RequestInterface $request, ?ClientOptions $clientOptions = null): ResponseInterface { if ($this->accessTokenCache->isExpired()) { $this->accessTokenCache->add($this->requestAccessToken()); @@ -112,6 +113,11 @@ public function send(Request $request, ?ClientOptions $clientOptions = null): Re return $this->httpClient->send($requestWithAuthorisation, $clientOptions); } + public function sendRequest(RequestInterface $request): ResponseInterface + { + return $this->send($request); + } + /** * Fetches the access token. * @@ -135,7 +141,7 @@ private function requestAccessToken(?ClientOptions $clientOptions = null): Acces * * @throws RuntimeException */ - private function assertIsValidJsonResponse(Response $response): void + private function assertIsValidJsonResponse(ResponseInterface $response): void { if ($response->getStatusCode() !== 200) { throw new RuntimeException( diff --git a/src/Client/HttpClient.php b/src/Client/HttpClient.php index 9016b4a..c43dc02 100644 --- a/src/Client/HttpClient.php +++ b/src/Client/HttpClient.php @@ -23,18 +23,19 @@ use Artemeon\HttpClient\Exception\Request\Network\ConnectException; use Artemeon\HttpClient\Exception\Request\TransferException; use Artemeon\HttpClient\Exception\RuntimeException; -use Artemeon\HttpClient\Http\Request; -use Artemeon\HttpClient\Http\Response; +use Psr\Http\Client\ClientInterface; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; /** * Interface to plug in third party http-client libraries. */ -interface HttpClient +interface HttpClient extends ClientInterface { /** * Sends the request. * - * @param Request $request Request object to send + * @param RequestInterface $request Request object to send * @param ClientOptions|null $clientOptions Optional client configuration object * * @throws HttpClientException Interface to catch all possible exceptions @@ -48,5 +49,5 @@ interface HttpClient * @throws RedirectResponseException 2.1.2.3 All response exceptions with 300 status codes * @throws \InvalidArgumentException */ - public function send(Request $request, ?ClientOptions $clientOptions = null): Response; + public function send(RequestInterface $request, ?ClientOptions $clientOptions = null): ResponseInterface; } diff --git a/src/Exception/HttpClientException.php b/src/Exception/HttpClientException.php index 51549b2..913ea2b 100644 --- a/src/Exception/HttpClientException.php +++ b/src/Exception/HttpClientException.php @@ -4,11 +4,11 @@ namespace Artemeon\HttpClient\Exception; -use Throwable; +use Psr\Http\Client\ClientExceptionInterface; /** * Interface to catch all possible HttpClient exceptions. */ -interface HttpClientException extends Throwable +interface HttpClientException extends ClientExceptionInterface { } diff --git a/src/Exception/Request/Http/ResponseException.php b/src/Exception/Request/Http/ResponseException.php index 70ef524..60a06d7 100644 --- a/src/Exception/Request/Http/ResponseException.php +++ b/src/Exception/Request/Http/ResponseException.php @@ -17,26 +17,28 @@ use Artemeon\HttpClient\Http\Request; use Artemeon\HttpClient\Http\Response; use Exception; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; /** * Exception class to catch all possible http status code ranges. */ class ResponseException extends TransferException { - protected ?Response $response = null; + protected ?ResponseInterface $response = null; protected int $statusCode; /** * Named constructor to create an instance based on the response of the failed request. * - * @param ?Response $response The failed response if exists - * @param Request $request The failed request + * @param ?ResponseInterface $response The failed response if exists + * @param RequestInterface $request The failed request * @param string $message The error message * @param Exception|null $previous The previous exception */ public static function fromResponse( - ?Response $response, - Request $request, + ?ResponseInterface $response, + RequestInterface $request, string $message, ?Exception $previous = null, ): static { @@ -51,7 +53,7 @@ public static function fromResponse( /** * Returns the Response object. */ - public function getResponse(): ?Response + public function getResponse(): ?ResponseInterface { return $this->response; } @@ -61,7 +63,7 @@ public function getResponse(): ?Response */ public function hasResponse(): bool { - return $this->response instanceof Response; + return $this->response instanceof ResponseInterface; } /** diff --git a/src/Exception/Request/TransferException.php b/src/Exception/Request/TransferException.php index 4a21f15..ee50fbf 100644 --- a/src/Exception/Request/TransferException.php +++ b/src/Exception/Request/TransferException.php @@ -14,25 +14,26 @@ namespace Artemeon\HttpClient\Exception\Request; use Artemeon\HttpClient\Exception\RuntimeException; -use Artemeon\HttpClient\Http\Request; use Exception; +use Psr\Http\Client\NetworkExceptionInterface; +use Psr\Http\Message\RequestInterface; use Throwable; /** * Class for all runtime exceptions during the request/response transfers. */ -class TransferException extends RuntimeException +class TransferException extends RuntimeException implements NetworkExceptionInterface { - protected Request $request; + protected RequestInterface $request; /** * Named constructor to create an instance based on the given request object. * - * @param Request $request The failed request object + * @param RequestInterface $request The failed request object * @param string $message The error message * @param Exception|null $previous The precious third party exception */ - public static function fromRequest(Request $request, string $message, ?Exception $previous = null): static + public static function fromRequest(RequestInterface $request, string $message, ?Exception $previous = null): static { $instance = new static($message, 0, $previous); $instance->request = $request; @@ -48,7 +49,7 @@ final public function __construct(string $message = '', int $code = 0, ?Throwabl /** * Returns the request object of the failed request. */ - public function getRequest(): Request + public function getRequest(): RequestInterface { return $this->request; } diff --git a/tests/Unit/Client/ArtemeonHttpClientTest.php b/tests/Unit/Client/ArtemeonHttpClientTest.php index fd29ad4..f1d2f6f 100644 --- a/tests/Unit/Client/ArtemeonHttpClientTest.php +++ b/tests/Unit/Client/ArtemeonHttpClientTest.php @@ -24,7 +24,6 @@ use Artemeon\HttpClient\Exception\Request\TransferException; use Artemeon\HttpClient\Exception\RuntimeException; use Artemeon\HttpClient\Http\Request; -use Artemeon\HttpClient\Http\Response; use Artemeon\HttpClient\Http\Uri; use GuzzleHttp\Client as GuzzleClient; use GuzzleHttp\Exception\BadResponseException as GuzzleBadResponseException; @@ -43,6 +42,7 @@ use Override; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; +use Psr\Http\Message\ResponseInterface; /** * @internal @@ -84,7 +84,7 @@ public function testSendWithoutOptionsUsesEmptyOptionsArray(): void $request = Request::forGet(Uri::fromString('http://apache/')); $response = $this->httpClient->send($request); - self::assertInstanceOf(Response::class, $response); + self::assertInstanceOf(ResponseInterface::class, $response); } public function testSendWithOptionsConvertOptions(): void @@ -98,7 +98,7 @@ public function testSendWithOptionsConvertOptions(): void $request = Request::forGet(Uri::fromString('http://apache/')); $response = $this->httpClient->send($request, $this->clientOptions); - self::assertInstanceOf(Response::class, $response); + self::assertInstanceOf(ResponseInterface::class, $response); } public function testSendConvertsGuzzleResponseToValidResponse(): void