Skip to content

Commit 0e217d6

Browse files
author
Kirill Nesmeyanov
committed
Improve value exceptions formatting
1 parent 2ce017a commit 0e217d6

File tree

70 files changed

+960
-909
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+960
-909
lines changed

src/Exception/Definition/PropertyTypeNotFoundException.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,11 @@ public static function becauseTypeOfPropertyNotDefined(
6767
class: $class,
6868
property: $property,
6969
type: $type,
70-
template: 'Type "{{type}}" for property {{class}}::${{property}} is not registered',
70+
template: \sprintf(
71+
'Type "{{type}}" for property %s::$%s is not registered',
72+
$class,
73+
$property,
74+
),
7175
code: self::CODE_ERROR_PROPERTY_TYPE_NOT_DEFINED,
7276
previous: $previous,
7377
);

src/Exception/Environment/ComposerPackageRequiredException.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,11 @@ public static function becausePackageNotInstalled(
5151
?\Throwable $previous = null,
5252
): self {
5353
$template = 'The {{package}} component is required to %s. '
54-
. 'Try running "composer require {{package}}"';
54+
. 'Try running "composer require %s"';
5555

5656
return new self(
5757
package: $package,
58-
template: \sprintf($template, $purpose),
58+
template: \sprintf($template, $purpose, $package),
5959
code: self::CODE_ERROR_PACKAGE_NOT_INSTALLED,
6060
previous: $previous,
6161
);
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypeLang\Mapper\Exception\Mapping;
6+
7+
use TypeLang\Parser\Node\Name;
8+
9+
interface AllowsExplainTypeInterface extends RuntimeExceptionInterface
10+
{
11+
/**
12+
* @param \Closure(Name):(Name|null) $transform
13+
*/
14+
public function explain(callable $transform): self;
15+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypeLang\Mapper\Exception\Mapping;
6+
7+
interface FieldExceptionInterface extends RuntimeExceptionInterface
8+
{
9+
/**
10+
* Returns object field name where the error occurred.
11+
*
12+
* @return non-empty-string
13+
*/
14+
public function getField(): string;
15+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypeLang\Mapper\Exception\Mapping;
6+
7+
/**
8+
* @internal this is an internal library trait, please do not use it in your code
9+
* @psalm-internal TypeLang\Mapper\Exception\Mapping
10+
*
11+
* @phpstan-require-implements FieldExceptionInterface
12+
*
13+
* @mixin FieldExceptionInterface
14+
*/
15+
trait FieldProvider
16+
{
17+
/**
18+
* @var non-empty-string
19+
*/
20+
protected readonly string $field;
21+
22+
public function getField(): string
23+
{
24+
return $this->field;
25+
}
26+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypeLang\Mapper\Exception\Mapping;
6+
7+
use TypeLang\Mapper\Runtime\Context\LocalContext;
8+
use TypeLang\Mapper\Runtime\Path\PathInterface;
9+
use TypeLang\Parser\Node\Stmt\TypeStatement;
10+
11+
class InvalidFieldTypeValueException extends RuntimeException implements
12+
FieldExceptionInterface,
13+
ValueExceptionInterface,
14+
MappingExceptionInterface
15+
{
16+
use FieldProvider;
17+
use ValueProvider;
18+
use TypeProvider;
19+
20+
/**
21+
* @var int
22+
*/
23+
public const CODE_ERROR_INVALID_VALUE = 0x01 + parent::CODE_ERROR_LAST;
24+
25+
/**
26+
* @var int
27+
*/
28+
protected const CODE_ERROR_LAST = self::CODE_ERROR_INVALID_VALUE;
29+
30+
/**
31+
* @param non-empty-string $field
32+
*/
33+
public function __construct(
34+
protected readonly string $field,
35+
protected readonly mixed $value,
36+
protected readonly TypeStatement $expected,
37+
PathInterface $path,
38+
string $template,
39+
int $code = 0,
40+
?\Throwable $previous = null,
41+
) {
42+
parent::__construct(
43+
path: $path,
44+
template: $template,
45+
code: $code,
46+
previous: $previous,
47+
);
48+
}
49+
50+
/**
51+
* @param non-empty-string $field
52+
*/
53+
public static function createFromPath(
54+
string $field,
55+
mixed $value,
56+
TypeStatement $expected,
57+
PathInterface $path,
58+
?\Throwable $previous = null
59+
): 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+
}
74+
75+
return new self(
76+
field: $field,
77+
value: $value,
78+
expected: $expected,
79+
path: $path,
80+
template: $template,
81+
code: self::CODE_ERROR_INVALID_VALUE,
82+
previous: $previous,
83+
);
84+
}
85+
86+
/**
87+
* @param non-empty-string $field
88+
*/
89+
public static function createFromContext(
90+
string $field,
91+
mixed $value,
92+
TypeStatement $expected,
93+
LocalContext $context,
94+
?\Throwable $previous = null,
95+
): self {
96+
return self::createFromPath(
97+
field: $field,
98+
value: $value,
99+
expected: $expected,
100+
path: clone $context->getPath(),
101+
previous: $previous,
102+
);
103+
}
104+
}

src/Exception/Mapping/InvalidValueException.php

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
namespace TypeLang\Mapper\Exception\Mapping;
66

77
use TypeLang\Mapper\Runtime\Context\LocalContext;
8-
use TypeLang\Parser\Node\Stmt\TypeStatement;
8+
use TypeLang\Mapper\Runtime\Path\PathInterface;
99

10-
class InvalidValueException extends ValueMappingException
10+
class InvalidValueException extends RuntimeException implements ValueExceptionInterface
1111
{
12+
use ValueProvider;
13+
1214
/**
1315
* @var int
1416
*/
@@ -19,34 +21,46 @@ class InvalidValueException extends ValueMappingException
1921
*/
2022
protected const CODE_ERROR_LAST = self::CODE_ERROR_INVALID_VALUE;
2123

22-
/**
23-
* @param TypeStatement|non-empty-string $expected
24-
*/
25-
public static function becauseInvalidValueGiven(
24+
public function __construct(
25+
protected readonly mixed $value,
26+
PathInterface $path,
27+
string $template,
28+
int $code = 0,
29+
?\Throwable $previous = null,
30+
) {
31+
parent::__construct(
32+
path: $path,
33+
template: $template,
34+
code: $code,
35+
previous: $previous,
36+
);
37+
}
38+
39+
public static function createFromPath(
2640
mixed $value,
27-
TypeStatement|string $expected,
28-
LocalContext $context,
41+
PathInterface $path,
2942
?\Throwable $previous = null,
3043
): self {
31-
$template = 'Passed value must be of type {{expected}}, but {{actual}} given';
32-
33-
if (\is_scalar($value)) {
34-
$template = \str_replace('{{actual}}', '{{actual}} ("{{value}}")', $template);
35-
}
36-
37-
$path = $context->getPath();
38-
39-
if (!$path->isEmpty()) {
40-
$template .= ' at {{path}}';
41-
}
44+
$template = 'Passed value {{value}} is invalid';
4245

4346
return new self(
4447
value: $value,
45-
expected: $expected,
4648
path: $path,
4749
template: $template,
4850
code: self::CODE_ERROR_INVALID_VALUE,
4951
previous: $previous,
5052
);
5153
}
54+
55+
public static function createFromContext(
56+
mixed $value,
57+
LocalContext $context,
58+
?\Throwable $previous = null,
59+
): self {
60+
return self::createFromPath(
61+
value: $value,
62+
path: clone $context->getPath(),
63+
previous: $previous,
64+
);
65+
}
5266
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypeLang\Mapper\Exception\Mapping;
6+
7+
use TypeLang\Mapper\Runtime\Context\LocalContext;
8+
use TypeLang\Mapper\Runtime\Path\PathInterface;
9+
use TypeLang\Parser\Node\Stmt\TypeStatement;
10+
11+
class InvalidValueMappingException extends RuntimeException implements
12+
ValueExceptionInterface,
13+
MappingExceptionInterface
14+
{
15+
use ValueProvider;
16+
use TypeProvider;
17+
18+
/**
19+
* @var int
20+
*/
21+
public const CODE_ERROR_INVALID_VALUE = 0x01 + parent::CODE_ERROR_LAST;
22+
23+
/**
24+
* @var int
25+
*/
26+
protected const CODE_ERROR_LAST = self::CODE_ERROR_INVALID_VALUE;
27+
28+
public function __construct(
29+
protected readonly mixed $value,
30+
protected readonly TypeStatement $expected,
31+
PathInterface $path,
32+
string $template,
33+
int $code = 0,
34+
?\Throwable $previous = null,
35+
) {
36+
parent::__construct(
37+
path: $path,
38+
template: $template,
39+
code: $code,
40+
previous: $previous,
41+
);
42+
}
43+
44+
public static function createFromPath(
45+
mixed $value,
46+
TypeStatement $expected,
47+
PathInterface $path,
48+
?\Throwable $prev = null
49+
): self {
50+
$template = 'Passed value must be of type {{expected}}, but {{value}} given';
51+
52+
return new self(
53+
value: $value,
54+
expected: $expected,
55+
path: $path,
56+
template: $template,
57+
code: self::CODE_ERROR_INVALID_VALUE,
58+
previous: $prev,
59+
);
60+
}
61+
62+
public static function createFromContext(
63+
mixed $value,
64+
TypeStatement $expected,
65+
LocalContext $context,
66+
?\Throwable $previous = null,
67+
): self {
68+
return self::createFromPath(
69+
value: $value,
70+
expected: $expected,
71+
path: clone $context->getPath(),
72+
prev: $previous,
73+
);
74+
}
75+
}

0 commit comments

Comments
 (0)