Skip to content

Commit 72f0cec

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

Some content is hidden

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

47 files changed

+1556
-1486
lines changed

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: 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' => json_decode(json_encode($this->variables), true)];
38+
}
2839
}

src/Errors.php

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

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+
$this->message = (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()->addError($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)