Skip to content

Commit 554f84d

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

48 files changed

Lines changed: 1894 additions & 1684 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Heavily inspired by the well-known TypeScript library [zod](https://github.com/c
3535
Through [Composer](http://getcomposer.org) as [chubbyphp/chubbyphp-parsing][1].
3636

3737
```sh
38-
composer require chubbyphp/chubbyphp-parsing "^1.4"
38+
composer require chubbyphp/chubbyphp-parsing "^2.0"
3939
```
4040

4141
## Usage
@@ -51,7 +51,7 @@ $schema->preParse(static fn ($input) => $input);
5151
$schema->postParse(static fn (string $output) => $output);
5252
$schema->parse('test');
5353
$schema->safeParse('test');
54-
$schema->catch(static fn (string $output, ParserErrorException $e) => $output);
54+
$schema->catch(static fn (string $output, ErrorsException $e) => $output);
5555
```
5656

5757
### array

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
},
5151
"extra": {
5252
"branch-alias": {
53-
"dev-master": "1.4-dev"
53+
"dev-master": "2.0-dev"
5454
}
5555
},
5656
"scripts": {

phpstan.neon

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
parameters:
22
ignoreErrors:
3-
-
4-
message: '/type specified in iterable type array/'
5-
path: %currentWorkingDirectory%/src/ParserErrorException.php
63
-
74
message: '/Instanceof between Chubbyphp\\Parsing\\Schema\\ObjectSchemaInterface and Chubbyphp\\Parsing\\Schema\\ObjectSchemaInterface will always evaluate to true./'
85
path: %currentWorkingDirectory%/src/Schema/DiscriminatedUnionSchema.php

src/Error.php

Lines changed: 17 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,16 @@ public function __toString()
2528

2629
return $message;
2730
}
31+
32+
/**
33+
* @return ErrorAsJson
34+
*/
35+
public function jsonSerialize(): array
36+
{
37+
return [
38+
'code' => $this->code,
39+
'template' => $this->template,
40+
'variables' => json_decode(json_encode($this->variables, JSON_THROW_ON_ERROR), true),
41+
];
42+
}
2843
}

src/Errors.php

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

src/ErrorsException.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Chubbyphp\Parsing;
6+
7+
final class ErrorsException extends \RuntimeException
8+
{
9+
private function __construct(public readonly Errors $errors)
10+
{
11+
parent::__construct((string) $errors);
12+
}
13+
14+
public static function fromErrors(Errors $errors): self
15+
{
16+
return new self($errors);
17+
}
18+
19+
public static function fromError(Error $error): self
20+
{
21+
return new self((new Errors())->add($error));
22+
}
23+
}

src/ParserErrorException.php

Lines changed: 0 additions & 116 deletions
This file was deleted.

src/ParserErrorExceptionToString.php

Lines changed: 0 additions & 25 deletions
This file was deleted.

src/Result.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ final class Result
88
{
99
public bool $success;
1010

11-
public function __construct(public mixed $data, public ?ParserErrorException $exception)
11+
public function __construct(public mixed $data, public ?ErrorsException $exception)
1212
{
1313
$this->success = null === $exception;
1414
}

0 commit comments

Comments
 (0)