diff --git a/src/Binary/Loader/ChainLoader.php b/src/Binary/Loader/ChainLoader.php index ab5517335..e1b254594 100644 --- a/src/Binary/Loader/ChainLoader.php +++ b/src/Binary/Loader/ChainLoader.php @@ -11,17 +11,19 @@ namespace Liip\ImagineBundle\Binary\Loader; +use Liip\ImagineBundle\Exception\Binary\Loader\ChainAttemptNotLoadableException; +use Liip\ImagineBundle\Exception\Binary\Loader\ChainNotLoadableException; use Liip\ImagineBundle\Exception\Binary\Loader\NotLoadableException; class ChainLoader implements LoaderInterface { /** - * @var LoaderInterface[] + * @var array */ private array $loaders; /** - * @param LoaderInterface[] $loaders + * @param array $loaders */ public function __construct(array $loaders) { @@ -37,36 +39,14 @@ public function find($path) { $exceptions = []; - foreach ($this->loaders as $loader) { + foreach ($this->loaders as $configName => $loader) { try { return $loader->find($path); - } catch (\Exception $e) { - $exceptions[$e->getMessage()] = $loader; + } catch (NotLoadableException $loaderException) { + $exceptions[] = new ChainAttemptNotLoadableException($configName, $loader, $loaderException); } } - throw new NotLoadableException(self::getLoaderExceptionMessage($path, $exceptions, $this->loaders)); - } - - /** - * @param array $exceptions - * @param array $loaders - */ - private static function getLoaderExceptionMessage(string $path, array $exceptions, array $loaders): string - { - $loaderMessages = array_map(static function (string $name, LoaderInterface $loader) { - return sprintf('%s=[%s]', (new \ReflectionObject($loader))->getShortName(), $name); - }, array_keys($loaders), $loaders); - - $exceptionMessages = array_map(static function (string $message, LoaderInterface $loader) { - return sprintf('%s=[%s]', (new \ReflectionObject($loader))->getShortName(), $message); - }, array_keys($exceptions), $exceptions); - - return vsprintf('Source image not resolvable "%s" using "%s" %d loaders (internal exceptions: %s).', [ - $path, - implode(', ', $loaderMessages), - \count($loaders), - implode(', ', $exceptionMessages), - ]); + throw new ChainNotLoadableException($path, ...$exceptions); } } diff --git a/src/Exception/Binary/Loader/ChainAttemptNotLoadableException.php b/src/Exception/Binary/Loader/ChainAttemptNotLoadableException.php new file mode 100644 index 000000000..c19a20d95 --- /dev/null +++ b/src/Exception/Binary/Loader/ChainAttemptNotLoadableException.php @@ -0,0 +1,53 @@ +loaderIndex = $loaderIndex; + $this->loaderClass = $loaderClass; + + parent::__construct($this->compileMessageTxt(), 0, $loaderException); + } + + public function getLoaderIndex(): string + { + return $this->loaderIndex; + } + + public function getLoaderClass(): LoaderInterface + { + return $this->loaderClass; + } + + public function getLoaderClassName(): string + { + return (new \ReflectionObject($this->getLoaderClass()))->getShortName(); + } + + public function getLoaderException(): string + { + return $this->getPrevious()->getMessage(); + } + + private function compileMessageTxt(): string + { + return sprintf('%s=[%s]', $this->getLoaderClassName(), $this->getLoaderIndex()); + } +} diff --git a/src/Exception/Binary/Loader/ChainNotLoadableException.php b/src/Exception/Binary/Loader/ChainNotLoadableException.php new file mode 100644 index 000000000..e19a2dfbc --- /dev/null +++ b/src/Exception/Binary/Loader/ChainNotLoadableException.php @@ -0,0 +1,49 @@ +getMessage(); + }, ...$exceptions); + } + + private static function compileLoaderErrorsList(ChainAttemptNotLoadableException ...$exceptions): string + { + return self::implodeMappedExceptions(static function (ChainAttemptNotLoadableException $e): string { + return sprintf('%s=[%s]', $e->getLoaderClassName(), $e->getLoaderException()); + }, ...$exceptions); + } + + private static function implodeMappedExceptions(\Closure $mapper, ChainAttemptNotLoadableException ...$exceptions): string + { + return implode(', ', array_map($mapper, $exceptions)); + } +} diff --git a/tests/AbstractTest.php b/tests/AbstractTest.php index e97147f2c..f2a77b2ff 100644 --- a/tests/AbstractTest.php +++ b/tests/AbstractTest.php @@ -271,16 +271,39 @@ protected function createObjectMock(string $object, array $methods = [], bool $c return $builder->getMock(); } - /** - * @param object $object - */ - protected function getVisibilityRestrictedMethod($object, string $name): \ReflectionMethod + protected static function getReflectionObject(object $object): \ReflectionObject + { + return new \ReflectionObject($object); + } + + protected static function getReflectionObjectName(object $object): string { - $r = new \ReflectionObject($object); + return self::getReflectionObject($object)->getName(); + } - $m = $r->getMethod($name); + protected function getVisibilityRestrictedMethod($object, string $name): \ReflectionMethod + { + $m = self::getReflectionObject($object)->getMethod($name); $m->setAccessible(true); return $m; } + + protected static function generateRandomInteger(int $lowerBound = null, int $upperBound = null): int + { + $lowerBound = $lowerBound ?? 0; + $upperBound = $upperBound ?? 100000000; + + if ($lowerBound > $upperBound) { + throw new \LogicException('Lower-bound argument must be less-than or equal-to upper-bound argument!'); + } + + try { + return random_int($lowerBound, $upperBound); + } catch (\Throwable $e) { + throw new \RuntimeException(vsprintf('Failed to generate random number between %d and %d: %s', [ + $lowerBound, $upperBound, $e->getMessage() + ])); + } + } } diff --git a/tests/Binary/Loader/ChainLoaderTest.php b/tests/Binary/Loader/ChainLoaderTest.php index 0040698e1..894b6adc3 100644 --- a/tests/Binary/Loader/ChainLoaderTest.php +++ b/tests/Binary/Loader/ChainLoaderTest.php @@ -16,6 +16,8 @@ use Liip\ImagineBundle\Binary\Loader\LoaderInterface; use Liip\ImagineBundle\Binary\Locator\FileSystemLocator; use Liip\ImagineBundle\Binary\Locator\LocatorInterface; +use Liip\ImagineBundle\Exception\Binary\Loader\ChainAttemptNotLoadableException; +use Liip\ImagineBundle\Exception\Binary\Loader\ChainNotLoadableException; use Liip\ImagineBundle\Exception\Binary\Loader\NotLoadableException; use Liip\ImagineBundle\Model\FileBinary; use Liip\ImagineBundle\Tests\AbstractTest; @@ -23,47 +25,53 @@ /** * @covers \Liip\ImagineBundle\Binary\Loader\ChainLoader + * @covers \Liip\ImagineBundle\Exception\Binary\Loader\ChainAttemptNotLoadableException + * @covers \Liip\ImagineBundle\Exception\Binary\Loader\ChainNotLoadableException */ class ChainLoaderTest extends AbstractTest { - public function testImplementsLoaderInterface(): void + public function testChainLoaderImplementsLoaderInterface(): void { - $this->assertInstanceOf(LoaderInterface::class, $this->getChainLoader()); + $this->assertInstanceOf(LoaderInterface::class, self::instantiateChainLoader()); } - /** - * @return array[] - */ - public static function provideLoadCases(): array + public function testChainAttemptNotLoadableExceptionImplementsNotLoadableException(): void + { + $this->assertInstanceOfNotLoadableException( + self::instantiateChainAttemptNotLoadableException(), + vsprintf('Expected "%s" to be an instance of "%s"', [ + ChainAttemptNotLoadableException::class, + NotLoadableException::class, + ]) + ); + } + + public function testChainNotLoadableExceptionImplementsNotLoadableException(): void + { + $this->assertInstanceOfNotLoadableException( + self::instantiateChainNotLoadableException() + ); + } + + private function assertInstanceOfNotLoadableException(object $provided, string $message = ''): void + { + $this->assertInstanceOf(NotLoadableException::class, $provided, $message + ?? vsprintf('Expected class "%s" to be an instance of "%s"', [ + self::getReflectionObjectName($provided), NotLoadableException::class, + ]) + ); + } + + public static function provideLoadCases(): \Generator { $file = pathinfo(__FILE__, PATHINFO_BASENAME); - return [ - [ - __DIR__, - $file, - ], - [ - __DIR__.'/', - $file, - ], - [ - __DIR__, '/'. - $file, - ], - [ - __DIR__.'/../../Binary/Loader', - '/'.$file, - ], - [ - realpath(__DIR__.'/..'), - 'Loader/'.$file, - ], - [ - __DIR__.'/../', - '/Loader/../../Binary/Loader/'.$file, - ], - ]; + yield [__DIR__, $file]; + yield [__DIR__.'/', $file]; + yield [__DIR__, '/'.$file]; + yield [__DIR__.'/../../Binary/Loader', '/'.$file]; + yield [realpath(__DIR__.'/..'), 'Loader/'.$file]; + yield [__DIR__.'/../', '/Loader/../../Binary/Loader/'.$file]; } /** @@ -71,18 +79,19 @@ public static function provideLoadCases(): array */ public function testLoad(string $root, string $path): void { - $this->assertValidLoaderFindReturn($this->getChainLoader([$root])->find($path)); + $this->assertValidLoaderFindReturn(self::instantiateChainLoader([$root])->find($path), vsprintf( + 'Expected valid "%s::find()" return with root of "%s" and file path of "%s".', [ + ChainLoader::class, + $root, + $path, + ] + )); } - /** - * @return array[] - */ - public function provideInvalidPathsData(): array + public function provideInvalidPathsData(): \Generator { - return [ - ['../Loader/../../Binary/Loader/../../../Resources/config/routing.yaml'], - ['../../Binary/'], - ]; + yield ['../Loader/../../Binary/Loader/../../../Resources/config/routing.yaml']; + yield ['../../Binary/']; } /** @@ -91,9 +100,10 @@ public function provideInvalidPathsData(): array public function testThrowsIfFileDoesNotExist(string $path): void { $this->expectException(NotLoadableException::class); + $this->expectException(ChainNotLoadableException::class); $this->expectExceptionMessageMatchesBC('{Source image not resolvable "[^"]+" using "FileSystemLoader=\[foo\]" 1 loaders}'); - $this->getChainLoader()->find($path); + self::instantiateChainLoader()->find($path); } /** @@ -102,59 +112,79 @@ public function testThrowsIfFileDoesNotExist(string $path): void public function testThrowsIfFileDoesNotExistWithMultipleLoaders(string $path): void { $this->expectException(NotLoadableException::class); - $this->expectExceptionMessageMatchesBC('{Source image not resolvable "[^"]+" using "FileSystemLoader=\[foo\], FileSystemLoader=\[bar\]" 2 loaders \(internal exceptions: FileSystemLoader=\[.+\], FileSystemLoader=\[.+\]\)\.}'); + $this->expectException(ChainNotLoadableException::class); + $this->expectExceptionMessageMatchesBC( + '{Source image not resolvable "[^"]+" using "FileSystemLoader=\[foo\], '. + 'FileSystemLoader=\[bar\]" 2 loaders \(internal exceptions: FileSystemLoader=\[.+\], '. + 'FileSystemLoader=\[.+\]\)\.}' + ); - $this->getChainLoader([], [ - 'foo' => $this->createFileSystemLoader( - $this->getFileSystemLocator([ + self::instantiateChainLoader([], [ + 'foo' => self::instantiateFileSystemLoader( + self::instantiateFileSystemLocator([ realpath(__DIR__.'/../../'), ]) ), - 'bar' => $this->createFileSystemLoader( - $this->getFileSystemLocator([ + 'bar' => self::instantiateFileSystemLoader( + self::instantiateFileSystemLocator([ realpath(__DIR__.'/../../../'), ]) ), ])->find($path); } - /** - * @param string[] $paths - */ - private function getFileSystemLocator(array $paths = []): FileSystemLocator + private function assertValidLoaderFindReturn(FileBinary $return, string $message = ''): void { - return new FileSystemLocator($paths); + $this->assertInstanceOf(FileBinary::class, $return, $message); + $this->assertStringStartsWith('text/', $return->getMimeType(), $message); } - /** - * @param string[] $paths - * @param FileSystemLoader[] $loaders - */ - private function getChainLoader(array $paths = [], array $loaders = null): ChainLoader + private static function instantiateRandomlyPopulatedChainNotLoadableException(string $loaderPath = null, int $maxInternalExceptions = 10): ChainNotLoadableException { - if (null === $loaders) { - $loaders = [ - 'foo' => $this->createFileSystemLoader($this->getFileSystemLocator($paths ?: [__DIR__])), - ]; - } - - return new ChainLoader($loaders); + self::instantiateChainNotLoadableException($loaderPath, ...array_map(function (): ChainAttemptNotLoadableException { + return self::instantiateChainAttemptNotLoadableException(); + }, range(0, self::generateRandomInteger(1, $maxInternalExceptions)))); } - private function assertValidLoaderFindReturn(FileBinary $return, string $message = ''): void + private static function instantiateChainNotLoadableException(string $loaderPath = null, ChainAttemptNotLoadableException ...$exceptions): ChainNotLoadableException { - $this->assertInstanceOf(FileBinary::class, $return, $message); - $this->assertStringStartsWith('text/', $return->getMimeType(), $message); + return new ChainNotLoadableException($loaderPath ?? (__DIR__), ...$exceptions); } - private function createFileSystemLoader(LocatorInterface $locator): FileSystemLoader + private static function instantiateChainAttemptNotLoadableException(): ChainAttemptNotLoadableException { - $mimeTypes = MimeTypes::getDefault(); + return new ChainAttemptNotLoadableException( + $name = sprintf('conf-item-%d', static::generateRandomInteger(1000, 9999)), + self::instantiateFileSystemLoader(self::instantiateFileSystemLocator()), + new NotLoadableException(sprintf('the "%s" loader encountered an error', $name)) + ); + } + private static function instantiateFileSystemLoader(LocatorInterface $locator = null): FileSystemLoader + { return new FileSystemLoader( - $mimeTypes, - $mimeTypes, - $locator + $mime = MimeTypes::getDefault(), $mime, $locator ?? self::instantiateFileSystemLocator() ); } + + /** + * @param string[] $paths + */ + private static function instantiateFileSystemLocator(array $paths = []): FileSystemLocator + { + return new FileSystemLocator($paths); + } + + /** + * @param string[] $paths + * @param array $loaders + */ + private static function instantiateChainLoader(array $paths = [], array $loaders = null): ChainLoader + { + return new ChainLoader($loaders ?? [ + 'foo' => self::instantiateFileSystemLoader( + self::instantiateFileSystemLocator($paths ?: [__DIR__]) + ), + ]); + } }