Skip to content

Commit ed9f0df

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

4 files changed

Lines changed: 531 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: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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->buildPath($this->path, $errorWithPath->path));
35+
}
36+
} elseif ($error instanceof ErrorWithPath) {
37+
$this->errorsWithPath[] = new ErrorWithPath($error->error, $this->buildPath($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+
$tree = [];
60+
61+
foreach ($this->errorsWithPath as $errorWithPath) {
62+
$current = &$tree;
63+
$pathParts = explode('.', $errorWithPath->path);
64+
$lastKey = array_key_last($pathParts);
65+
66+
foreach ($pathParts as $key => $pathPart) {
67+
if ($key !== $lastKey) {
68+
$current[$pathPart] ??= [];
69+
$current = &$current[$pathPart];
70+
71+
continue;
72+
}
73+
74+
$current[$pathPart] = array_merge($current[$pathPart] ?? [], [(string) $errorWithPath->error]);
75+
}
76+
}
77+
78+
return $tree;
79+
}
80+
81+
/**
82+
* @return array<ApiProblem>
83+
*/
84+
public function toApiProblem(): array
85+
{
86+
return array_map(static function (ErrorWithPath $errorWithPath) {
87+
$pathParts = explode('.', $errorWithPath->path);
88+
$nameParts = array_map(
89+
static fn (string $pathPart, $i) => 0 === $i ? $pathPart : '['.$pathPart.']',
90+
$pathParts,
91+
array_keys($pathParts)
92+
);
93+
$name = implode('', $nameParts);
94+
95+
return [
96+
'name' => $name,
97+
'reason' => (string) $errorWithPath->error,
98+
'details' => [
99+
'_template' => $errorWithPath->error->template,
100+
...$errorWithPath->error->variables,
101+
],
102+
];
103+
}, $this->errorsWithPath);
104+
}
105+
106+
private function buildPath(string $path, string $existingPath): string
107+
{
108+
return '' === $path ? $existingPath : $path.'.'.$existingPath;
109+
}
110+
}

0 commit comments

Comments
 (0)