Skip to content

Commit ba9b360

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

4 files changed

Lines changed: 568 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: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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 string $path, public readonly Error $error) {}
14+
15+
public function __toString()
16+
{
17+
return $this->path.': '.(string) $this->error;
18+
}
19+
20+
/**
21+
* @return ErrorWithPathJson
22+
*/
23+
public function jsonSerialize(): array
24+
{
25+
return ['path' => $this->path, 'error' => $this->error->jsonSerialize()];
26+
}
27+
}

src/ErrorsWithPath.php

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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+
public function __construct(private string $path = '') {}
21+
22+
public function __toString()
23+
{
24+
return implode(PHP_EOL, array_map(static fn (ErrorWithPath $errorWithPath) => (string) $errorWithPath, $this->errorsWithPath));
25+
}
26+
27+
public function addErrorsWithPath(self $errorsWithPath): self
28+
{
29+
foreach ($errorsWithPath->errorsWithPath as $errorWithPath) {
30+
$this->addErrorWithPath($errorWithPath);
31+
}
32+
33+
return $this;
34+
}
35+
36+
public function addError(Error $error): self
37+
{
38+
$this->errorsWithPath[] = new ErrorWithPath($this->path, $error);
39+
40+
return $this;
41+
}
42+
43+
/**
44+
* @return array<string, mixed>
45+
*/
46+
public function toTree(): array
47+
{
48+
// @var array<string, mixed> $tree
49+
return array_reduce(
50+
$this->errorsWithPath,
51+
static function (array $tree, $errorWithPath): array {
52+
$pathParts = explode('.', $errorWithPath->path);
53+
54+
$current = &$tree;
55+
$lastIndex = \count($pathParts) - 1;
56+
57+
foreach ($pathParts as $i => $pathPart) {
58+
if ($i < $lastIndex) {
59+
$current[$pathPart] ??= [];
60+
$current = &$current[$pathPart];
61+
62+
continue;
63+
}
64+
65+
$current[$pathPart] = array_merge($current[$pathPart] ?? [], [(string) $errorWithPath->error]);
66+
}
67+
68+
return $tree;
69+
},
70+
[]
71+
);
72+
}
73+
74+
/**
75+
* @return array<ApiProblem>
76+
*/
77+
public function toApiProblems(): array
78+
{
79+
return array_map(
80+
fn (ErrorWithPath $errorWithPath) => [
81+
'name' => $this->pathToName($errorWithPath->path),
82+
'reason' => (string) $errorWithPath->error,
83+
'details' => [
84+
'_template' => $errorWithPath->error->template,
85+
...$errorWithPath->error->variables,
86+
],
87+
],
88+
$this->errorsWithPath
89+
);
90+
}
91+
92+
/**
93+
* @return ErrorsWithPathJson
94+
*/
95+
public function jsonSerialize(): array
96+
{
97+
return array_map(static fn (ErrorWithPath $errorWithPath) => $errorWithPath->jsonSerialize(), $this->errorsWithPath);
98+
}
99+
100+
private function addErrorWithPath(ErrorWithPath $errorWithPath): self
101+
{
102+
$this->errorsWithPath[] = new ErrorWithPath($this->mergePath($this->path, $errorWithPath->path), $errorWithPath->error);
103+
104+
return $this;
105+
}
106+
107+
private function mergePath(string $path, string $existingPath): string
108+
{
109+
return '' === $path ? $existingPath : $path.'.'.$existingPath;
110+
}
111+
112+
private function pathToName(string $path): string
113+
{
114+
$pathParts = explode('.', $path);
115+
116+
return implode('', array_map(
117+
static fn (string $pathPart, $i) => 0 === $i ? $pathPart : '['.$pathPart.']',
118+
$pathParts,
119+
array_keys($pathParts)
120+
));
121+
}
122+
}

0 commit comments

Comments
 (0)