Skip to content

Commit 2e7fe6f

Browse files
committed
better-error-handling
2 parents 67c8e08 + 8e9e3f8 commit 2e7fe6f

Some content is hidden

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

50 files changed

+1987
-1732
lines changed

README.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,13 @@ 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
4242

4343
```php
44+
use Chubbyphp\Parsing\ErrorsException;
4445
use Chubbyphp\Parsing\Schema\SchemaInterface;
4546

4647
/** @var SchemaInterface $schema */
@@ -51,7 +52,13 @@ $schema->preParse(static fn ($input) => $input);
5152
$schema->postParse(static fn (string $output) => $output);
5253
$schema->parse('test');
5354
$schema->safeParse('test');
54-
$schema->catch(static fn (string $output, ParserErrorException $e) => $output);
55+
$schema->catch(static fn (string $output, ErrorsException $e) => $output);
56+
57+
try {
58+
$schema->parse('test');
59+
} catch (ErrorsException $e) {
60+
var_dump($e->errors->toApiProblemInvalidParameters());
61+
}
5562
```
5663

5764
### array
@@ -391,8 +398,15 @@ $schema = $p->union([$p->string(), $p->int()]);
391398
$data = $schema->parse('42');
392399
```
393400

401+
## Migration
402+
403+
* [1.x to 2.x][10]
404+
394405
## Copyright
395406

396407
2025 Dominik Zogg
397408

398409
[1]: https://packagist.org/packages/chubbyphp/chubbyphp-parsing
410+
411+
412+
[10]: doc/Migration/1.x-2.x.md

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": {

doc/Migration/1.x-2.x.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# 1.x to 2.x
2+
3+
Adapted error handling.
4+
5+
Added:
6+
7+
- `Chubbyphp\Parsing\Errors`
8+
- `Chubbyphp\Parsing\ErrorsException`
9+
10+
Removed:
11+
12+
- `Chubbyphp\Parsing\ParserErrorException`
13+
- `Chubbyphp\Parsing\ParserErrorExceptionToString`
14+
15+
16+
old:
17+
18+
```php
19+
<?php
20+
21+
declare(strict_types=1);
22+
23+
namespace App;
24+
25+
use Chubbyphp\Parsing\Parser;
26+
use Chubbyphp\Parsing\ParserErrorException;
27+
28+
$p = new Parser();
29+
30+
$schema = $p->string();
31+
32+
try {
33+
$schema->parse('test');
34+
} catch (ParserErrorException $e) {
35+
var_dump($e->getApiProblemErrorMessages());
36+
}
37+
```
38+
39+
new
40+
41+
```php
42+
<?php
43+
44+
declare(strict_types=1);
45+
46+
namespace App;
47+
48+
use Chubbyphp\Parsing\ErrorsException;
49+
use Chubbyphp\Parsing\Parser;
50+
51+
$p = new Parser();
52+
53+
$schema = $p->string();
54+
55+
try {
56+
$schema->parse('test');
57+
} catch (ErrorsException $e) {
58+
var_dump($e->errors->toApiProblemInvalidParameters());
59+
}
60+
```

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: 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 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 ApiProblemInvalidParameter 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 $errors, string $path = ''): self
28+
{
29+
if ($errors instanceof self) {
30+
foreach ($errors->errorsWithPath as $errorWithPath) {
31+
$this->errorsWithPath[] = ['path' => $this->mergePath($path, $errorWithPath['path']), 'error' => $errorWithPath['error']];
32+
}
33+
34+
return $this;
35+
}
36+
37+
$this->errorsWithPath[] = ['path' => $path, 'error' => $errors];
38+
39+
return $this;
40+
}
41+
42+
public function has(): bool
43+
{
44+
return 0 !== \count($this->errorsWithPath);
45+
}
46+
47+
/**
48+
* @return ErrorsWithPathJson
49+
*/
50+
public function jsonSerialize(): array
51+
{
52+
return array_map(static fn ($errorWithPath) => [
53+
'path' => $errorWithPath['path'],
54+
'error' => $errorWithPath['error']->jsonSerialize(),
55+
], $this->errorsWithPath);
56+
}
57+
58+
/**
59+
* @return array<ApiProblemInvalidParameter>
60+
*/
61+
public function toApiProblemInvalidParameters(): array
62+
{
63+
return array_map(
64+
fn ($errorWithPath) => [
65+
'name' => $this->pathToName($errorWithPath['path']),
66+
'reason' => (string) $errorWithPath['error'],
67+
'details' => [
68+
'_template' => $errorWithPath['error']->template,
69+
...$errorWithPath['error']->variables,
70+
],
71+
],
72+
$this->errorsWithPath
73+
);
74+
}
75+
76+
/**
77+
* @return array<string, mixed>
78+
*/
79+
public function toTree(): array
80+
{
81+
// @var array<string, mixed> $tree
82+
return array_reduce(
83+
$this->errorsWithPath,
84+
static function (array $tree, $errorWithPath): array {
85+
$pathParts = explode('.', $errorWithPath['path']);
86+
87+
$current = &$tree;
88+
$lastIndex = \count($pathParts) - 1;
89+
90+
foreach ($pathParts as $i => $pathPart) {
91+
if ($i < $lastIndex) {
92+
$current[$pathPart] ??= [];
93+
$current = &$current[$pathPart];
94+
95+
continue;
96+
}
97+
98+
$current[$pathPart] = array_merge($current[$pathPart] ?? [], [(string) $errorWithPath['error']]);
99+
}
100+
101+
return $tree;
102+
},
103+
[]
104+
);
105+
}
106+
107+
private function mergePath(string $path, string $existingPath): string
108+
{
109+
return implode('.', array_filter([$path, $existingPath], static fn ($part) => '' !== $part));
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+
}

src/ErrorsException.php

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

0 commit comments

Comments
 (0)