Skip to content

Commit 782fa1b

Browse files
author
Kirill Nesmeyanov
committed
Improve field errors
1 parent c26d43c commit 782fa1b

File tree

9 files changed

+54
-90
lines changed

9 files changed

+54
-90
lines changed

src/Exception/Mapping/InvalidFieldTypeValueException.php

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
use TypeLang\Mapper\Runtime\Context\LocalContext;
88
use TypeLang\Mapper\Runtime\Path\PathInterface;
99
use TypeLang\Parser\Node\Stmt\TypeStatement;
10+
use TypeLang\Parser\Traverser;
11+
use TypeLang\Parser\Traverser\TypeMapVisitor;
1012

1113
class InvalidFieldTypeValueException extends RuntimeException implements
1214
FieldExceptionInterface,
@@ -15,7 +17,9 @@ class InvalidFieldTypeValueException extends RuntimeException implements
1517
{
1618
use FieldProvider;
1719
use ValueProvider;
18-
use TypeProvider;
20+
use TypeProvider {
21+
TypeProvider::explain as private explainExpectedType;
22+
}
1923

2024
/**
2125
* @var int
@@ -34,6 +38,7 @@ public function __construct(
3438
protected readonly string $field,
3539
protected readonly mixed $value,
3640
protected readonly TypeStatement $expected,
41+
protected readonly TypeStatement $object,
3742
PathInterface $path,
3843
string $template,
3944
int $code = 0,
@@ -54,28 +59,17 @@ public static function createFromPath(
5459
string $field,
5560
mixed $value,
5661
TypeStatement $expected,
62+
TypeStatement $object,
5763
PathInterface $path,
5864
?\Throwable $previous = null
5965
): self {
60-
$template = 'Passed value of field {{field}} must be of type {{expected}}, but {{value}} given';
61-
62-
if ($previous instanceof FieldExceptionInterface) {
63-
$field = $previous->getField();
64-
$path = $previous->getPath();
65-
66-
if ($previous instanceof ValueExceptionInterface) {
67-
$value = $previous->getValue();
68-
}
69-
70-
if ($previous instanceof MappingExceptionInterface) {
71-
$expected = $previous->getExpectedType();
72-
}
73-
}
66+
$template = 'Passed value in {{field}} of {{object}} must be of type {{expected}}, but {{value}} given';
7467

7568
return new self(
7669
field: $field,
7770
value: $value,
7871
expected: $expected,
72+
object: $object,
7973
path: $path,
8074
template: $template,
8175
code: self::CODE_ERROR_INVALID_VALUE,
@@ -90,15 +84,29 @@ public static function createFromContext(
9084
string $field,
9185
mixed $value,
9286
TypeStatement $expected,
87+
TypeStatement $object,
9388
LocalContext $context,
9489
?\Throwable $previous = null,
9590
): self {
9691
return self::createFromPath(
9792
field: $field,
9893
value: $value,
9994
expected: $expected,
95+
object: $object,
10096
path: clone $context->getPath(),
10197
previous: $previous,
10298
);
10399
}
100+
101+
public function explain(callable $transform): self
102+
{
103+
$this->explainExpectedType($transform);
104+
105+
Traverser::through(
106+
visitor: new TypeMapVisitor($transform(...)),
107+
nodes: [$this->object],
108+
);
109+
110+
return $this;
111+
}
104112
}

src/Exception/Mapping/MissingFieldValueException.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public static function createFromPath(
5353
PathInterface $path,
5454
?\Throwable $previous = null,
5555
): self {
56-
$template = 'Object of type {{expected}} requires field {{field}}';
56+
$template = 'Object of type {{expected}} requires missing field {{field}}';
5757

5858
return new self(
5959
expected: $expected,

src/Mapping/Driver/DocBlockDriver.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
use TypeLang\PHPDoc\Standard\VarTagFactory;
2020
use TypeLang\PHPDoc\Tag\Factory\TagFactory;
2121

22+
/**
23+
* Note: This driver requires installed "type-lang/phpdoc" and
24+
* "type-lang/phpdoc-standard-tags" Composer packages.
25+
*/
2226
final class DocBlockDriver extends LoadableDriver
2327
{
2428
/**

src/Mapping/Driver/ReflectionDriver.php

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ protected function load(\ReflectionClass $reflection, ClassMetadata $class, Repo
3232
$metadata = $class->getPropertyOrCreate($property->getName());
3333

3434
$this->fillType($property, $metadata, $types);
35-
$this->fillReadonlyModifier($property, $metadata);
3635
$this->fillDefaultValue($property, $metadata);
3736
}
3837
}
@@ -48,15 +47,6 @@ private function fillDefaultValue(\ReflectionProperty $property, PropertyMetadat
4847
$meta->setDefaultValue($default);
4948
}
5049

51-
private function fillReadonlyModifier(\ReflectionProperty $property, PropertyMetadata $meta): void
52-
{
53-
if (!$property->isReadOnly()) {
54-
return;
55-
}
56-
57-
$meta->markAsReadonly();
58-
}
59-
6050
/**
6151
* @throws PropertyTypeNotFoundException
6252
* @throws \InvalidArgumentException

src/Mapping/Metadata/PropertyMetadata.php

Lines changed: 12 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
namespace TypeLang\Mapper\Mapping\Metadata;
66

77
use TypeLang\Mapper\Runtime\Context\LocalContext;
8-
use TypeLang\Mapper\Type\TypeInterface;
8+
use TypeLang\Parser\Node\Stmt\NamedTypeNode;
99
use TypeLang\Parser\Node\Stmt\Shape\NamedFieldNode;
1010
use TypeLang\Parser\Node\Stmt\TypeStatement;
1111

@@ -20,8 +20,6 @@ final class PropertyMetadata extends Metadata
2020

2121
private bool $hasDefaultValue = false;
2222

23-
private bool $readonly = false;
24-
2523
/**
2624
* @param non-empty-string $export
2725
*/
@@ -44,7 +42,17 @@ public function getTypeStatement(LocalContext $context): ?TypeStatement
4442
{
4543
$info = $this->findTypeInfo();
4644

47-
return $info?->getTypeStatement();
45+
if ($info === null) {
46+
return null;
47+
}
48+
49+
$statement = clone $info->getTypeStatement();
50+
51+
if ($context->isDetailedTypes() || !$statement instanceof NamedTypeNode) {
52+
return $statement;
53+
}
54+
55+
return new NamedTypeNode($statement->name);
4856
}
4957

5058
/**
@@ -131,19 +139,6 @@ public function hasTypeInfo(): bool
131139
return $this->type !== null;
132140
}
133141

134-
/**
135-
* Note: The prefix "find" is used to indicate that the {@see TypeInterface}
136-
* definition may be optional and method may return {@see null}.
137-
* The prefix "get" is used when the value is forced to be obtained
138-
* and should throw an exception if the type definition is missing.
139-
*
140-
* @api
141-
*/
142-
public function findType(): ?TypeInterface
143-
{
144-
return $this->type?->getType();
145-
}
146-
147142
/**
148143
* Note: The prefix "find" is used to indicate that the {@see TypeMetadata}
149144
* definition may be optional and method may return {@see null}.
@@ -195,20 +190,4 @@ public function hasDefaultValue(): bool
195190
{
196191
return $this->hasDefaultValue;
197192
}
198-
199-
/**
200-
* @api
201-
*/
202-
public function markAsReadonly(bool $readonly = true): void
203-
{
204-
$this->readonly = $readonly;
205-
}
206-
207-
/**
208-
* @api
209-
*/
210-
public function isReadonly(): bool
211-
{
212-
return $this->readonly;
213-
}
214193
}

src/Type/ObjectType.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ protected function normalizeObject(object $object, LocalContext $context): array
105105
field: $meta->getExportName(),
106106
value: $fieldValue,
107107
expected: $info->getTypeStatement(),
108+
object: $this->metadata->getTypeStatement($context),
108109
context: $context,
109110
previous: $e,
110111
);
@@ -199,6 +200,7 @@ private function denormalizeObject(array $value, object $object, LocalContext $c
199200
field: $meta->getExportName(),
200201
value: $fieldValue,
201202
expected: $info->getTypeStatement(),
203+
object: $this->metadata->getTypeStatement($context),
202204
context: $context,
203205
previous: $e,
204206
);

src/Type/ObjectType/PropertyAccessor/ReflectionPropertyAccessor.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,6 @@ public function setValue(object $object, PropertyMetadata $meta, mixed $value):
4646

4747
public function isWritable(object $object, PropertyMetadata $meta): bool
4848
{
49-
if ($meta->isReadonly()) {
50-
return false;
51-
}
52-
5349
try {
5450
$property = $this->getProperty($object, $meta);
5551

tests/Unit/Mapping/Metadata/PropertyTest.php

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -58,51 +58,36 @@ public function testDefaultValue(): void
5858
self::assertFalse($property->hasDefaultValue());
5959
}
6060

61-
public function testReadonlyModifier(): void
62-
{
63-
$property = new PropertyMetadata('foo');
64-
65-
self::assertFalse($property->isReadonly());
66-
67-
$property->markAsReadonly();
68-
69-
self::assertTrue($property->isReadonly());
70-
71-
$property->markAsReadonly(false);
72-
73-
self::assertFalse($property->isReadonly());
74-
75-
$property->markAsReadonly(true);
76-
77-
self::assertTrue($property->isReadonly());
78-
}
79-
8061
public function testType(): void
8162
{
8263
$property = new PropertyMetadata('foo');
8364

84-
self::assertNull($property->findType());
65+
self::assertNull($property->findTypeInfo());
8566
self::assertFalse($property->hasTypeInfo());
8667

8768
$property = new PropertyMetadata('foo', new TypeMetadata(
8869
type: $type = new NullType(),
89-
statement: new NullLiteralNode(),
70+
statement: $statement = new NullLiteralNode(),
9071
));
9172

92-
self::assertSame($type, $property->findType());
73+
self::assertNotNull($info = $property->findTypeInfo());
74+
self::assertSame($type, $info->getType());
75+
self::assertSame($statement, $info->getTypeStatement());
9376
self::assertTrue($property->hasTypeInfo());
9477

9578
$property->setTypeInfo(new TypeMetadata(
9679
type: new NullType(),
9780
statement: new NullLiteralNode()
9881
));
9982

100-
self::assertNotSame($type, $property->findType());
83+
self::assertNotNull($info = $property->findTypeInfo());
84+
self::assertNotSame($type, $info->getType());
85+
self::assertNotSame($statement, $info->getTypeStatement());
10186
self::assertTrue($property->hasTypeInfo());
10287

10388
$property->removeTypeInfo();
10489

105-
self::assertNull($property->findType());
90+
self::assertNull($property->findTypeInfo());
10691
self::assertFalse($property->hasTypeInfo());
10792
}
10893
}

tests/Unit/Type/TypeTestCase.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use PHPUnit\Framework\Attributes\DataProvider;
99
use PHPUnit\Framework\Attributes\Group;
1010
use TypeLang\Mapper\Exception\Definition\TypeNotFoundException;
11-
use TypeLang\Mapper\Exception\Mapping\MappingException;
11+
use TypeLang\Mapper\Exception\Mapping\RuntimeException;
1212
use TypeLang\Mapper\Runtime\Context\Context;
1313
use TypeLang\Mapper\Runtime\Context\Direction;
1414
use TypeLang\Mapper\Runtime\Context\LocalContext;
@@ -48,14 +48,14 @@ protected function getDenormalizationExpectation(mixed $value, ValueType $type,
4848

4949
protected function expectCastIfNonStrict(mixed $expected, Context $ctx): mixed
5050
{
51-
$this->expectException(MappingException::class);
51+
$this->expectException(RuntimeException::class);
5252

5353
return "<\0MUST_THROW_ERROR(" . __FUNCTION__ . ")\0>";
5454
}
5555

5656
protected function expectMappingError(): mixed
5757
{
58-
$this->expectException(MappingException::class);
58+
$this->expectException(RuntimeException::class);
5959

6060
return "<\0MUST_THROW_ERROR(" . __FUNCTION__ . ")\0>";
6161
}

0 commit comments

Comments
 (0)