Skip to content

Commit 30c7235

Browse files
committed
Add exception handling to downloader
1 parent 2bd659d commit 30c7235

10 files changed

+320
-59
lines changed

src/Downloader/Downloader.php

Lines changed: 69 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,17 @@
1313

1414
namespace RoachPHP\Downloader;
1515

16+
use Exception;
17+
use RoachPHP\Events\ExceptionReceived;
18+
use RoachPHP\Events\ExceptionReceiving;
1619
use RoachPHP\Events\RequestDropped;
1720
use RoachPHP\Events\RequestSending;
1821
use RoachPHP\Events\ResponseDropped;
1922
use RoachPHP\Events\ResponseReceived;
2023
use RoachPHP\Events\ResponseReceiving;
2124
use RoachPHP\Http\ClientInterface;
2225
use RoachPHP\Http\Request;
26+
use RoachPHP\Http\RequestException;
2327
use RoachPHP\Http\Response;
2428
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
2529

@@ -53,52 +57,56 @@ public function scheduledRequests(): int
5357
return \count($this->requests);
5458
}
5559

56-
public function prepare(Request $request): void
60+
public function prepare(Request $request, ?callable $onRejected): void
5761
{
58-
foreach ($this->middleware as $middleware) {
59-
$request = $middleware->handleRequest($request);
62+
try {
63+
foreach ($this->middleware as $middleware) {
64+
$request = $middleware->handleRequest($request);
65+
66+
if ($request->wasDropped()) {
67+
$this->eventDispatcher->dispatch(
68+
new RequestDropped($request),
69+
RequestDropped::NAME,
70+
);
71+
72+
return;
73+
}
74+
}
75+
76+
/**
77+
* @psalm-suppress UnnecessaryVarAnnotation
78+
*
79+
* @var RequestSending $event
80+
*/
81+
$event = $this->eventDispatcher->dispatch(
82+
new RequestSending($request),
83+
RequestSending::NAME,
84+
);
6085

61-
if ($request->wasDropped()) {
86+
if ($event->request->wasDropped()) {
6287
$this->eventDispatcher->dispatch(
63-
new RequestDropped($request),
88+
new RequestDropped($event->request),
6489
RequestDropped::NAME,
6590
);
6691

6792
return;
6893
}
69-
}
70-
71-
/**
72-
* @psalm-suppress UnnecessaryVarAnnotation
73-
*
74-
* @var RequestSending $event
75-
*/
76-
$event = $this->eventDispatcher->dispatch(
77-
new RequestSending($request),
78-
RequestSending::NAME,
79-
);
8094

81-
if ($event->request->wasDropped()) {
82-
$this->eventDispatcher->dispatch(
83-
new RequestDropped($event->request),
84-
RequestDropped::NAME,
85-
);
86-
87-
return;
95+
$this->requests[] = $event->request;
96+
} catch (Exception $exception) {
97+
$this->onExceptionReceived($exception, $request, $onRejected);
8898
}
89-
90-
$this->requests[] = $event->request;
9199
}
92100

93-
public function flush(?callable $callback = null): void
101+
public function flush(?callable $onFullFilled = null, ?callable $onRejected = null): void
94102
{
95103
$requests = $this->requests;
96104

97105
$this->requests = [];
98106

99107
foreach ($requests as $key => $request) {
100108
if ($request->getResponse() !== null) {
101-
$this->onResponseReceived($request->getResponse(), $callback);
109+
$this->onResponseReceived($request->getResponse(), $onFullFilled);
102110

103111
unset($requests[$key]);
104112
}
@@ -108,9 +116,19 @@ public function flush(?callable $callback = null): void
108116
return;
109117
}
110118

111-
$this->client->pool(\array_values($requests), function (Response $response) use ($callback): void {
112-
$this->onResponseReceived($response, $callback);
113-
});
119+
$this->client->pool(
120+
\array_values($requests),
121+
function (Response $response) use ($onFullFilled): void {
122+
$this->onResponseReceived($response, $onFullFilled);
123+
},
124+
function (RequestException $requestException) use ($onRejected): void {
125+
$this->onExceptionReceived(
126+
$requestException->getReason(),
127+
$requestException->getRequest(),
128+
$onRejected,
129+
);
130+
}
131+
);
114132
}
115133

116134
private function onResponseReceived(Response $response, ?callable $callback): void
@@ -158,4 +176,25 @@ private function onResponseReceived(Response $response, ?callable $callback): vo
158176
$callback($response);
159177
}
160178
}
179+
180+
private function onExceptionReceived(\Throwable $exception, Request $request, ?callable $callback): void
181+
{
182+
$this->eventDispatcher->dispatch(
183+
new ExceptionReceiving($exception),
184+
ExceptionReceiving::NAME,
185+
);
186+
187+
foreach ($this->middleware as $middleware) {
188+
$request = $middleware->handleException($exception, $request);
189+
}
190+
191+
$this->eventDispatcher->dispatch(
192+
new ExceptionReceived($exception),
193+
ExceptionReceived::NAME,
194+
);
195+
196+
if (null !== $callback) {
197+
$callback($exception, $request);
198+
}
199+
}
161200
}

src/Downloader/DownloaderMiddlewareInterface.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@
1313

1414
namespace RoachPHP\Downloader;
1515

16+
use RoachPHP\Downloader\Middleware\ExceptionMiddlewareInterface;
1617
use RoachPHP\Downloader\Middleware\RequestMiddlewareInterface;
1718
use RoachPHP\Downloader\Middleware\ResponseMiddlewareInterface;
1819

19-
interface DownloaderMiddlewareInterface extends RequestMiddlewareInterface, ResponseMiddlewareInterface
20+
interface DownloaderMiddlewareInterface extends RequestMiddlewareInterface, ResponseMiddlewareInterface, ExceptionMiddlewareInterface
2021
{
2122
}

src/Downloader/Middleware/DownloaderMiddlewareAdapter.php

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace RoachPHP\Downloader\Middleware;
1515

16+
use Exception;
1617
use RoachPHP\Downloader\DownloaderMiddlewareInterface;
1718
use RoachPHP\Http\Request;
1819
use RoachPHP\Http\Response;
@@ -23,12 +24,12 @@
2324
final class DownloaderMiddlewareAdapter implements DownloaderMiddlewareInterface
2425
{
2526
private function __construct(
26-
private RequestMiddlewareInterface|ResponseMiddlewareInterface $middleware,
27+
private RequestMiddlewareInterface|ResponseMiddlewareInterface|ExceptionMiddlewareInterface $middleware,
2728
) {
2829
}
2930

3031
public static function fromMiddleware(
31-
RequestMiddlewareInterface|ResponseMiddlewareInterface $middleware,
32+
RequestMiddlewareInterface|ResponseMiddlewareInterface|ExceptionMiddlewareInterface $middleware,
3233
): DownloaderMiddlewareInterface {
3334
if ($middleware instanceof DownloaderMiddlewareInterface) {
3435
return $middleware;
@@ -55,12 +56,21 @@ public function handleResponse(Response $response): Response
5556
return $response;
5657
}
5758

59+
public function handleException(Exception $exception, Request $request): ?Request
60+
{
61+
if ($this->middleware instanceof ExceptionMiddlewareInterface) {
62+
return $this->middleware->handleException($exception, $request);
63+
}
64+
65+
return $request;
66+
}
67+
5868
public function configure(array $options): void
5969
{
6070
$this->middleware->configure($options);
6171
}
6272

63-
public function getMiddleware(): RequestMiddlewareInterface|ResponseMiddlewareInterface
73+
public function getMiddleware(): RequestMiddlewareInterface|ResponseMiddlewareInterface|ExceptionMiddlewareInterface
6474
{
6575
return $this->middleware;
6676
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* Copyright (c) 2025 Auke Geerts
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*
11+
* @see https://github.com/roach-php/roach
12+
*/
13+
14+
namespace RoachPHP\Downloader\Middleware;
15+
16+
use Exception;
17+
use RoachPHP\Http\Request;
18+
use RoachPHP\Support\ConfigurableInterface;
19+
20+
interface ExceptionMiddlewareInterface extends ConfigurableInterface
21+
{
22+
public function handleException(Exception $exception, Request $request): ?Request;
23+
}

src/Downloader/Middleware/FakeMiddleware.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace RoachPHP\Downloader\Middleware;
1515

16+
use Exception;
1617
use PHPUnit\Framework\Assert;
1718
use RoachPHP\Downloader\DownloaderMiddlewareInterface;
1819
use RoachPHP\Http\Request;
@@ -36,13 +37,19 @@ final class FakeMiddleware implements DownloaderMiddlewareInterface
3637
*/
3738
private array $responsesHandled = [];
3839

40+
/**
41+
* @var array Exception[]
42+
*/
43+
private array $exceptionsHandled = [];
44+
3945
/**
4046
* @param ?\Closure(Request): Request $requestHandler
4147
* @param ?\Closure(Response): Response $responseHandler
4248
*/
4349
public function __construct(
4450
private ?\Closure $requestHandler = null,
4551
private ?\Closure $responseHandler = null,
52+
private ?\Closure $exceptionHandler = null,
4653
) {
4754
}
4855

@@ -68,6 +75,17 @@ public function handleResponse(Response $response): Response
6875
return $response;
6976
}
7077

78+
public function handleException(Exception $exception, Request $request): ?Request
79+
{
80+
$this->exceptionsHandled[] = $exception;
81+
82+
if (null !== $this->exceptionHandler) {
83+
return ($this->exceptionHandler)($exception, $request);
84+
}
85+
86+
return $request;
87+
}
88+
7189
public function assertRequestHandled(Request $request): void
7290
{
7391
Assert::assertContains($request, $this->requestsHandled);
@@ -97,4 +115,19 @@ public function assertNoResponseHandled(): void
97115
{
98116
Assert::assertEmpty($this->responsesHandled);
99117
}
118+
119+
public function assertExceptionHandled(Exception $exception): void
120+
{
121+
Assert::assertContains($exception, $this->exceptionsHandled);
122+
}
123+
124+
public function assertExceptionNotHandled(Exception $exception): void
125+
{
126+
Assert::assertNotContains($exception, $this->exceptionsHandled);
127+
}
128+
129+
public function assertNoExceptionHandled(): void
130+
{
131+
Assert::assertEmpty($this->exceptionsHandled);
132+
}
100133
}

src/Events/ExceptionReceived.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace RoachPHP\Events;
6+
7+
use Exception;
8+
use Symfony\Contracts\EventDispatcher\Event;
9+
10+
final class ExceptionReceived extends Event
11+
{
12+
public const NAME = 'exception.processed';
13+
14+
public function __construct(public Exception $exception)
15+
{
16+
}
17+
}

src/Events/ExceptionReceiving.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace RoachPHP\Events;
6+
7+
use Exception;
8+
use Symfony\Contracts\EventDispatcher\Event;
9+
10+
final class ExceptionReceiving extends Event
11+
{
12+
public const NAME = 'exception.receiving';
13+
14+
public function __construct(public Exception $exception)
15+
{
16+
}
17+
}

src/Http/FakeClient.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
namespace RoachPHP\Http;
1515

16+
use GuzzleHttp\Exception\BadResponseException;
17+
use GuzzleHttp\Exception\GuzzleException;
1618
use GuzzleHttp\Psr7\Response as GuzzleResponse;
1719
use PHPUnit\Framework\Assert;
1820

@@ -26,11 +28,22 @@ final class FakeClient implements ClientInterface
2628
*/
2729
private array $sentRequestUrls = [];
2830

31+
/**
32+
* @var array<array-key, Request>
33+
*/
34+
private array $failingRequests = [];
35+
2936
public function pool(array $requests, ?callable $onFulfilled = null, ?callable $onRejected = null): void
3037
{
3138
foreach ($requests as $request) {
3239
$this->sentRequestUrls[] = $request->getUri();
3340

41+
if (null !== $onRejected && in_array($request, $this->failingRequests)) {
42+
$exception = new RequestException($request, new FakeGuzzleException());
43+
44+
$onRejected($exception);
45+
}
46+
3447
if (null !== $onFulfilled) {
3548
$response = new Response(new GuzzleResponse(), $request);
3649

@@ -39,6 +52,13 @@ public function pool(array $requests, ?callable $onFulfilled = null, ?callable $
3952
}
4053
}
4154

55+
public function makeRequestsFail(Request ...$request): static
56+
{
57+
$this->failingRequests = $request;
58+
59+
return $this;
60+
}
61+
4262
public function assertRequestWasSent(Request $request): void
4363
{
4464
$uri = $request->getUri();

src/Http/FakeGuzzleException.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace RoachPHP\Http;
4+
5+
use Exception;
6+
use GuzzleHttp\Exception\GuzzleException;
7+
8+
class FakeGuzzleException extends Exception implements GuzzleException
9+
{
10+
}

0 commit comments

Comments
 (0)