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
4 changes: 2 additions & 2 deletions src/api-client/src/ApiClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ class ApiClient
protected bool $enableMiddleware = true;

/**
* @var array<RequestMiddleware|string>
* @var array<callable|object|string>
*/
protected array $requestMiddleware = [];

/**
* @var array<ResponseMiddleware|string>
* @var array<callable|object|string>
*/
protected array $responseMiddleware = [];

Expand Down
3 changes: 3 additions & 0 deletions src/api-client/src/ApiRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

class ApiRequest extends HttpClientRequest
{
use HasContext;

/**
* Determine if the request data has changed.
*/
Expand Down Expand Up @@ -180,6 +182,7 @@ public function withoutHeaders(array $headers): static
public function withBody(string $body): static
{
$this->request = $this->request->withBody(new Stream($body));
$this->dataChanged = false;

return $this;
}
Expand Down
10 changes: 4 additions & 6 deletions src/api-client/src/ApiResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
use Hyperf\Contract\Arrayable;
use Hyperf\Contract\Jsonable;
use Hyperf\Support\Traits\ForwardsCalls;
use Hypervel\HttpClient\Request;
use Hypervel\HttpClient\Response;
use JsonSerializable;
use Stringable;

Expand All @@ -25,8 +23,8 @@ class ApiResource implements Stringable, ArrayAccess, JsonSerializable, Arrayabl
* Create a new resource instance.
*/
public function __construct(
protected Response $response,
protected Request $request
protected ApiResponse $response,
protected ApiRequest $request
) {
}

Expand Down Expand Up @@ -73,12 +71,12 @@ public function __call(string $method, array $parameters): mixed
return $this->forwardCallTo($this->response, $method, $parameters);
}

public function getResponse(): Response
public function getResponse(): ApiResponse
{
return $this->response;
}

public function getRequest(): Request
public function getRequest(): ApiRequest
{
return $this->request;
}
Expand Down
56 changes: 56 additions & 0 deletions src/api-client/src/ApiResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,63 @@
namespace Hypervel\ApiClient;

use Hypervel\HttpClient\Response as HttpClientResponse;
use Psr\Http\Message\StreamInterface;

class ApiResponse extends HttpClientResponse
{
use HasContext;

public function withStatus(int $code, string $reasonPhrase = ''): static
{
$this->response = $this->toPsrResponse()
->withStatus($code, $reasonPhrase);

return $this;
}

public function withProtocolVersion(string $version): static
{
$this->response = $this->toPsrResponse()
->withProtocolVersion($version);

return $this;
}

public function hasHeader(string $name): bool
{
return $this->toPsrResponse()
->hasHeader($name);
}

public function withHeader(string $name, mixed $value): static
{
$this->response = $this->toPsrResponse()
->withHeader($name, $value);

return $this;
}

public function withAddedHeader(string $name, mixed $value): static
{
$this->response = $this->toPsrResponse()
->withAddedHeader($name, $value);

return $this;
}

public function withoutHeader(string $name): static
{
$this->response = $this->toPsrResponse()
->withoutHeader($name);

return $this;
}

public function withBody(StreamInterface $body): static
{
$this->response = $this->toPsrResponse()
->withBody($body);

return $this;
}
}
38 changes: 38 additions & 0 deletions src/api-client/src/HasContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace Hypervel\ApiClient;

trait HasContext
{
protected array $context = [];

/**
* Set the API request/response context.
*/
public function withContext(string|array $key, mixed $value = null): static
{
if (is_array($key)) {
$this->context = array_merge($this->context, $key);

return $this;
}

$this->context[$key] = $value;

return $this;
}

/**
* Get the API request/response context.
*/
public function context(?string $key = null): mixed
{
if ($key !== null) {
return $this->context[$key] ?? null;
}

return $this->context;
}
}
68 changes: 50 additions & 18 deletions src/api-client/src/PendingRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Hypervel\HttpClient\PendingRequest as ClientPendingRequest;
use Hypervel\HttpClient\Request;
use Hypervel\Support\Facades\Http;
use Hypervel\Support\Pipeline;
use Hypervel\Support\Traits\Conditionable;
use InvalidArgumentException;
use JsonSerializable;
Expand All @@ -34,24 +35,30 @@ class PendingRequest
protected array $guzzleOptions = [];

/**
* @var array<RequestMiddleware|string>
* @var array<callable|object|string>
*/
protected array $requestMiddleware = [];

/**
* @var array<ResponseMiddleware|string>
* @var array<callable|object|string>
*/
protected array $responseMiddleware = [];

protected ?ClientPendingRequest $request = null;

protected Pipeline $pipeline;

protected static $cachedMiddleware = [];

public function __construct(
protected ApiClient $client,
?Pipeline $pipeline = null,
) {
$this->resource = $this->client->getResource();
$this->enableMiddleware = $this->client->getEnableMiddleware();
$this->requestMiddleware = $this->client->getRequestMiddleware();
$this->responseMiddleware = $this->client->getResponseMiddleware();
$this->pipeline = $pipeline ?? Pipeline::make();
}

/**
Expand Down Expand Up @@ -236,6 +243,14 @@ public function send(string $method, string $url, array $options = []): ApiResou
return $this->sendRequest('send', $method, $url, $options);
}

/**
* Flush the cached middleware instances.
*/
public function flushCache(): void
{
static::$cachedMiddleware = [];
}

/**
* Provide a dynamic method to pass calls to the pending request.
*/
Expand All @@ -255,17 +270,43 @@ protected function sendRequest(): ApiResource
$request = null;
$response = $this->getClient()
->beforeSending(function (Request $httpRequest) use (&$request) {
$request = $httpRequest;
$request = new ApiRequest($httpRequest->toPsrRequest());
if ($this->enableMiddleware) {
$request = $this->handleMiddlewareRequest($request);
}
return $request->toPsrRequest();
})->{$method}(...$arguments);

if ($response instanceof PromiseInterface) {
throw new InvalidArgumentException('Api client does not support async requests');
}

$response = new ApiResponse($response->toPsrResponse());

if ($this->enableMiddleware) {
$response = $this->handleMiddlewareResponse($response);
}

return $this->resource::make($response, $request);
}

protected function createMiddleware(array $middlewareClasses, array $options): array
protected function handleMiddlewareRequest(ApiRequest $request): ApiRequest
{
return $this->pipeline
->send($request->withContext('options', $this->middlewareOptions))
->through($this->createMiddleware($this->requestMiddleware))
->thenReturn();
}

protected function handleMiddlewareResponse(ApiResponse $response): ApiResponse
{
return $this->pipeline
->send($response)
->through($this->createMiddleware($this->responseMiddleware))
->thenReturn();
}

protected function createMiddleware(array $middlewareClasses): array
{
$middleware = [];
foreach ($middlewareClasses as $value) {
Expand All @@ -274,8 +315,11 @@ protected function createMiddleware(array $middlewareClasses, array $options): a
sprintf('Middleware class `%s` does not exist', $value)
);
}

$middleware[] = new $value($this->client->getConfig(), $options);
if ($cache = static::$cachedMiddleware[$value] ?? null) {
$middleware[] = $cache;
continue;
}
$middleware[] = static::$cachedMiddleware[$value] = new $value($this->client->getConfig());
}

return $middleware;
Expand All @@ -293,18 +337,6 @@ protected function getClient(): ClientPendingRequest
$request->withOptions($this->guzzleOptions);
}

if (! $this->enableMiddleware) {
return $request;
}

foreach ($this->createMiddleware($this->requestMiddleware, $this->middlewareOptions) as $middleware) {
$request->withRequestMiddleware($middleware);
}

foreach ($this->createMiddleware($this->responseMiddleware, $this->middlewareOptions) as $middleware) {
$request->withResponseMiddleware($middleware);
}

return $this->request = $request;
}
}
18 changes: 0 additions & 18 deletions src/api-client/src/RequestMiddleware.php

This file was deleted.

18 changes: 0 additions & 18 deletions src/api-client/src/ResponseMiddleware.php

This file was deleted.

12 changes: 6 additions & 6 deletions tests/ApiClient/ApiResourceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
namespace Hypervel\Tests\ApiClient;

use BadMethodCallException;
use Hypervel\ApiClient\ApiRequest;
use Hypervel\ApiClient\ApiResource;
use Hypervel\HttpClient\Request;
use Hypervel\HttpClient\Response;
use Hypervel\ApiClient\ApiResponse;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

Expand All @@ -18,12 +18,12 @@
class ApiResourceTest extends TestCase
{
/**
* @var MockObject&Response
* @var ApiResponse&MockObject
*/
private $response;

/**
* @var MockObject&Request
* @var ApiRequest&MockObject
*/
private $request;

Expand All @@ -34,8 +34,8 @@ protected function setUp(): void
parent::setUp();

// Create mock objects for the Response and Request
$this->response = $this->createMock(Response::class);
$this->request = $this->createMock(Request::class);
$this->response = $this->createMock(ApiResponse::class);
$this->request = $this->createMock(ApiRequest::class);

// Create the resource with our mocks
$this->resource = new ApiResource($this->response, $this->request);
Expand Down
Loading