Skip to content

Commit 0e2ad62

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

4 files changed

Lines changed: 512 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: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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+
public function toTree(): array
54+
{
55+
$tree = [];
56+
57+
foreach ($this->errorsWithPath as $errorWithPath) {
58+
$current = &$tree;
59+
$pathParts = explode('.', $errorWithPath->path);
60+
$pathPartsCount = \count($pathParts);
61+
62+
foreach ($pathParts as $i => $pathPart) {
63+
if ($i + 1 < $pathPartsCount) {
64+
$current[$pathPart] ??= [];
65+
$current = &$current[$pathPart];
66+
} else {
67+
$current[$pathPart] = array_merge($current[$pathPart] ?? [], [(string) $errorWithPath->error]);
68+
}
69+
}
70+
}
71+
72+
return $tree;
73+
}
74+
75+
public function toApiProblem(): array
76+
{
77+
return array_map(static fn ($errorWithPath) => [
78+
'name' => $errorWithPath->path,
79+
'reason' => (string) $errorWithPath->error,
80+
'details' => [
81+
'_template' => $errorWithPath->error->template,
82+
...$errorWithPath->error->variables,
83+
],
84+
], $this->errorsWithPath);
85+
}
86+
87+
private function buildPath(string $path, string $existingPath): string
88+
{
89+
return '' === $path ? $existingPath : $path.'.'.$existingPath;
90+
}
91+
}

0 commit comments

Comments
 (0)