Skip to content

Commit

Permalink
MetaLoader: improve error messages, add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
mabar committed Jul 9, 2023
1 parent 7efdb71 commit 03f34ac
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 38 deletions.
56 changes: 43 additions & 13 deletions src/Meta/MetaLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -22,6 +24,7 @@
use function interface_exists;
use function is_subclass_of;
use function trait_exists;
use const PHP_VERSION_ID;

final class MetaLoader
{
Expand All @@ -45,6 +48,9 @@ public function __construct(
$this->resolverFactory = $resolverFactory;
}

/**
* @param class-string<MappedObject> $class
*/
public function load(string $class): RuntimeMeta
{
return $this->metaCache->load($class)
Expand All @@ -70,32 +76,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 (PHP_VERSION_ID >= 8_01_00 && $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;
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/Rules/MappedObjectRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down
3 changes: 2 additions & 1 deletion tests/Doubles/InternalClassExtendingVO.php
Original file line number Diff line number Diff line change
Expand Up @@ -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() */
Expand Down
13 changes: 13 additions & 0 deletions tests/Doubles/Meta/AbstractVO.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php declare(strict_types = 1);

namespace Tests\Orisai\ObjectMapper\Doubles\Meta;

use Orisai\ObjectMapper\MappedObject;

/**
* phpcs:disable SlevomatCodingStandard.Classes.SuperfluousAbstractClassNaming
*/
abstract class AbstractVO implements MappedObject
{

}
10 changes: 10 additions & 0 deletions tests/Doubles/Meta/EnumVO.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php declare(strict_types = 1);

namespace Tests\Orisai\ObjectMapper\Doubles\Meta;

use Orisai\ObjectMapper\MappedObject;

enum EnumVO: string implements MappedObject
{

}
13 changes: 13 additions & 0 deletions tests/Doubles/Meta/InterfaceVO.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php declare(strict_types = 1);

namespace Tests\Orisai\ObjectMapper\Doubles\Meta;

use Orisai\ObjectMapper\MappedObject;

/**
* phpcs:disable SlevomatCodingStandard.Classes.SuperfluousInterfaceNaming
*/
interface InterfaceVO extends MappedObject
{

}
77 changes: 77 additions & 0 deletions tests/Unit/Meta/MetaLoaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,93 @@

namespace Tests\Orisai\ObjectMapper\Unit\Meta;

use Orisai\Exceptions\Logic\InvalidArgument;
use stdClass;
use Tests\Orisai\ObjectMapper\Doubles\Dependencies\DependenciesUsingVoInjector;
use Tests\Orisai\ObjectMapper\Doubles\Dependencies\DependentBaseVoInjector;
use Tests\Orisai\ObjectMapper\Doubles\Dependencies\DependentChildVoInjector1;
use Tests\Orisai\ObjectMapper\Doubles\Dependencies\DependentChildVoInjector2;
use Tests\Orisai\ObjectMapper\Doubles\Meta\AbstractVO;
use Tests\Orisai\ObjectMapper\Doubles\Meta\EnumVO;
use Tests\Orisai\ObjectMapper\Doubles\Meta\InterfaceVO;
use Tests\Orisai\ObjectMapper\Toolkit\ProcessingTestCase;
use const PHP_VERSION_ID;

final class MetaLoaderTest extends ProcessingTestCase
{

public function testNotAClass(): void
{
$this->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
*/
Expand All @@ -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';
Expand Down
24 changes: 0 additions & 24 deletions tests/Unit/Processing/DefaultProcessorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit 03f34ac

Please sign in to comment.