diff --git a/src/Downloader/Downloader.php b/src/Downloader/Downloader.php index acc72cf..9ff9bd6 100644 --- a/src/Downloader/Downloader.php +++ b/src/Downloader/Downloader.php @@ -13,8 +13,10 @@ namespace RoachPHP\Downloader; +use RoachPHP\Downloader\Middleware\ExceptionMiddlewareInterface; use RoachPHP\Events\RequestDropped; use RoachPHP\Events\RequestSending; +use RoachPHP\Exception\Exception; use RoachPHP\Http\ClientInterface; use RoachPHP\Http\Request; use RoachPHP\Http\Response; @@ -78,14 +80,16 @@ public function prepare(Request $request): void $this->requests[] = $event->request; } - public function flush(?callable $callback = null): void + public function flush(?callable $onFullfilled = null): void { $requests = $this->requests; $this->requests = []; - $this->client->pool($requests, function (Response $response) use ($callback): void { - $this->onResponseReceived($response, $callback); + $this->client->pool($requests, function (Response $response) use ($onFullfilled): void { + $this->onResponseReceived($response, $onFullfilled); + }, function (Exception $exception): void { + $this->onExceptionHandled($exception); }); } @@ -103,4 +107,11 @@ private function onResponseReceived(Response $response, ?callable $callback): vo $callback($response); } } + + private function onExceptionHandled(Exception $exception): void + { + foreach ($this->middleware as $middleware) { + $exception = $middleware->handleException($exception); + } + } } diff --git a/src/Downloader/DownloaderMiddlewareInterface.php b/src/Downloader/DownloaderMiddlewareInterface.php index bb09173..19b3826 100644 --- a/src/Downloader/DownloaderMiddlewareInterface.php +++ b/src/Downloader/DownloaderMiddlewareInterface.php @@ -13,9 +13,13 @@ namespace RoachPHP\Downloader; +use RoachPHP\Downloader\Middleware\ExceptionMiddlewareInterface; use RoachPHP\Downloader\Middleware\RequestMiddlewareInterface; use RoachPHP\Downloader\Middleware\ResponseMiddlewareInterface; -interface DownloaderMiddlewareInterface extends RequestMiddlewareInterface, ResponseMiddlewareInterface +interface DownloaderMiddlewareInterface extends + RequestMiddlewareInterface, + ResponseMiddlewareInterface, + ExceptionMiddlewareInterface { } diff --git a/src/Downloader/Middleware/DownloaderMiddlewareAdapter.php b/src/Downloader/Middleware/DownloaderMiddlewareAdapter.php index 0445e4b..f5ceb3e 100644 --- a/src/Downloader/Middleware/DownloaderMiddlewareAdapter.php +++ b/src/Downloader/Middleware/DownloaderMiddlewareAdapter.php @@ -14,6 +14,7 @@ namespace RoachPHP\Downloader\Middleware; use RoachPHP\Downloader\DownloaderMiddlewareInterface; +use RoachPHP\Exception\Exception; use RoachPHP\Http\Request; use RoachPHP\Http\Response; @@ -23,12 +24,12 @@ final class DownloaderMiddlewareAdapter implements DownloaderMiddlewareInterface { private function __construct( - private RequestMiddlewareInterface | ResponseMiddlewareInterface $middleware, + private RequestMiddlewareInterface | ResponseMiddlewareInterface | ExceptionMiddlewareInterface $middleware, ) { } public static function fromMiddleware( - RequestMiddlewareInterface|ResponseMiddlewareInterface $middleware, + RequestMiddlewareInterface|ResponseMiddlewareInterface|ExceptionMiddlewareInterface $middleware, ): DownloaderMiddlewareInterface { if ($middleware instanceof DownloaderMiddlewareInterface) { return $middleware; @@ -55,6 +56,15 @@ public function handleResponse(Response $response): Response return $response; } + public function handleException(Exception $exception): Exception + { + if ($this->middleware instanceof ExceptionMiddlewareInterface) { + return $this->middleware->handleException($exception); + } + + return $exception; + } + public function configure(array $options): void { $this->middleware->configure($options); diff --git a/src/Downloader/Middleware/ExceptionMiddlewareInterface.php b/src/Downloader/Middleware/ExceptionMiddlewareInterface.php new file mode 100644 index 0000000..5b89f66 --- /dev/null +++ b/src/Downloader/Middleware/ExceptionMiddlewareInterface.php @@ -0,0 +1,22 @@ +exceptionsHandled[] = $exception; + + if (null !== $this->exceptionHandler) { + return ($this->exceptionHandler)($exception); + } + + return $exception; + } + public function assertRequestHandled(Request $request): void { Assert::assertContains($request, $this->requestsHandled); diff --git a/src/Downloader/Middleware/RetryMiddleware.php b/src/Downloader/Middleware/RetryMiddleware.php new file mode 100644 index 0000000..5e66392 --- /dev/null +++ b/src/Downloader/Middleware/RetryMiddleware.php @@ -0,0 +1,78 @@ +requestScheduler = $requestScheduler; + $this->eventDispatcher = $eventDispatcher; + } + + public function handleRequest(Request $request): Request + { + return $request->withMeta('max_retries', $this->option('max_retries')); + } + + + public function handleException(Exception $exception): Exception + { + $request = $exception->getRequest(); + + $retries = $request->getMeta('retries', 0); + if ($retries < $request->getMeta('max_retries')) { + $request = $request->withMeta('retries', ++$retries); + $this->eventDispatcher->dispatch( + new RequestScheduling($request), + RequestScheduling::NAME, + ); + + if ($request->wasDropped()) { + $this->eventDispatcher->dispatch( + new RequestDropped($request), + RequestDropped::NAME, + ); + + return $exception; + } + + $this->requestScheduler->schedule($request); + } + + return $exception; + } + + #[ArrayShape(['max_retries' => "int"])] + private function defaultOptions(): array + { + return [ + 'max_retries' => 5, + ]; + } +} diff --git a/src/Exception/Exception.php b/src/Exception/Exception.php new file mode 100644 index 0000000..713af6f --- /dev/null +++ b/src/Exception/Exception.php @@ -0,0 +1,39 @@ +request = $request; + } + + public function getRequest(): Request + { + return $this->request; + } + + public function setRequest(Request $request): static + { + $this->request = $request; + + return $this; + } +} diff --git a/src/Http/Client.php b/src/Http/Client.php index 9d6cdc9..9db4f8e 100644 --- a/src/Http/Client.php +++ b/src/Http/Client.php @@ -15,8 +15,10 @@ use Generator; use GuzzleHttp\Client as GuzzleClient; +use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\Pool; use Psr\Http\Message\ResponseInterface; +use RoachPHP\Exception\Exception; final class Client implements ClientInterface { @@ -30,14 +32,22 @@ public function __construct(?GuzzleClient $client = null) /** * @param Request[] $requests */ - public function pool(array $requests, ?callable $onFulfilled = null): void + public function pool(array $requests, ?callable $onFulfilled = null, ?callable $onRejected = null): void { $makeRequests = function () use ($requests): Generator { foreach ($requests as $request) { yield function () use ($request) { return $this->client ->sendAsync($request->getPsrRequest(), $request->getOptions()) - ->then(static fn (ResponseInterface $response) => new Response($response, $request)); + ->then( + static fn (ResponseInterface $response) => new Response($response, $request), + static fn (GuzzleException $e) => throw new Exception( + $request, + $e->getMessage(), + $e->getCode(), + $e + ), + ); }; } }; @@ -45,6 +55,7 @@ public function pool(array $requests, ?callable $onFulfilled = null): void $pool = new Pool($this->client, $makeRequests(), [ 'concurrency' => 0, 'fulfilled' => $onFulfilled, + 'rejected' => $onRejected, ]); $pool->promise()->wait(); diff --git a/src/Http/ClientInterface.php b/src/Http/ClientInterface.php index 6ab86b2..c91d9ed 100644 --- a/src/Http/ClientInterface.php +++ b/src/Http/ClientInterface.php @@ -18,6 +18,7 @@ interface ClientInterface /** * @param Request[] $requests * @param ?callable(Response): void $onFulfilled + * @param ?callable(Response): void $onRejected */ - public function pool(array $requests, ?callable $onFulfilled = null): void; + public function pool(array $requests, ?callable $onFulfilled = null, ?callable $onRejected = null): void; } diff --git a/src/Http/FakeClient.php b/src/Http/FakeClient.php index 9edd642..8c06872 100644 --- a/src/Http/FakeClient.php +++ b/src/Http/FakeClient.php @@ -26,7 +26,7 @@ final class FakeClient implements ClientInterface */ private array $sentRequestUrls = []; - public function pool(array $requests, ?callable $onFulfilled = null): void + public function pool(array $requests, ?callable $onFulfilled = null, ?callable $onRejected = null): void { foreach ($requests as $request) { $this->sentRequestUrls[] = $request->getUri(); diff --git a/src/Roach.php b/src/Roach.php index c9b871c..8893d95 100644 --- a/src/Roach.php +++ b/src/Roach.php @@ -70,7 +70,7 @@ private static function defaultContainer(): ContainerInterface $container->share(EventDispatcher::class, EventDispatcher::class); $container->share(EventDispatcherInterface::class, EventDispatcher::class); $container->add(ClockInterface::class, SystemClock::class); - $container->add( + $container->share( RequestSchedulerInterface::class, /** @psalm-suppress MixedReturnStatement, MixedInferredReturnType */ static fn (): RequestSchedulerInterface => $container->get(ArrayRequestScheduler::class), diff --git a/tests/Downloader/DownloaderMiddlewareAdapterTest.php b/tests/Downloader/DownloaderMiddlewareAdapterTest.php index a79914e..29b8af8 100644 --- a/tests/Downloader/DownloaderMiddlewareAdapterTest.php +++ b/tests/Downloader/DownloaderMiddlewareAdapterTest.php @@ -19,6 +19,7 @@ use RoachPHP\Downloader\Middleware\DownloaderMiddlewareAdapter; use RoachPHP\Downloader\Middleware\RequestMiddlewareInterface; use RoachPHP\Downloader\Middleware\ResponseMiddlewareInterface; +use RoachPHP\Exception\Exception; use RoachPHP\Http\Request; use RoachPHP\Http\Response; use RoachPHP\Support\Configurable; @@ -45,6 +46,11 @@ public function handleResponse(Response $response): Response { return $response; } + + public function handleException(Exception $exception): Exception + { + return $exception; + } }; $class = DownloaderMiddlewareAdapter::fromMiddleware($middleware);