diff --git a/src/Connector/ImportConnector.php b/src/Connector/ImportConnector.php index 1e0bb50..52171d3 100644 --- a/src/Connector/ImportConnector.php +++ b/src/Connector/ImportConnector.php @@ -7,6 +7,7 @@ use ScriptFUSION\Porter\Cache\CacheUnavailableException; use ScriptFUSION\Porter\Connector\Recoverable\RecoverableExceptionHandler; use ScriptFUSION\Porter\Connector\Recoverable\StatelessRecoverableExceptionHandler; +use ScriptFUSION\Porter\ExceptionDescriptor; /** * Connector whose lifecycle is synchronised with an import operation. Ensures correct ConnectionContext is delivered @@ -27,21 +28,26 @@ final class ImportConnector implements ConnectorWrapper * * @var RecoverableExceptionHandler */ - private $userReh; + private $userExceptionHandler; /** * Resource-defined exception handler called when a recoverable exception is thrown by Connector::fetch(). * * @var RecoverableExceptionHandler */ - private $resourceReh; + private $resourceExceptionHandler; private $maxFetchAttempts; + /** + * @var ExceptionDescriptor[] + */ + private $recoverableExceptionDescriptors = []; + /** * @param Connector|AsyncConnector $connector Wrapped connector. * @param ConnectionContext $connectionContext Connection context. - * @param RecoverableExceptionHandler $recoverableExceptionHandler + * @param RecoverableExceptionHandler $recoverableExceptionHandler User's recoverable exception handler. * @param int $maxFetchAttempts */ public function __construct( @@ -56,7 +62,7 @@ public function __construct( $this->connector = clone $connector; $this->connectionContext = $connectionContext; - $this->userReh = $recoverableExceptionHandler; + $this->userExceptionHandler = $recoverableExceptionHandler; $this->maxFetchAttempts = $maxFetchAttempts; } @@ -92,20 +98,35 @@ private function createExceptionHandler(): \Closure return function (\Exception $exception) use (&$userHandlerCloned, &$resourceHandlerCloned): void { // Throw exception instead of retrying, if unrecoverable. - if (!$exception instanceof RecoverableConnectorException) { + if (!$this->isRecoverable($exception)) { throw $exception; } // Call resource's exception handler, if defined. - if ($this->resourceReh) { - self::invokeHandler($this->resourceReh, $exception, $resourceHandlerCloned); + if ($this->resourceExceptionHandler) { + self::invokeHandler($this->resourceExceptionHandler, $exception, $resourceHandlerCloned); } // Call user's exception handler. - self::invokeHandler($this->userReh, $exception, $userHandlerCloned); + self::invokeHandler($this->userExceptionHandler, $exception, $userHandlerCloned); }; } + private function isRecoverable(\Exception $exception): bool + { + if ($exception instanceof RecoverableConnectorException) { + return true; + } + + foreach ($this->recoverableExceptionDescriptors as $exceptionDescriptor) { + if ($exceptionDescriptor->matches($exception)) { + return true; + } + } + + return false; + } + /** * Invokes the specified fetch exception handler, cloning it if required. * @@ -163,10 +184,20 @@ public function findBaseConnector() */ public function setRecoverableExceptionHandler(RecoverableExceptionHandler $recoverableExceptionHandler): void { - if ($this->resourceReh !== null) { + if ($this->resourceExceptionHandler !== null) { throw new \LogicException('Cannot set resource\'s recoverable exception handler: already set!'); } - $this->resourceReh = $recoverableExceptionHandler; + $this->resourceExceptionHandler = $recoverableExceptionHandler; + } + + /** + * Adds the specified exception descriptor, designating it as a recoverable exception type. + * + * @param ExceptionDescriptor $descriptor + */ + public function addRecoverableExceptionDescriptor(ExceptionDescriptor $descriptor): void + { + $this->recoverableExceptionDescriptors[] = $descriptor; } } diff --git a/src/ExceptionDescriptor.php b/src/ExceptionDescriptor.php new file mode 100644 index 0000000..96bbf67 --- /dev/null +++ b/src/ExceptionDescriptor.php @@ -0,0 +1,49 @@ +type = $type; + } + + public function matches(\Exception $exception): bool + { + if (!is_a($exception, $this->type)) { + return false; + } + + if ($this->message !== null && $exception->getMessage() !== $this->message) { + return false; + } + + return true; + } + + public function getType(): string + { + return $this->type; + } + + public function setMessage(?string $message): self + { + $this->message = $message; + + return $this; + } + + public function getMessage(): ?string + { + return $this->message; + } +} diff --git a/test/Integration/Porter/ExceptionDescriptorTest.php b/test/Integration/Porter/ExceptionDescriptorTest.php new file mode 100644 index 0000000..8d5d964 --- /dev/null +++ b/test/Integration/Porter/ExceptionDescriptorTest.php @@ -0,0 +1,72 @@ +matches($exception)); + } + + public function provideMatches(): array + { + return [ + 'Class match' => [new ExceptionDescriptor(\LogicException::class), new \LogicException], + 'Subclass match' => [new ExceptionDescriptor(\LogicException::class), new \DomainException], + 'Message exact match' => [ + (new ExceptionDescriptor(\Exception::class)) + ->setMessage('foo'), + new \Exception('foo') + ], + 'Null message' => [ + (new ExceptionDescriptor(\Exception::class)) + ->setMessage(null), + new \Exception('foo') + ], + ]; + } + + /** + * @dataProvider provideNonMatches + */ + public function testNonMatches(ExceptionDescriptor $descriptor, \Exception $exception): void + { + self::assertFalse($descriptor->matches($exception)); + } + + public function provideNonMatches(): array + { + return [ + 'Class mismatch' => [new ExceptionDescriptor(\LogicException::class), new \RuntimeException], + 'Superclass mismatch' => [new ExceptionDescriptor(\DomainException::class), new \LogicException], + 'Message mismatch' => [ + (new ExceptionDescriptor(\Exception::class)) + ->setMessage('foo'), + new \Exception('bar') + ], + 'Message blank' => [ + (new ExceptionDescriptor(\Exception::class)) + ->setMessage(''), + new \Exception('foo') + ], + 'Message partial match' => [ + (new ExceptionDescriptor(\Exception::class)) + ->setMessage('foo'), + new \Exception('foo ') + ], + 'Message case mismatch' => [ + (new ExceptionDescriptor(\Exception::class)) + ->setMessage('foo'), + new \Exception('Foo') + ] + ]; + } +} diff --git a/test/Unit/Porter/Connector/ImportConnectorTest.php b/test/Unit/Porter/Connector/ImportConnectorTest.php index 2de4bab..a422b32 100644 --- a/test/Unit/Porter/Connector/ImportConnectorTest.php +++ b/test/Unit/Porter/Connector/ImportConnectorTest.php @@ -194,7 +194,7 @@ public function testStatelessExceptionHandlerNotCloned(): void $handler, \Closure::bind( function (): RecoverableExceptionHandler { - return $this->userReh; + return $this->userExceptionHandler; }, $connector, $connector