Skip to content

Commit 488793e

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

4 files changed

Lines changed: 288 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: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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, private string $path)
18+
{
19+
foreach ($errors as $error) {
20+
$this->addError($error);
21+
}
22+
}
23+
24+
public function addError(Error|ErrorWithPath|self $error): self
25+
{
26+
if ($error instanceof self) {
27+
foreach ($error->errorsWithPath as $errorWithPath) {
28+
$this->errorsWithPath[] = new ErrorWithPath($errorWithPath->error, $this->buildPath($this->path, $errorWithPath->path));
29+
}
30+
} elseif ($error instanceof ErrorWithPath) {
31+
$this->errorsWithPath[] = new ErrorWithPath($error->error, $this->buildPath($this->path, $error->path));
32+
} else {
33+
$this->errorsWithPath[] = new ErrorWithPath($error, $this->path);
34+
}
35+
36+
return $this;
37+
}
38+
39+
public function jsonSerialize(): array
40+
{
41+
return array_map(static fn ($errorWithPath) => $errorWithPath->jsonSerialize(), $this->errorsWithPath);
42+
}
43+
44+
private function buildPath(string $path, string $existingPath): string
45+
{
46+
return '' === $path ? $existingPath : $path.'.'.$existingPath;
47+
}
48+
}

tests/Unit/ErrorsWithPathTest.php

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

0 commit comments

Comments
 (0)