Skip to content

Commit 03f34ac

Browse files
committed
MetaLoader: improve error messages, add tests
1 parent 7efdb71 commit 03f34ac

File tree

8 files changed

+159
-38
lines changed

8 files changed

+159
-38
lines changed

src/Meta/MetaLoader.php

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Nette\Loaders\RobotLoader;
66
use Orisai\Exceptions\Logic\InvalidArgument;
7+
use Orisai\Exceptions\Message;
78
use Orisai\ObjectMapper\MappedObject;
89
use Orisai\ObjectMapper\Meta\Cache\MetaCache;
910
use Orisai\ObjectMapper\Meta\Compile\ClassCompileMeta;
@@ -13,7 +14,8 @@
1314
use Orisai\ReflectionMeta\Structure\ClassStructure;
1415
use Orisai\SourceMap\ClassSource;
1516
use ReflectionClass;
16-
use ReflectionEnum;
17+
use ReflectionException;
18+
use UnitEnum;
1719
use function array_merge;
1820
use function array_unique;
1921
use function array_values;
@@ -22,6 +24,7 @@
2224
use function interface_exists;
2325
use function is_subclass_of;
2426
use function trait_exists;
27+
use const PHP_VERSION_ID;
2528

2629
final class MetaLoader
2730
{
@@ -45,6 +48,9 @@ public function __construct(
4548
$this->resolverFactory = $resolverFactory;
4649
}
4750

51+
/**
52+
* @param class-string<MappedObject> $class
53+
*/
4854
public function load(string $class): RuntimeMeta
4955
{
5056
return $this->metaCache->load($class)
@@ -70,32 +76,56 @@ private function getRuntimeMeta(string $class): RuntimeMeta
7076
*/
7177
private function validateClass(string $class): ReflectionClass
7278
{
73-
if (!class_exists($class)) {
79+
try {
80+
/** @phpstan-ignore-next-line In case object is not a class, ReflectionException is thrown */
81+
$reflector = new ReflectionClass($class);
82+
} catch (ReflectionException $exception) {
7483
throw InvalidArgument::create()
75-
->withMessage("Class '$class' does not exist");
84+
->withMessage("Class '$class' does not exist.");
7685
}
7786

78-
$classRef = new ReflectionClass($class);
79-
80-
if (!$classRef->isSubclassOf(MappedObject::class)) {
87+
if (!$reflector->isSubclassOf(MappedObject::class)) {
8188
$mappedObjectClass = MappedObject::class;
8289

90+
$message = Message::create()
91+
->withContext("Resolving metadata of mapped object '$class'.")
92+
->withProblem('Class does not implement interface of mapped object.')
93+
->withSolution("Implement the '$mappedObjectClass' interface.");
94+
95+
throw InvalidArgument::create()
96+
->withMessage($message);
97+
}
98+
99+
if ($reflector->isInterface()) {
100+
$message = Message::create()
101+
->withContext("Resolving metadata of mapped object '$class'.")
102+
->withProblem("'$class' is an interface.")
103+
->withSolution('Load metadata only for classes.');
104+
83105
throw InvalidArgument::create()
84-
->withMessage("Class '$class' should be subclass of '$mappedObjectClass'.");
106+
->withMessage($message);
85107
}
86108

87-
// Intentionally not calling isInstantiable() - we are able to skip (private) ctor
88-
if ($classRef->isAbstract() || $classRef->isInterface()) {
109+
if ($reflector->isAbstract()) {
110+
$message = Message::create()
111+
->withContext("Resolving metadata of mapped object '$class'.")
112+
->withProblem("'$class' is abstract.")
113+
->withSolution('Load metadata only for non-abstract classes.');
114+
89115
throw InvalidArgument::create()
90-
->withMessage("Class '$class' must be instantiable.");
116+
->withMessage($message);
91117
}
92118

93-
if ($classRef instanceof ReflectionEnum) {
119+
if (PHP_VERSION_ID >= 8_01_00 && $reflector->isSubclassOf(UnitEnum::class)) {
120+
$message = Message::create()
121+
->withContext("Resolving metadata of mapped object '$class'.")
122+
->withProblem("Mapped object can't be an enum.");
123+
94124
throw InvalidArgument::create()
95-
->withMessage("Class '$class' can't be an enum.");
125+
->withMessage($message);
96126
}
97127

98-
return $classRef;
128+
return $reflector;
99129
}
100130

101131
/**

src/Rules/MappedObjectRule.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ public function resolveArgs(array $args, ArgsContext $context): MappedObjectArgs
4343
if (!array_key_exists($type, $this->alreadyResolved)) {
4444
$this->alreadyResolved[$type] = null;
4545
try {
46+
/** @phpstan-ignore-next-line Meta loader validates type */
4647
$context->getMetaLoader()->load($type);
4748
} catch (Throwable $e) {
4849
unset($this->alreadyResolved[$type]);

tests/Doubles/InternalClassExtendingVO.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44

55
use Orisai\ObjectMapper\MappedObject;
66
use Orisai\ObjectMapper\Rules\StringValue;
7+
use stdClass;
78

8-
final class InternalClassExtendingVO extends \stdClass implements MappedObject
9+
final class InternalClassExtendingVO extends stdClass implements MappedObject
910
{
1011

1112
/** @StringValue() */

tests/Doubles/Meta/AbstractVO.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Tests\Orisai\ObjectMapper\Doubles\Meta;
4+
5+
use Orisai\ObjectMapper\MappedObject;
6+
7+
/**
8+
* phpcs:disable SlevomatCodingStandard.Classes.SuperfluousAbstractClassNaming
9+
*/
10+
abstract class AbstractVO implements MappedObject
11+
{
12+
13+
}

tests/Doubles/Meta/EnumVO.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Tests\Orisai\ObjectMapper\Doubles\Meta;
4+
5+
use Orisai\ObjectMapper\MappedObject;
6+
7+
enum EnumVO: string implements MappedObject
8+
{
9+
10+
}

tests/Doubles/Meta/InterfaceVO.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Tests\Orisai\ObjectMapper\Doubles\Meta;
4+
5+
use Orisai\ObjectMapper\MappedObject;
6+
7+
/**
8+
* phpcs:disable SlevomatCodingStandard.Classes.SuperfluousInterfaceNaming
9+
*/
10+
interface InterfaceVO extends MappedObject
11+
{
12+
13+
}

tests/Unit/Meta/MetaLoaderTest.php

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,93 @@
22

33
namespace Tests\Orisai\ObjectMapper\Unit\Meta;
44

5+
use Orisai\Exceptions\Logic\InvalidArgument;
56
use stdClass;
67
use Tests\Orisai\ObjectMapper\Doubles\Dependencies\DependenciesUsingVoInjector;
78
use Tests\Orisai\ObjectMapper\Doubles\Dependencies\DependentBaseVoInjector;
89
use Tests\Orisai\ObjectMapper\Doubles\Dependencies\DependentChildVoInjector1;
910
use Tests\Orisai\ObjectMapper\Doubles\Dependencies\DependentChildVoInjector2;
11+
use Tests\Orisai\ObjectMapper\Doubles\Meta\AbstractVO;
12+
use Tests\Orisai\ObjectMapper\Doubles\Meta\EnumVO;
13+
use Tests\Orisai\ObjectMapper\Doubles\Meta\InterfaceVO;
1014
use Tests\Orisai\ObjectMapper\Toolkit\ProcessingTestCase;
1115
use const PHP_VERSION_ID;
1216

1317
final class MetaLoaderTest extends ProcessingTestCase
1418
{
1519

20+
public function testNotAClass(): void
21+
{
22+
$this->expectException(InvalidArgument::class);
23+
$this->expectExceptionMessage("Class 'foo' does not exist.");
24+
25+
/** @phpstan-ignore-next-line */
26+
$this->metaLoader->load('foo');
27+
}
28+
29+
public function testNotAMappedObject(): void
30+
{
31+
$this->expectException(InvalidArgument::class);
32+
$this->expectExceptionMessage(
33+
<<<'TXT'
34+
Context: Resolving metadata of mapped object 'stdClass'.
35+
Problem: Class does not implement interface of mapped object.
36+
Solution: Implement the 'Orisai\ObjectMapper\MappedObject' interface.
37+
TXT,
38+
);
39+
40+
/** @phpstan-ignore-next-line */
41+
$this->metaLoader->load(stdClass::class);
42+
}
43+
44+
public function testAbstractClass(): void
45+
{
46+
$this->expectException(InvalidArgument::class);
47+
$this->expectExceptionMessage(
48+
<<<'TXT'
49+
Context: Resolving metadata of mapped object
50+
'Tests\Orisai\ObjectMapper\Doubles\Meta\AbstractVO'.
51+
Problem: 'Tests\Orisai\ObjectMapper\Doubles\Meta\AbstractVO' is abstract.
52+
Solution: Load metadata only for non-abstract classes.
53+
TXT,
54+
);
55+
56+
$this->metaLoader->load(AbstractVO::class);
57+
}
58+
59+
public function testInterface(): void
60+
{
61+
$this->expectException(InvalidArgument::class);
62+
$this->expectExceptionMessage(
63+
<<<'TXT'
64+
Context: Resolving metadata of mapped object
65+
'Tests\Orisai\ObjectMapper\Doubles\Meta\InterfaceVO'.
66+
Problem: 'Tests\Orisai\ObjectMapper\Doubles\Meta\InterfaceVO' is an interface.
67+
Solution: Load metadata only for classes.
68+
TXT,
69+
);
70+
71+
$this->metaLoader->load(InterfaceVO::class);
72+
}
73+
74+
public function testEnum(): void
75+
{
76+
if (PHP_VERSION_ID < 8_01_00) {
77+
self::markTestSkipped('Enums are available on PHP 8.1+');
78+
}
79+
80+
$this->expectException(InvalidArgument::class);
81+
$this->expectExceptionMessage(
82+
<<<'TXT'
83+
Context: Resolving metadata of mapped object
84+
'Tests\Orisai\ObjectMapper\Doubles\Meta\EnumVO'.
85+
Problem: Mapped object can't be an enum.
86+
TXT,
87+
);
88+
89+
$this->metaLoader->load(EnumVO::class);
90+
}
91+
1692
/**
1793
* @runInSeparateProcess
1894
*/
@@ -33,6 +109,7 @@ public function testPreload(): void
33109
$excludes[] = __DIR__ . '/../../Doubles/Meta/ClassMetaInvalidScopeRootVO.php';
34110
$excludes[] = __DIR__ . '/../../Doubles/Meta/ClassInterfaceMetaInvalidScopeRootVO.php';
35111
$excludes[] = __DIR__ . '/../../Doubles/Meta/ClassTraitMetaInvalidScopeRootVO.php';
112+
$excludes[] = __DIR__ . '/../../Doubles/Meta/EnumVO.php';
36113
$excludes[] = __DIR__ . '/../../Doubles/Meta/FieldMetaInvalidScopeRootVO.php';
37114
$excludes[] = __DIR__ . '/../../Doubles/Meta/FieldTraitMetaInvalidScopeRootVO.php';
38115
$excludes[] = __DIR__ . '/../../Doubles/Meta/StaticMappedPropertyVO.php';

tests/Unit/Processing/DefaultProcessorTest.php

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
use DateTimeImmutable;
66
use DateTimeInterface;
7-
use Orisai\Exceptions\Logic\InvalidArgument;
87
use Orisai\Exceptions\Logic\InvalidState;
98
use Orisai\ObjectMapper\Exception\InvalidData;
109
use Orisai\ObjectMapper\MappedObject;
@@ -68,7 +67,6 @@
6867
use Tests\Orisai\ObjectMapper\Doubles\StructuresVO;
6968
use Tests\Orisai\ObjectMapper\Doubles\TransformingVO;
7069
use Tests\Orisai\ObjectMapper\Toolkit\ProcessingTestCase;
71-
use function sprintf;
7270
use const PHP_VERSION_ID;
7371

7472
final class DefaultProcessorTest extends ProcessingTestCase
@@ -1321,28 +1319,6 @@ public function testSkippedFieldAlreadyInitialized(): void
13211319
$this->processor->processSkippedFields(['whatever'], $vo);
13221320
}
13231321

1324-
public function testNotAClass(): void
1325-
{
1326-
$this->expectException(InvalidArgument::class);
1327-
$this->expectExceptionMessage("Class 'foo' does not exist");
1328-
1329-
/** @phpstan-ignore-next-line */
1330-
$this->processor->process([], 'foo');
1331-
}
1332-
1333-
public function testNotAValueObject(): void
1334-
{
1335-
$this->expectException(InvalidArgument::class);
1336-
$this->expectExceptionMessage(sprintf(
1337-
"Class '%s' should be subclass of '%s'",
1338-
stdClass::class,
1339-
MappedObject::class,
1340-
));
1341-
1342-
/** @phpstan-ignore-next-line */
1343-
$this->processor->process([], stdClass::class);
1344-
}
1345-
13461322
public function testAttributes(): void
13471323
{
13481324
if (PHP_VERSION_ID < 8_00_00) {

0 commit comments

Comments
 (0)