Skip to content
Draft
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
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
44 changes: 12 additions & 32 deletions src/Client/ArtemeonHttpClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand All @@ -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);
Expand All @@ -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:
Expand All @@ -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) {
Expand All @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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());
Expand Down
10 changes: 0 additions & 10 deletions src/Client/Decorator/HttpClientDecorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
}
11 changes: 8 additions & 3 deletions src/Client/Decorator/Logger/LoggerDecorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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);
Expand All @@ -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);
}
}
16 changes: 11 additions & 5 deletions src/Client/Decorator/OAuth2/ClientCredentialsDecorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand Down Expand Up @@ -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());
Expand All @@ -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.
*
Expand All @@ -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(
Expand Down
11 changes: 6 additions & 5 deletions src/Client/HttpClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}
4 changes: 2 additions & 2 deletions src/Exception/HttpClientException.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
}
16 changes: 9 additions & 7 deletions src/Exception/Request/Http/ResponseException.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -51,7 +53,7 @@ public static function fromResponse(
/**
* Returns the Response object.
*/
public function getResponse(): ?Response
public function getResponse(): ?ResponseInterface
{
return $this->response;
}
Expand All @@ -61,7 +63,7 @@ public function getResponse(): ?Response
*/
public function hasResponse(): bool
{
return $this->response instanceof Response;
return $this->response instanceof ResponseInterface;
}

/**
Expand Down
13 changes: 7 additions & 6 deletions src/Exception/Request/TransferException.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
Expand Down
Loading
Loading