Skip to content

Feat: Exception handling #267

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
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
103 changes: 73 additions & 30 deletions src/Downloader/Downloader.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,17 @@

namespace RoachPHP\Downloader;

use Exception;
use RoachPHP\Events\ExceptionReceived;
use RoachPHP\Events\ExceptionReceiving;
use RoachPHP\Events\RequestDropped;
use RoachPHP\Events\RequestSending;
use RoachPHP\Events\ResponseDropped;
use RoachPHP\Events\ResponseReceived;
use RoachPHP\Events\ResponseReceiving;
use RoachPHP\Http\ClientInterface;
use RoachPHP\Http\Request;
use RoachPHP\Http\RequestException;
use RoachPHP\Http\Response;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

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

public function prepare(Request $request): void
public function prepare(Request $request, ?callable $onRejected = null): void
{
foreach ($this->middleware as $middleware) {
$request = $middleware->handleRequest($request);
try {
foreach ($this->middleware as $middleware) {
$request = $middleware->handleRequest($request);

if ($request->wasDropped()) {
$this->eventDispatcher->dispatch(
new RequestDropped($request),
RequestDropped::NAME,
);

return;
}
}

/**
* @psalm-suppress UnnecessaryVarAnnotation
*
* @var RequestSending $event
*/
$event = $this->eventDispatcher->dispatch(
new RequestSending($request),
RequestSending::NAME,
);

if ($request->wasDropped()) {
if ($event->request->wasDropped()) {
$this->eventDispatcher->dispatch(
new RequestDropped($request),
new RequestDropped($event->request),
RequestDropped::NAME,
);

return;
}
}

/**
* @psalm-suppress UnnecessaryVarAnnotation
*
* @var RequestSending $event
*/
$event = $this->eventDispatcher->dispatch(
new RequestSending($request),
RequestSending::NAME,
);

if ($event->request->wasDropped()) {
$this->eventDispatcher->dispatch(
new RequestDropped($event->request),
RequestDropped::NAME,
);

return;
$this->requests[] = $event->request;
} catch (Exception $exception) {
$this->onExceptionReceived(new RequestException($request, $exception), $onRejected);
}

$this->requests[] = $event->request;
}

public function flush(?callable $callback = null): void
public function flush(?callable $onFullFilled = null, ?callable $onRejected = null): void
{
$requests = $this->requests;

$this->requests = [];

foreach ($requests as $key => $request) {
if ($request->getResponse() !== null) {
$this->onResponseReceived($request->getResponse(), $callback);
$this->onResponseReceived($request->getResponse(), $onFullFilled);

unset($requests[$key]);
}
Expand All @@ -108,9 +116,15 @@ public function flush(?callable $callback = null): void
return;
}

$this->client->pool(\array_values($requests), function (Response $response) use ($callback): void {
$this->onResponseReceived($response, $callback);
});
$this->client->pool(
\array_values($requests),
function (Response $response) use ($onFullFilled): void {
$this->onResponseReceived($response, $onFullFilled);
},
function (RequestException $requestException) use ($onRejected): void {
$this->onExceptionReceived($requestException, $onRejected);
}
);
}

private function onResponseReceived(Response $response, ?callable $callback): void
Expand Down Expand Up @@ -158,4 +172,33 @@ private function onResponseReceived(Response $response, ?callable $callback): vo
$callback($response);
}
}

private function onExceptionReceived(RequestException $requestException, ?callable $callback): void
{
$this->eventDispatcher->dispatch(
new ExceptionReceiving($requestException),
ExceptionReceiving::NAME,
);

$handled = false;
foreach ($this->middleware as $middleware) {
$middleware->handleException($requestException);

if ($requestException->isHandled()) {
$handled = true;
}
}

$this->eventDispatcher->dispatch(
new ExceptionReceived($requestException),
ExceptionReceived::NAME,
);

if (null !== $callback) {
$callback($requestException);

} else if (!$handled) {
throw $requestException;
}
}
}
3 changes: 2 additions & 1 deletion src/Downloader/DownloaderMiddlewareInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@

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
{
}
17 changes: 14 additions & 3 deletions src/Downloader/Middleware/DownloaderMiddlewareAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@

namespace RoachPHP\Downloader\Middleware;

use Exception;
use RoachPHP\Downloader\DownloaderMiddlewareInterface;
use RoachPHP\Http\Request;
use RoachPHP\Http\RequestException;
use RoachPHP\Http\Response;

/**
Expand All @@ -23,12 +25,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;
Expand All @@ -55,12 +57,21 @@ public function handleResponse(Response $response): Response
return $response;
}

public function handleException(RequestException $requestException): RequestException
{
if ($this->middleware instanceof ExceptionMiddlewareInterface) {
return $this->middleware->handleException($requestException);
}

return $requestException;
}

public function configure(array $options): void
{
$this->middleware->configure($options);
}

public function getMiddleware(): RequestMiddlewareInterface|ResponseMiddlewareInterface
public function getMiddleware(): RequestMiddlewareInterface|ResponseMiddlewareInterface|ExceptionMiddlewareInterface
{
return $this->middleware;
}
Expand Down
13 changes: 13 additions & 0 deletions src/Downloader/Middleware/ExceptionMiddlewareInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace RoachPHP\Downloader\Middleware;

use RoachPHP\Http\RequestException;
use RoachPHP\Support\ConfigurableInterface;

interface ExceptionMiddlewareInterface extends ConfigurableInterface
{
public function handleException(RequestException $requestException): RequestException;
}
36 changes: 36 additions & 0 deletions src/Downloader/Middleware/FakeMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@

namespace RoachPHP\Downloader\Middleware;

use Exception;
use PHPUnit\Framework\Assert;
use RoachPHP\Downloader\DownloaderMiddlewareInterface;
use RoachPHP\Http\ExceptionContext;
use RoachPHP\Http\Request;
use RoachPHP\Http\RequestException;
use RoachPHP\Http\Response;
use RoachPHP\Support\Configurable;

Expand All @@ -36,13 +39,20 @@ final class FakeMiddleware implements DownloaderMiddlewareInterface
*/
private array $responsesHandled = [];

/**
* @var array Exception[]
*/
private array $exceptionsHandled = [];

/**
* @param ?\Closure(Request): Request $requestHandler
* @param ?\Closure(Response): Response $responseHandler
* @param ?\Closure(RequestException): RequestException $exceptionHandler
*/
public function __construct(
private ?\Closure $requestHandler = null,
private ?\Closure $responseHandler = null,
private ?\Closure $exceptionHandler = null,
) {
}

Expand All @@ -68,6 +78,17 @@ public function handleResponse(Response $response): Response
return $response;
}

public function handleException(RequestException $requestException): RequestException
{
$this->exceptionsHandled[] = $requestException->getReason();

if (null !== $this->exceptionHandler) {
return ($this->exceptionHandler)($requestException);
}

return $requestException;
}

public function assertRequestHandled(Request $request): void
{
Assert::assertContains($request, $this->requestsHandled);
Expand Down Expand Up @@ -97,4 +118,19 @@ public function assertNoResponseHandled(): void
{
Assert::assertEmpty($this->responsesHandled);
}

public function assertExceptionHandled(Exception $exception): void
{
Assert::assertContains($exception, $this->exceptionsHandled);
}

public function assertExceptionNotHandled(Exception $exception): void
{
Assert::assertNotContains($exception, $this->exceptionsHandled);
}

public function assertNoExceptionHandled(): void
{
Assert::assertEmpty($this->exceptionsHandled);
}
}
17 changes: 17 additions & 0 deletions src/Events/ExceptionReceived.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace RoachPHP\Events;

use Exception;
use Symfony\Contracts\EventDispatcher\Event;

final class ExceptionReceived extends Event
{
public const NAME = 'exception.processed';

public function __construct(public Exception $exception)
{
}
}
17 changes: 17 additions & 0 deletions src/Events/ExceptionReceiving.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace RoachPHP\Events;

use Exception;
use Symfony\Contracts\EventDispatcher\Event;

final class ExceptionReceiving extends Event
{
public const NAME = 'exception.receiving';

public function __construct(public Exception $exception)
{
}
}
9 changes: 9 additions & 0 deletions src/Extensions/LoggerExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace RoachPHP\Extensions;

use Psr\Log\LoggerInterface;
use RoachPHP\Events\ExceptionReceived;
use RoachPHP\Events\ItemDropped;
use RoachPHP\Events\ItemScraped;
use RoachPHP\Events\RequestDropped;
Expand All @@ -39,6 +40,7 @@ public static function getSubscribedEvents(): array
RequestDropped::NAME => ['onRequestDropped', 100],
ItemScraped::NAME => ['onItemScraped', 100],
ItemDropped::NAME => ['onItemDropped', 100],
ExceptionReceived::NAME => ['onExceptionReceived', 100],
];
}

Expand Down Expand Up @@ -81,4 +83,11 @@ public function onItemDropped(ItemDropped $event): void
'reason' => $event->item->getDropReason(),
]);
}

public function onExceptionReceived(ExceptionReceived $event): void
{
$this->logger->warning('Exception received', [
'exception' => $event->exception,
]);
}
}
Loading