Skip to content

Commit ceb5634

Browse files
committed
prototype-better-error-handling
1 parent 67c8e08 commit ceb5634

4 files changed

Lines changed: 292 additions & 2 deletions

File tree

src/Error.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44

55
namespace Chubbyphp\Parsing;
66

7-
final class Error
7+
final class Error implements \JsonSerializable
88
{
99
/**
1010
* @param array<string, mixed> $variables
1111
*/
12-
public function __construct(public string $code, public string $template, public array $variables) {}
12+
public function __construct(public readonly string $code, public readonly string $template, public readonly array $variables) {}
1313

1414
public function __toString()
1515
{
@@ -25,4 +25,9 @@ public function __toString()
2525

2626
return $message;
2727
}
28+
29+
public function jsonSerialize(): array
30+
{
31+
return ['code' => $this->code, 'template' => $this->template, 'variables' => $this->variables];
32+
}
2833
}

src/ErrorWithPath.php

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 Chubbyphp\Parsing;
6+
7+
final class ErrorWithPath implements \JsonSerializable
8+
{
9+
public function __construct(public readonly Error $error, public readonly string $path) {}
10+
11+
public function jsonSerialize(): array
12+
{
13+
return ['error' => $this->error->jsonSerialize(), 'path' => $this->path];
14+
}
15+
}

src/ErrorsWithPath.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Chubbyphp\Parsing;
6+
7+
final class ErrorsWithPath implements \JsonSerializable
8+
{
9+
/**
10+
* @var array<ErrorWithPath>
11+
*/
12+
private array $errorsWithPath = [];
13+
14+
/**
15+
* @param array<Error|ErrorsWithPath|ErrorWithPath> $errors
16+
*/
17+
public function __construct(array $errors, string $path)
18+
{
19+
foreach ($errors as $error) {
20+
if ($error instanceof self) {
21+
foreach ($error->toArray() as $errorWithPath) {
22+
$this->errorsWithPath[] = new ErrorWithPath($errorWithPath->error, $this->buildPath($path, $errorWithPath->path));
23+
}
24+
} elseif ($error instanceof ErrorWithPath) {
25+
$this->errorsWithPath[] = new ErrorWithPath($error->error, $this->buildPath($path, $error->path));
26+
} else {
27+
$this->errorsWithPath[] = new ErrorWithPath($error, $path);
28+
}
29+
}
30+
}
31+
32+
public function jsonSerialize(): array
33+
{
34+
return array_map(static fn ($errorWithPath) => $errorWithPath->jsonSerialize(), $this->errorsWithPath);
35+
}
36+
37+
/**
38+
* @return array<ErrorWithPath>
39+
*/
40+
public function toArray(): array
41+
{
42+
return $this->errorsWithPath;
43+
}
44+
45+
private function buildPath(string $path, string $existingPath): string
46+
{
47+
return '' === $path ? $existingPath : $path.'.'.$existingPath;
48+
}
49+
}

tests/Unit/ErrorsWithPathTest.php

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Chubbyphp\Tests\Parsing\Unit;
6+
7+
use Chubbyphp\Parsing\Error;
8+
use Chubbyphp\Parsing\ErrorsWithPath;
9+
use Chubbyphp\Parsing\ErrorWithPath;
10+
use PHPUnit\Framework\TestCase;
11+
12+
/**
13+
* @covers \Chubbyphp\Parsing\ErrorsWithPath
14+
*
15+
* @internal
16+
*/
17+
final class ErrorsWithPathTest extends TestCase
18+
{
19+
public function testWithoutErrors(): void
20+
{
21+
$errorsWithPath = new ErrorsWithPath([], '');
22+
23+
self::assertSame([], $errorsWithPath->toArray());
24+
}
25+
26+
public function testWithErrors(): void
27+
{
28+
$errorsWithPath = new ErrorsWithPath([
29+
new ErrorsWithPath([
30+
new Error('int.type', 'Type should be "int", {{given}} given', ['given' => 'float']),
31+
new Error('string.type', 'Type should be "string", {{given}} given', ['given' => 'float']),
32+
], 'offset'),
33+
new ErrorsWithPath([
34+
new Error('int.type', 'Type should be "int", {{given}} given', ['given' => 'float']),
35+
new Error('string.type', 'Type should be "string", {{given}} given', ['given' => 'float']),
36+
], 'limit'),
37+
new ErrorsWithPath([
38+
new ErrorWithPath(
39+
new Error('literal.type', 'Type should be "bool|float|int|string", {{given}} given', ['given' => 'null']),
40+
'name'
41+
),
42+
], 'sort'),
43+
new ErrorsWithPath([
44+
new ErrorsWithPath([
45+
new ErrorWithPath(
46+
new Error('string.type', 'Type should be "string", {{given}} given', ['given' => 'float']),
47+
'id'
48+
),
49+
new ErrorWithPath(
50+
new Error('datetime.type', 'Type should be "\DateTimeInterface", {{given}} given', ['given' => 'float']),
51+
'createdAt'
52+
),
53+
new ErrorWithPath(
54+
new Error('datetime.type', 'Type should be "\DateTimeInterface", {{given}} given', ['given' => 'float']),
55+
'updatedAt'
56+
),
57+
new ErrorWithPath(
58+
new Error('string.type', 'Type should be "string", {{given}} given', ['given' => 'float']),
59+
'name'
60+
),
61+
new ErrorWithPath(
62+
new Error('string.type', 'Type should be "string", {{given}} given', ['given' => 'float']),
63+
'tag'
64+
),
65+
new ErrorsWithPath([
66+
new ErrorsWithPath([
67+
new ErrorWithPath(
68+
new Error('string.type', 'Type should be "string", {{given}} given', ['given' => 'float']),
69+
'name'
70+
),
71+
], '0'),
72+
new ErrorsWithPath([
73+
new ErrorWithPath(
74+
new Error('string.type', 'Type should be "string", {{given}} given', ['given' => 'float']),
75+
'name'
76+
),
77+
], '3'),
78+
], 'vaccinations'),
79+
], '0'),
80+
], 'items'),
81+
82+
new ErrorWithPath(
83+
new Error('literal.type', 'Type should be "bool|float|int|string", {{given}} given', ['given' => 'null']),
84+
'_type'
85+
),
86+
], '');
87+
88+
self::assertSame([
89+
[
90+
'error' => [
91+
'code' => 'int.type',
92+
'template' => 'Type should be "int", {{given}} given',
93+
'variables' => [
94+
'given' => 'float',
95+
],
96+
],
97+
'path' => 'offset',
98+
],
99+
[
100+
'error' => [
101+
'code' => 'string.type',
102+
'template' => 'Type should be "string", {{given}} given',
103+
'variables' => [
104+
'given' => 'float',
105+
],
106+
],
107+
'path' => 'offset',
108+
],
109+
[
110+
'error' => [
111+
'code' => 'int.type',
112+
'template' => 'Type should be "int", {{given}} given',
113+
'variables' => [
114+
'given' => 'float',
115+
],
116+
],
117+
'path' => 'limit',
118+
],
119+
[
120+
'error' => [
121+
'code' => 'string.type',
122+
'template' => 'Type should be "string", {{given}} given',
123+
'variables' => [
124+
'given' => 'float',
125+
],
126+
],
127+
'path' => 'limit',
128+
],
129+
[
130+
'error' => [
131+
'code' => 'literal.type',
132+
'template' => 'Type should be "bool|float|int|string", {{given}} given',
133+
'variables' => [
134+
'given' => 'null',
135+
],
136+
],
137+
'path' => 'sort.name',
138+
],
139+
[
140+
'error' => [
141+
'code' => 'string.type',
142+
'template' => 'Type should be "string", {{given}} given',
143+
'variables' => [
144+
'given' => 'float',
145+
],
146+
],
147+
'path' => 'items.0.id',
148+
],
149+
[
150+
'error' => [
151+
'code' => 'datetime.type',
152+
'template' => 'Type should be "\DateTimeInterface", {{given}} given',
153+
'variables' => [
154+
'given' => 'float',
155+
],
156+
],
157+
'path' => 'items.0.createdAt',
158+
],
159+
[
160+
'error' => [
161+
'code' => 'datetime.type',
162+
'template' => 'Type should be "\DateTimeInterface", {{given}} given',
163+
'variables' => [
164+
'given' => 'float',
165+
],
166+
],
167+
'path' => 'items.0.updatedAt',
168+
],
169+
[
170+
'error' => [
171+
'code' => 'string.type',
172+
'template' => 'Type should be "string", {{given}} given',
173+
'variables' => [
174+
'given' => 'float',
175+
],
176+
],
177+
'path' => 'items.0.name',
178+
],
179+
[
180+
'error' => [
181+
'code' => 'string.type',
182+
'template' => 'Type should be "string", {{given}} given',
183+
'variables' => [
184+
'given' => 'float',
185+
],
186+
],
187+
'path' => 'items.0.tag',
188+
],
189+
[
190+
'error' => [
191+
'code' => 'string.type',
192+
'template' => 'Type should be "string", {{given}} given',
193+
'variables' => [
194+
'given' => 'float',
195+
],
196+
],
197+
'path' => 'items.0.vaccinations.0.name',
198+
],
199+
[
200+
'error' => [
201+
'code' => 'string.type',
202+
'template' => 'Type should be "string", {{given}} given',
203+
'variables' => [
204+
'given' => 'float',
205+
],
206+
],
207+
'path' => 'items.0.vaccinations.3.name',
208+
],
209+
[
210+
'error' => [
211+
'code' => 'literal.type',
212+
'template' => 'Type should be "bool|float|int|string", {{given}} given',
213+
'variables' => [
214+
'given' => 'null',
215+
],
216+
],
217+
'path' => '_type',
218+
],
219+
], $errorsWithPath->jsonSerialize());
220+
}
221+
}

0 commit comments

Comments
 (0)