From a88a23dc75fbca347c4fc8c13a18834916a5fa58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Barto=C5=A1?= Date: Sun, 9 Jul 2023 21:02:57 +0200 Subject: [PATCH] MetaLoader: improve error messages, add tests --- src/Meta/MetaLoader.php | 55 +++++++++---- src/Rules/MappedObjectRule.php | 1 + tests/Doubles/InternalClassExtendingVO.php | 3 +- tests/Doubles/Meta/AbstractVO.php | 13 ++++ tests/Doubles/Meta/EnumVO.php | 10 +++ tests/Doubles/Meta/InterfaceVO.php | 13 ++++ tests/Unit/Meta/MetaLoaderTest.php | 77 +++++++++++++++++++ .../Unit/Processing/DefaultProcessorTest.php | 24 ------ 8 files changed, 158 insertions(+), 38 deletions(-) create mode 100644 tests/Doubles/Meta/AbstractVO.php create mode 100644 tests/Doubles/Meta/EnumVO.php create mode 100644 tests/Doubles/Meta/InterfaceVO.php diff --git a/src/Meta/MetaLoader.php b/src/Meta/MetaLoader.php index 5c7a3c56..dfdf4cdd 100644 --- a/src/Meta/MetaLoader.php +++ b/src/Meta/MetaLoader.php @@ -4,6 +4,7 @@ use Nette\Loaders\RobotLoader; use Orisai\Exceptions\Logic\InvalidArgument; +use Orisai\Exceptions\Message; use Orisai\ObjectMapper\MappedObject; use Orisai\ObjectMapper\Meta\Cache\MetaCache; use Orisai\ObjectMapper\Meta\Compile\ClassCompileMeta; @@ -13,7 +14,8 @@ use Orisai\ReflectionMeta\Structure\ClassStructure; use Orisai\SourceMap\ClassSource; use ReflectionClass; -use ReflectionEnum; +use ReflectionException; +use UnitEnum; use function array_merge; use function array_unique; use function array_values; @@ -45,6 +47,9 @@ public function __construct( $this->resolverFactory = $resolverFactory; } + /** + * @param class-string $class + */ public function load(string $class): RuntimeMeta { return $this->metaCache->load($class) @@ -70,32 +75,56 @@ private function getRuntimeMeta(string $class): RuntimeMeta */ private function validateClass(string $class): ReflectionClass { - if (!class_exists($class)) { + try { + /** @phpstan-ignore-next-line In case object is not a class, ReflectionException is thrown */ + $reflector = new ReflectionClass($class); + } catch (ReflectionException $exception) { throw InvalidArgument::create() - ->withMessage("Class '$class' does not exist"); + ->withMessage("Class '$class' does not exist."); } - $classRef = new ReflectionClass($class); - - if (!$classRef->isSubclassOf(MappedObject::class)) { + if (!$reflector->isSubclassOf(MappedObject::class)) { $mappedObjectClass = MappedObject::class; + $message = Message::create() + ->withContext("Resolving metadata of mapped object '$class'.") + ->withProblem('Class does not implement interface of mapped object.') + ->withSolution("Implement the '$mappedObjectClass' interface."); + + throw InvalidArgument::create() + ->withMessage($message); + } + + if ($reflector->isInterface()) { + $message = Message::create() + ->withContext("Resolving metadata of mapped object '$class'.") + ->withProblem("'$class' is an interface.") + ->withSolution('Load metadata only for classes.'); + throw InvalidArgument::create() - ->withMessage("Class '$class' should be subclass of '$mappedObjectClass'."); + ->withMessage($message); } - // Intentionally not calling isInstantiable() - we are able to skip (private) ctor - if ($classRef->isAbstract() || $classRef->isInterface()) { + if ($reflector->isAbstract()) { + $message = Message::create() + ->withContext("Resolving metadata of mapped object '$class'.") + ->withProblem("'$class' is abstract.") + ->withSolution('Load metadata only for non-abstract classes.'); + throw InvalidArgument::create() - ->withMessage("Class '$class' must be instantiable."); + ->withMessage($message); } - if ($classRef instanceof ReflectionEnum) { + if ($reflector->isSubclassOf(UnitEnum::class)) { + $message = Message::create() + ->withContext("Resolving metadata of mapped object '$class'.") + ->withProblem("Mapped object can't be an enum."); + throw InvalidArgument::create() - ->withMessage("Class '$class' can't be an enum."); + ->withMessage($message); } - return $classRef; + return $reflector; } /** diff --git a/src/Rules/MappedObjectRule.php b/src/Rules/MappedObjectRule.php index 66b5893f..49ba4b6f 100644 --- a/src/Rules/MappedObjectRule.php +++ b/src/Rules/MappedObjectRule.php @@ -43,6 +43,7 @@ public function resolveArgs(array $args, ArgsContext $context): MappedObjectArgs if (!array_key_exists($type, $this->alreadyResolved)) { $this->alreadyResolved[$type] = null; try { + /** @phpstan-ignore-next-line Meta loader validates type */ $context->getMetaLoader()->load($type); } catch (Throwable $e) { unset($this->alreadyResolved[$type]); diff --git a/tests/Doubles/InternalClassExtendingVO.php b/tests/Doubles/InternalClassExtendingVO.php index 3dd35709..80700915 100644 --- a/tests/Doubles/InternalClassExtendingVO.php +++ b/tests/Doubles/InternalClassExtendingVO.php @@ -4,8 +4,9 @@ use Orisai\ObjectMapper\MappedObject; use Orisai\ObjectMapper\Rules\StringValue; +use stdClass; -final class InternalClassExtendingVO extends \stdClass implements MappedObject +final class InternalClassExtendingVO extends stdClass implements MappedObject { /** @StringValue() */ diff --git a/tests/Doubles/Meta/AbstractVO.php b/tests/Doubles/Meta/AbstractVO.php new file mode 100644 index 00000000..ff4a0f73 --- /dev/null +++ b/tests/Doubles/Meta/AbstractVO.php @@ -0,0 +1,13 @@ +expectException(InvalidArgument::class); + $this->expectExceptionMessage("Class 'foo' does not exist."); + + /** @phpstan-ignore-next-line */ + $this->metaLoader->load('foo'); + } + + public function testNotAMappedObject(): void + { + $this->expectException(InvalidArgument::class); + $this->expectExceptionMessage( + <<<'TXT' +Context: Resolving metadata of mapped object 'stdClass'. +Problem: Class does not implement interface of mapped object. +Solution: Implement the 'Orisai\ObjectMapper\MappedObject' interface. +TXT, + ); + + /** @phpstan-ignore-next-line */ + $this->metaLoader->load(stdClass::class); + } + + public function testAbstractClass(): void + { + $this->expectException(InvalidArgument::class); + $this->expectExceptionMessage( + <<<'TXT' +Context: Resolving metadata of mapped object + 'Tests\Orisai\ObjectMapper\Doubles\Meta\AbstractVO'. +Problem: 'Tests\Orisai\ObjectMapper\Doubles\Meta\AbstractVO' is abstract. +Solution: Load metadata only for non-abstract classes. +TXT, + ); + + $this->metaLoader->load(AbstractVO::class); + } + + public function testInterface(): void + { + $this->expectException(InvalidArgument::class); + $this->expectExceptionMessage( + <<<'TXT' +Context: Resolving metadata of mapped object + 'Tests\Orisai\ObjectMapper\Doubles\Meta\InterfaceVO'. +Problem: 'Tests\Orisai\ObjectMapper\Doubles\Meta\InterfaceVO' is an interface. +Solution: Load metadata only for classes. +TXT, + ); + + $this->metaLoader->load(InterfaceVO::class); + } + + public function testEnum(): void + { + if (PHP_VERSION_ID < 8_01_00) { + self::markTestSkipped('Enums are available on PHP 8.1+'); + } + + $this->expectException(InvalidArgument::class); + $this->expectExceptionMessage( + <<<'TXT' +Context: Resolving metadata of mapped object + 'Tests\Orisai\ObjectMapper\Doubles\Meta\EnumVO'. +Problem: Mapped object can't be an enum. +TXT, + ); + + $this->metaLoader->load(EnumVO::class); + } + /** * @runInSeparateProcess */ @@ -33,6 +109,7 @@ public function testPreload(): void $excludes[] = __DIR__ . '/../../Doubles/Meta/ClassMetaInvalidScopeRootVO.php'; $excludes[] = __DIR__ . '/../../Doubles/Meta/ClassInterfaceMetaInvalidScopeRootVO.php'; $excludes[] = __DIR__ . '/../../Doubles/Meta/ClassTraitMetaInvalidScopeRootVO.php'; + $excludes[] = __DIR__ . '/../../Doubles/Meta/EnumVO.php'; $excludes[] = __DIR__ . '/../../Doubles/Meta/FieldMetaInvalidScopeRootVO.php'; $excludes[] = __DIR__ . '/../../Doubles/Meta/FieldTraitMetaInvalidScopeRootVO.php'; $excludes[] = __DIR__ . '/../../Doubles/Meta/StaticMappedPropertyVO.php'; diff --git a/tests/Unit/Processing/DefaultProcessorTest.php b/tests/Unit/Processing/DefaultProcessorTest.php index 483fcbc9..65a7cb60 100644 --- a/tests/Unit/Processing/DefaultProcessorTest.php +++ b/tests/Unit/Processing/DefaultProcessorTest.php @@ -4,7 +4,6 @@ use DateTimeImmutable; use DateTimeInterface; -use Orisai\Exceptions\Logic\InvalidArgument; use Orisai\Exceptions\Logic\InvalidState; use Orisai\ObjectMapper\Exception\InvalidData; use Orisai\ObjectMapper\MappedObject; @@ -68,7 +67,6 @@ use Tests\Orisai\ObjectMapper\Doubles\StructuresVO; use Tests\Orisai\ObjectMapper\Doubles\TransformingVO; use Tests\Orisai\ObjectMapper\Toolkit\ProcessingTestCase; -use function sprintf; use const PHP_VERSION_ID; final class DefaultProcessorTest extends ProcessingTestCase @@ -1321,28 +1319,6 @@ public function testSkippedFieldAlreadyInitialized(): void $this->processor->processSkippedFields(['whatever'], $vo); } - public function testNotAClass(): void - { - $this->expectException(InvalidArgument::class); - $this->expectExceptionMessage("Class 'foo' does not exist"); - - /** @phpstan-ignore-next-line */ - $this->processor->process([], 'foo'); - } - - public function testNotAValueObject(): void - { - $this->expectException(InvalidArgument::class); - $this->expectExceptionMessage(sprintf( - "Class '%s' should be subclass of '%s'", - stdClass::class, - MappedObject::class, - )); - - /** @phpstan-ignore-next-line */ - $this->processor->process([], stdClass::class); - } - public function testAttributes(): void { if (PHP_VERSION_ID < 8_00_00) {