Skip to content

Commit

Permalink
Introduce the tap middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
cerbero90 committed Feb 16, 2024
1 parent 0ca3adc commit 00ddf7e
Show file tree
Hide file tree
Showing 5 changed files with 287 additions and 86 deletions.
43 changes: 28 additions & 15 deletions src/LazyJsonPages.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
use Closure;
use GuzzleHttp\RequestOptions;
use Illuminate\Support\LazyCollection;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\RequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Throwable;

/**
* The Lazy JSON Pages entry-point
Expand All @@ -23,7 +24,7 @@ final class LazyJsonPages
/**
* The HTTP client factory.
*/
private readonly ClientFactory $factory;
private readonly ClientFactory $client;

/**
* The raw configuration of the API pagination.
Expand Down Expand Up @@ -53,7 +54,7 @@ public static function from(mixed $source): self
*/
public function __construct(private readonly mixed $source)
{
$this->factory = new ClientFactory();
$this->client = new ClientFactory();
}

/**
Expand Down Expand Up @@ -171,7 +172,7 @@ public function async(int $requests): self
*/
public function connectionTimeout(float|int $seconds): self
{
$this->factory->option(RequestOptions::CONNECT_TIMEOUT, max(0, $seconds));
$this->client->config(RequestOptions::CONNECT_TIMEOUT, max(0, $seconds));

return $this;
}
Expand All @@ -181,8 +182,8 @@ public function connectionTimeout(float|int $seconds): self
*/
public function requestTimeout(float|int $seconds): self
{
$this->factory->option(RequestOptions::TIMEOUT, max(0, $seconds));
$this->factory->option(RequestOptions::READ_TIMEOUT, max(0, $seconds));
$this->client->config(RequestOptions::TIMEOUT, max(0, $seconds));
$this->client->config(RequestOptions::READ_TIMEOUT, max(0, $seconds));

return $this;
}
Expand Down Expand Up @@ -212,43 +213,55 @@ public function backoff(Closure $callback): self
*/
public function middleware(string $name, callable $middleware): self
{
$this->factory->middleware($name, $middleware);
$this->client->middleware($name, $middleware);

return $this;
}

/**
* Handle the sending request.
*
* @param (Closure(RequestInterface): void) $callback
* @param Closure(Request $request, array<string, mixed> $config): void $callback
*/
public function onRequest(Closure $callback): self
{
$this->factory->onRequest($callback);
$this->client->onRequest($callback);

return $this;
}

/**
* Handle the received response.
*
* @param (Closure(ResponseInterface, RequestInterface): void) $callback
* @param Closure(Response $response, Request $request, array<string, mixed> $config): void $callback
*/
public function onResponse(Closure $callback): self
{
$this->factory->onResponse($callback);
$this->client->onResponse($callback);

return $this;
}

/**
* Handle a transaction error.
*
* @param (Closure(\Throwable, RequestInterface, ?ResponseInterface): void) $callback
* @param Closure(Throwable $e, Request $request, ?Response $response, array<string, mixed> $config): void $callback
*/
public function onError(Closure $callback): self
{
$this->factory->onError($callback);
$this->client->onError($callback);

return $this;
}

/**
* Throttle the requests to respect rate limiting.
*/
public function throttle(int $requests, int $perSeconds = 0, int $perMinutes = 0, int $perHours = 0): self
{
$seconds = max(0, $perSeconds + $perMinutes * 60 + $perHours * 3600);

$this->client->throttle($requests, $seconds);

return $this;
}
Expand All @@ -264,7 +277,7 @@ public function collect(string $dot = '*'): LazyCollection
$this->config['itemsPointer'] = DotsConverter::toPointer($dot);

return new LazyCollection(function() {
$client = $this->factory->make();
$client = $this->client->make();
$config = new Config(...$this->config);
$source = new AnySource($this->source, $client);

Expand Down
99 changes: 99 additions & 0 deletions src/Middleware/Tap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

declare(strict_types=1);

namespace Cerbero\LazyJsonPages\Middleware;

use Cerbero\LazyJsonPages\Services\TapCallbacks;
use Closure;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Promise\Create;
use GuzzleHttp\Promise\PromiseInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

/**
* The middleware to handle an HTTP request before and after it is sent.
*/
final class Tap
{
/**
* The HTTP request.
*/
private RequestInterface $request;

/**
* The Guzzle client configuration.
*
* @var array<string, mixed>
*/
private array $config;

/**
* Instantiate the class statically to tap once.
*/
public static function once(?Closure $onRequest = null, ?Closure $onResponse = null, ?Closure $onError = null): self
{
$callbacks = new TapCallbacks();
$onRequest && $callbacks->onRequest($onRequest);
$onResponse && $callbacks->onResponse($onResponse);
$onError && $callbacks->onError($onError);

return new self($callbacks);
}

/**
* Instantiate the class.
*/
public function __construct(private readonly TapCallbacks $callbacks)
{
}

/**
* Handle an HTTP request before and after it is sent.
*
* @param callable(RequestInterface, array): PromiseInterface $handler
*/
public function __invoke(callable $handler): Closure
{
return function (RequestInterface $request, array $config) use ($handler) {
$this->request = $request;
$this->config = $config;

foreach ($this->callbacks->onRequestCallbacks() as $callback) {
$callback($request, $config);
}

return $handler($request, $config)
->then($this->handleResponse(...))
->otherwise($this->handleError(...));
};
}

/**
* Handle the given response.
*/
private function handleResponse(ResponseInterface $response): ResponseInterface
{
foreach ($this->callbacks->onResponseCallbacks() as $callback) {
$callback($response, $this->request, $this->config);
}

return $response;
}

/**
* Handle the given transaction error.
*/
private function handleError(mixed $reason): PromiseInterface
{
$exception = Create::exceptionFor($reason);
$response = $reason instanceof RequestException ? $reason->getResponse() : null;

foreach ($this->callbacks->onErrorCallbacks() as $callback) {
$callback($exception, $this->request, $response, $this->config);
}

return Create::rejectionFor($reason);
}
}
29 changes: 17 additions & 12 deletions src/Providers/LazyJsonPagesServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
namespace Cerbero\LazyJsonPages\Providers;

use Cerbero\LazyJsonPages\LazyJsonPages;
use GuzzleHttp\Middleware;
use GuzzleHttp\Promise\PromiseInterface;
use Cerbero\LazyJsonPages\Middleware\Tap;
use Illuminate\Http\Client\Events\ConnectionFailed;
use Illuminate\Http\Client\Events\RequestSending;
use Illuminate\Http\Client\Events\ResponseReceived;
Expand All @@ -15,6 +14,7 @@
use Illuminate\Support\ServiceProvider;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Throwable;

/**
* The service provider to integrate with Laravel.
Expand All @@ -26,27 +26,32 @@ final class LazyJsonPagesServiceProvider extends ServiceProvider
*/
public function boot(): void
{
LazyJsonPages::globalMiddleware('laravel_events', Middleware::tap($this->sending(...), $this->sent(...)));
$fireEvents = Tap::once($this->onRequest(...), $this->onResponse(...), $this->onError(...));

LazyJsonPages::globalMiddleware('laravel_events', $fireEvents);
}

/**
* Handle HTTP requests before they are sent.
*/
private function sending(RequestInterface $request): void
private function onRequest(RequestInterface $request): void
{
event(new RequestSending(new Request($request)));
$this->app['events']->dispatch(new RequestSending(new Request($request)));
}

/**
* Handle HTTP requests after they are sent.
* Handle HTTP responses after they are received.
*/
private function sent(RequestInterface $request, array $options, PromiseInterface $promise): void
private function onResponse(ResponseInterface $response, RequestInterface $request): void
{
$clientRequest = new Request($request);
$this->app['events']->dispatch(new ResponseReceived(new Request($request), new Response($response)));
}

$promise->then(
fn(ResponseInterface $response) => event(new ResponseReceived($clientRequest, new Response($response))),
fn() => event(new ConnectionFailed($clientRequest)),
);
/**
* Handle a transaction error.
*/
private function onError(Throwable $e, RequestInterface $request): void
{
$this->app['events']->dispatch(new ConnectionFailed(new Request($request)));
}
}
Loading

0 comments on commit 00ddf7e

Please sign in to comment.