Skip to content

Commit

Permalink
Implement onRequest, onResponse and onError
Browse files Browse the repository at this point in the history
  • Loading branch information
cerbero90 committed Feb 12, 2024
1 parent 24887ed commit a0b2d87
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 61 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,15 @@ If we need a middleware to be added every time we invoke Lazy JSON Pages, we can
LazyJsonPages::globalMiddleware('fire_events', $fireEvents);
```

Sometimes writing Guzzle middleware might be cumbersome, alternatively Lazy JSON Pages provides convenient methods to fire callbacks when sending a request, receiving a response or dealing with a transaction error:

```php
LazyJsonPages::from($source)
->onRequest(fn(RequestInterface $request) => ...)
->onResponse(fn(ResponseInterface $response, RequestInterface $request) => ...)
->onError(fn(Throwable $e, RequestInterface $request, ?ResponseInterface $response) => ...);
```


### 💢 Errors handling

Expand Down
72 changes: 47 additions & 25 deletions src/LazyJsonPages.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
use Closure;
use GuzzleHttp\RequestOptions;
use Illuminate\Support\LazyCollection;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

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

/**
* The Guzzle HTTP request options.
*
* @var array<string, mixed>
*/
private array $options = [
RequestOptions::CONNECT_TIMEOUT => 5,
RequestOptions::READ_TIMEOUT => 5,
RequestOptions::TIMEOUT => 5,
];

/**
* The Guzzle client middleware.
*
* @var array<string, callable>
*/
private array $middleware = [];

/**
* The raw configuration of the API pagination.
*
Expand Down Expand Up @@ -195,7 +179,7 @@ public function async(int $requests): self
*/
public function connectionTimeout(float|int $seconds): self
{
$this->options[RequestOptions::CONNECT_TIMEOUT] = max(0, $seconds);
$this->factory->option(RequestOptions::CONNECT_TIMEOUT, max(0, $seconds));

return $this;
}
Expand All @@ -205,8 +189,8 @@ public function connectionTimeout(float|int $seconds): self
*/
public function requestTimeout(float|int $seconds): self
{
$this->options[RequestOptions::TIMEOUT] = max(0, $seconds);
$this->options[RequestOptions::READ_TIMEOUT] = max(0, $seconds);
$this->factory->option(RequestOptions::TIMEOUT, max(0, $seconds));
$this->factory->option(RequestOptions::READ_TIMEOUT, max(0, $seconds));

return $this;
}
Expand Down Expand Up @@ -236,7 +220,43 @@ public function backoff(Closure $callback): self
*/
public function middleware(string $name, callable $middleware): self
{
$this->middleware[$name] = $middleware;
$this->factory->middleware($name, $middleware);

return $this;
}

/**
* Handle the sending request.
*
* @param (Closure(RequestInterface): void) $callback
*/
public function onRequest(Closure $callback): self
{
$this->factory->onRequest($callback);

return $this;
}

/**
* Handle the received response.
*
* @param (Closure(ResponseInterface, RequestInterface): void) $callback
*/
public function onResponse(Closure $callback): self
{
$this->factory->onResponse($callback);

return $this;
}

/**
* Handle a transaction error.
*
* @param (Closure(\Throwable, RequestInterface, ?ResponseInterface): void) $callback
*/
public function onError(Closure $callback): self
{
$this->factory->onError($callback);

return $this;
}
Expand All @@ -249,9 +269,11 @@ public function middleware(string $name, callable $middleware): self
*/
public function collect(string $dot = '*'): LazyCollection
{
return new LazyCollection(function() use ($dot) {
$config = new Config(...$this->config, itemsPointer: DotsConverter::toPointer($dot));
$client = $this->factory->options($this->options)->middleware($this->middleware)->make();
$this->config['itemsPointer'] = DotsConverter::toPointer($dot);

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

yield from new AnyPagination($source, $client, $config);
Expand Down
111 changes: 99 additions & 12 deletions src/Services/ClientFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@

use Closure;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\Promise\Create;
use GuzzleHttp\RequestOptions;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

/**
* The HTTP client factory.
Expand All @@ -22,6 +26,9 @@ final class ClientFactory
* @var array<string, mixed>
*/
private static array $defaultOptions = [
RequestOptions::CONNECT_TIMEOUT => 5,
RequestOptions::READ_TIMEOUT => 5,
RequestOptions::TIMEOUT => 5,
RequestOptions::STREAM => true,
RequestOptions::HEADERS => [
'Accept' => 'application/json',
Expand All @@ -46,10 +53,31 @@ final class ClientFactory
/**
* The local middleware.
*
* @var array<string, mixed>
* @var array<string, callable>
*/
private array $middleware = [];

/**
* The callbacks to handle the sending request.
*
* @var Closure[]
*/
private array $onRequestCallbacks = [];

/**
* The callbacks to handle the received response.
*
* @var Closure[]
*/
private array $onResponseCallbacks = [];

/**
* The callbacks to handle a transaction error.
*
* @var Closure[]
*/
private array $onErrorCallbacks = [];

/**
* Add a global middleware.
*/
Expand All @@ -61,7 +89,7 @@ public static function globalMiddleware(string $name, callable $middleware): voi
/**
* Fake HTTP requests for testing purposes.
*
* @param \Psr\Http\Message\ResponseInterface[]|GuzzleHttp\Exception\RequestException[] $responses
* @param ResponseInterface[]|RequestException[] $responses
* @return array<int, array<string, mixed>>
*/
public static function fake(array $responses, Closure $callback): array
Expand All @@ -82,29 +110,88 @@ public static function fake(array $responses, Closure $callback): array
}

/**
* Set the Guzzle client options.
*
* @param array<string, mixed> $options
* Add the given Guzzle client option.
*/
public function options(array $options): self
public function option(string $name, mixed $value): self
{
$this->options = $options;
$this->options[$name] = $value;

return $this;
}

/**
* Set the Guzzle client middleware.
*
* @param array<string, callable> $middleware
* Add the given Guzzle client middleware.
*/
public function middleware(array $middleware): self
public function middleware(string $name, callable $middleware): self
{
$this->middleware = $middleware;
$this->middleware[$name] = $middleware;

return $this;
}

/**
* Add the given callback to handle the sending request.
*/
public function onRequest(Closure $callback): self
{
$this->onRequestCallbacks[] = $callback;

return $this->tap();
}

/**
* Add the middleware to handle a request before and after it is sent.
*/
private function tap(): self
{
$this->middleware['lazy_json_pages_tap'] ??= function (callable $handler): callable {
return function (RequestInterface $request, array $options) use ($handler) {
foreach ($this->onRequestCallbacks as $callback) {
$callback($request);
}

return $handler($request, $options)->then(function(ResponseInterface $response) use ($request) {
foreach ($this->onResponseCallbacks as $callback) {
$callback($response, $request);
}

return $response;
}, function(mixed $reason) use ($request) {
$exception = Create::exceptionFor($reason);
$response = $reason instanceof RequestException ? $reason->getResponse() : null;

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

return Create::rejectionFor($reason);
});
};
};

return $this;
}

/**
* Add the given callback to handle the received response.
*/
public function onResponse(Closure $callback): self
{
$this->onResponseCallbacks[] = $callback;

return $this->tap();
}

/**
* Add the given callback to handle a transaction error.
*/
public function onError(Closure $callback): self
{
$this->onErrorCallbacks[] = $callback;

return $this->tap();
}

/**
* Retrieve a configured Guzzle client instance.
*/
Expand Down
24 changes: 0 additions & 24 deletions tests/Feature/OptimizationTest.php

This file was deleted.

51 changes: 51 additions & 0 deletions tests/Feature/RequestsOptimizationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

use Cerbero\LazyJsonPages\LazyJsonPages;
use GuzzleHttp\Middleware;

it('adds middleware for Guzzle', function () {
$log = collect();

$lazyCollection = LazyJsonPages::from('https://example.com/api/v1/users')
->middleware('log', Middleware::tap(fn() => $log->push('before'), fn() => $log->push('after')))
->onRequest(fn() => $log->push('onRequest'))
->onResponse(fn() => $log->push('onResponse'))
->sync()
->totalPages('meta.total_pages')
->collect('data.*');

expect($lazyCollection)->toLoadItemsViaRequests([
'https://example.com/api/v1/users' => 'pagination/page1.json',
'https://example.com/api/v1/users?page=2' => 'pagination/page2.json',
'https://example.com/api/v1/users?page=3' => 'pagination/page3.json',
]);

expect($log)->sequence(...[
'before',
'onRequest',
'after',
'onResponse',
'before',
'onRequest',
'after',
'onResponse',
'before',
'onRequest',
'after',
'onResponse',
]);
});

it('handles transaction errors', function () {
$log = collect();

$lazyCollection = LazyJsonPages::from('https://example.com/api/v1/users')
->middleware('log', Middleware::tap(fn() => $log->push('before'), fn() => $log->push('after')))
->onError(fn() => $log->push('onError'))
->totalPages('meta.total_pages')
->collect('data.*');

expect($lazyCollection)->toFailRequest('https://example.com/api/v1/users');

expect($log)->sequence('before', 'after', 'onError');
});

0 comments on commit a0b2d87

Please sign in to comment.