Skip to content

Commit 5027ef3

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

4 files changed

Lines changed: 538 additions & 2 deletions

File tree

src/Error.php

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

55
namespace Chubbyphp\Parsing;
66

7-
final class Error
7+
/**
8+
* @phpstan-type ErrorAsJson array{code: string, template: string, variables: array<string, mixed>}
9+
*/
10+
final class Error implements \JsonSerializable
811
{
912
/**
1013
* @param array<string, mixed> $variables
1114
*/
12-
public function __construct(public string $code, public string $template, public array $variables) {}
15+
public function __construct(public readonly string $code, public readonly string $template, public readonly array $variables) {}
1316

1417
public function __toString()
1518
{
@@ -25,4 +28,12 @@ public function __toString()
2528

2629
return $message;
2730
}
31+
32+
/**
33+
* @return ErrorAsJson
34+
*/
35+
public function jsonSerialize(): array
36+
{
37+
return ['code' => $this->code, 'template' => $this->template, 'variables' => $this->variables];
38+
}
2839
}

src/ErrorWithPath.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Chubbyphp\Parsing;
6+
7+
/**
8+
* @phpstan-type ErrorAsJson array{code: string, template: string, variables: array<string, mixed>}
9+
* @phpstan-type ErrorWithPathJson array{error: ErrorAsJson, path: string}
10+
*/
11+
final class ErrorWithPath implements \JsonSerializable
12+
{
13+
public function __construct(public readonly Error $error, public readonly string $path) {}
14+
15+
/**
16+
* @return ErrorWithPathJson
17+
*/
18+
public function jsonSerialize(): array
19+
{
20+
return ['error' => $this->error->jsonSerialize(), 'path' => $this->path];
21+
}
22+
}

src/ErrorsWithPath.php

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Chubbyphp\Parsing;
6+
7+
/**
8+
* @phpstan-type ErrorAsJson array{code: string, template: string, variables: array<string, mixed>}
9+
* @phpstan-type ErrorWithPathJson array{error: ErrorAsJson, path: string}
10+
* @phpstan-type ErrorsWithPathJson array<ErrorWithPathJson>
11+
* @phpstan-type ApiProblem array{name: string, reason: string, details: non-empty-array<string, mixed>}
12+
*/
13+
final class ErrorsWithPath implements \JsonSerializable
14+
{
15+
/**
16+
* @var array<ErrorWithPath>
17+
*/
18+
private array $errorsWithPath = [];
19+
20+
/**
21+
* @param array<Error|ErrorWithPath|self> $errors
22+
*/
23+
public function __construct(array $errors, private string $path = '')
24+
{
25+
foreach ($errors as $error) {
26+
$this->addError($error);
27+
}
28+
}
29+
30+
public function addError(Error|ErrorWithPath|self $error): self
31+
{
32+
if ($error instanceof self) {
33+
foreach ($error->errorsWithPath as $errorWithPath) {
34+
$this->errorsWithPath[] = new ErrorWithPath($errorWithPath->error, $this->mergePath($this->path, $errorWithPath->path));
35+
}
36+
} elseif ($error instanceof ErrorWithPath) {
37+
$this->errorsWithPath[] = new ErrorWithPath($error->error, $this->mergePath($this->path, $error->path));
38+
} else {
39+
$this->errorsWithPath[] = new ErrorWithPath($error, $this->path);
40+
}
41+
42+
return $this;
43+
}
44+
45+
/**
46+
* @return ErrorsWithPathJson
47+
*/
48+
public function jsonSerialize(): array
49+
{
50+
return array_map(static fn ($errorWithPath) => $errorWithPath->jsonSerialize(), $this->errorsWithPath);
51+
}
52+
53+
/**
54+
* @return array<string, mixed>
55+
*/
56+
public function toTree(): array
57+
{
58+
// @var array<string, mixed> $tree
59+
return array_reduce(
60+
$this->errorsWithPath,
61+
static function (array $tree, $errorWithPath): array {
62+
$pathParts = explode('.', $errorWithPath->path);
63+
64+
$current = &$tree;
65+
$lastIndex = \count($pathParts) - 1;
66+
67+
foreach ($pathParts as $i => $pathPart) {
68+
if ($i < $lastIndex) {
69+
$current[$pathPart] ??= [];
70+
$current = &$current[$pathPart];
71+
72+
continue;
73+
}
74+
75+
$current[$pathPart] = array_merge($current[$pathPart] ?? [], [(string) $errorWithPath->error]);
76+
}
77+
78+
return $tree;
79+
},
80+
[]
81+
);
82+
}
83+
84+
/**
85+
* @return array<ApiProblem>
86+
*/
87+
public function toApiProblem(): array
88+
{
89+
return array_map(
90+
fn (ErrorWithPath $errorWithPath) => [
91+
'name' => $this->pathToName($errorWithPath->path),
92+
'reason' => (string) $errorWithPath->error,
93+
'details' => [
94+
'_template' => $errorWithPath->error->template,
95+
...$errorWithPath->error->variables,
96+
],
97+
],
98+
$this->errorsWithPath
99+
);
100+
}
101+
102+
private function mergePath(string $path, string $existingPath): string
103+
{
104+
return '' === $path ? $existingPath : $path.'.'.$existingPath;
105+
}
106+
107+
private function pathToName(string $path): string
108+
{
109+
$pathParts = explode('.', $path);
110+
111+
return implode('', array_map(
112+
static fn (string $pathPart, $i) => 0 === $i ? $pathPart : '['.$pathPart.']',
113+
$pathParts,
114+
array_keys($pathParts)
115+
));
116+
}
117+
}

0 commit comments

Comments
 (0)