Skip to content

Commit fd089c6

Browse files
committed
Improve error sourcing
- Allow all exceptions to have a "source" - Move ErrorProviderInterface into Exception namespace Fixes #85
1 parent 751d882 commit fd089c6

28 files changed

+205
-206
lines changed

docs/errors.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ The interface defines two methods:
2626

2727
```php
2828
use JsonApiPhp\JsonApi\Error;
29-
use Tobyz\JsonApiServer\ErrorProviderInterface;
29+
use Tobyz\JsonApiServer\Exception\ErrorProviderInterface;
3030

3131
class ImATeapotException implements ErrorProviderInterface
3232
{

src/Endpoint/Concerns/IncludesData.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ private function validateInclude(
7676
continue 2;
7777
}
7878

79-
throw new BadRequestException("Invalid include [{$path}{$name}]", [
79+
throw (new BadRequestException("Invalid include [$path$name]"))->setSource([
8080
'parameter' => 'include',
8181
]);
8282
}

src/Endpoint/Concerns/SavesData.php

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Tobyz\JsonApiServer\Exception\BadRequestException;
77
use Tobyz\JsonApiServer\Exception\ConflictException;
88
use Tobyz\JsonApiServer\Exception\ForbiddenException;
9+
use Tobyz\JsonApiServer\Exception\Sourceable;
910
use Tobyz\JsonApiServer\Exception\UnprocessableEntityException;
1011

1112
use function Tobyz\JsonApiServer\get_value;
@@ -27,39 +28,49 @@ private function parseData(Context $context): array
2728
$body = (array) $context->body();
2829

2930
if (!isset($body['data']) || !is_array($body['data'])) {
30-
throw new BadRequestException('data must be an object', ['pointer' => '/data']);
31+
throw (new BadRequestException('data must be an object'))->setSource([
32+
'pointer' => '/data',
33+
]);
3134
}
3235

3336
if (!isset($body['data']['type'])) {
34-
throw new BadRequestException('data.type must be present', ['pointer' => '/data/type']);
37+
throw (new BadRequestException('data.type must be present'))->setSource([
38+
'pointer' => '/data/type',
39+
]);
3540
}
3641

3742
if (isset($context->model)) {
3843
if (!isset($body['data']['id'])) {
39-
throw new BadRequestException('data.id must be present', ['pointer' => '/data/id']);
44+
throw (new BadRequestException('data.id must be present'))->setSource([
45+
'pointer' => '/data/id',
46+
]);
4047
}
4148

4249
if ($body['data']['id'] !== $context->resource->getId($context->model, $context)) {
43-
throw new ConflictException('data.id does not match the resource ID', [
50+
throw (new ConflictException('data.id does not match the resource ID'))->setSource([
4451
'pointer' => '/data/id',
4552
]);
4653
}
4754
} elseif (isset($body['data']['id'])) {
48-
throw new ForbiddenException('Client-generated IDs are not supported');
55+
throw (new ForbiddenException('Client-generated IDs are not supported'))->setSource([
56+
'pointer' => '/data/id',
57+
]);
4958
}
5059

5160
if (!in_array($body['data']['type'], $context->collection->resources())) {
52-
throw new ConflictException('collection does not support this resource type');
61+
throw (new ConflictException(
62+
'collection does not support this resource type',
63+
))->setSource(['pointer' => '/data/type']);
5364
}
5465

5566
if (isset($body['data']['attributes']) && !is_array($body['data']['attributes'])) {
56-
throw new BadRequestException('data.attributes must be an object', [
67+
throw (new BadRequestException('data.attributes must be an object'))->setSource([
5768
'pointer' => '/data/attributes',
5869
]);
5970
}
6071

6172
if (isset($body['data']['relationships']) && !is_array($body['data']['relationships'])) {
62-
throw new BadRequestException('data.relationships must be an object', [
73+
throw (new BadRequestException('data.relationships must be an object'))->setSource([
6374
'pointer' => '/data/relationships',
6475
]);
6576
}
@@ -88,7 +99,7 @@ private function assertFieldsExist(Context $context, array $data): void
8899
foreach (['attributes', 'relationships'] as $location) {
89100
foreach ($data[$location] as $name => $value) {
90101
if (!isset($fields[$name]) || $location !== location($fields[$name])) {
91-
throw new BadRequestException("Unknown field [$name]", [
102+
throw (new BadRequestException("Unknown field [$name]"))->setSource([
92103
'pointer' => "/data/$location/$name",
93104
]);
94105
}
@@ -109,7 +120,9 @@ private function assertFieldsWritable(Context $context, array $data): void
109120
}
110121

111122
if (!$field->isWritable($context->withField($field))) {
112-
throw new ForbiddenException("Field [$field->name] is not writable");
123+
throw (new ForbiddenException("Field [$field->name] is not writable"))->setSource([
124+
'pointer' => '/data/' . location($field) . '/' . $field->name,
125+
]);
113126
}
114127
}
115128
}
@@ -128,7 +141,7 @@ private function deserializeValues(Context $context, array &$data): void
128141

129142
try {
130143
set_value($data, $field, $field->deserializeValue($value, $context));
131-
} catch (BadRequestException $e) {
144+
} catch (Sourceable $e) {
132145
throw $e->prependSource([
133146
'pointer' => '/data/' . location($field) . '/' . $field->name,
134147
]);

src/Endpoint/Index.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Tobyz\JsonApiServer\Exception\BadRequestException;
1111
use Tobyz\JsonApiServer\Exception\ForbiddenException;
1212
use Tobyz\JsonApiServer\Exception\MethodNotAllowedException;
13+
use Tobyz\JsonApiServer\Exception\Sourceable;
1314
use Tobyz\JsonApiServer\Pagination\OffsetPagination;
1415
use Tobyz\JsonApiServer\Resource\Countable;
1516
use Tobyz\JsonApiServer\Resource\Listable;
@@ -141,7 +142,9 @@ private function applySorts($query, Context $context): void
141142
}
142143
}
143144

144-
throw new BadRequestException("Invalid sort: $name", ['parameter' => 'sort']);
145+
throw (new BadRequestException("Invalid sort: $name"))->setSource([
146+
'parameter' => 'sort',
147+
]);
145148
}
146149
}
147150

@@ -152,12 +155,14 @@ private function applyFilters($query, Context $context): void
152155
}
153156

154157
if (!is_array($filters)) {
155-
throw new BadRequestException('filter must be an array', ['parameter' => 'filter']);
158+
throw (new BadRequestException('filter must be an array'))->setSource([
159+
'parameter' => 'filter',
160+
]);
156161
}
157162

158163
try {
159164
apply_filters($query, $filters, $context->collection, $context);
160-
} catch (BadRequestException $e) {
165+
} catch (Sourceable $e) {
161166
throw $e->prependSource(['parameter' => 'filter']);
162167
}
163168
}

src/Exception/BadRequestException.php

Lines changed: 3 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3,56 +3,11 @@
33
namespace Tobyz\JsonApiServer\Exception;
44

55
use DomainException;
6-
use Throwable;
7-
use Tobyz\JsonApiServer\ErrorProviderInterface;
6+
use Tobyz\JsonApiServer\Exception\Concerns\SingleError;
87

9-
class BadRequestException extends DomainException implements ErrorProviderInterface
8+
class BadRequestException extends DomainException implements ErrorProviderInterface, Sourceable
109
{
11-
public function __construct(
12-
string $message = '',
13-
public ?array $source = null,
14-
int $code = 0,
15-
?Throwable $previous = null,
16-
) {
17-
parent::__construct($message, $code, $previous);
18-
}
19-
20-
public function setSource(?array $source): static
21-
{
22-
$this->source = $source;
23-
24-
return $this;
25-
}
26-
27-
public function prependSource(array $source): static
28-
{
29-
foreach ($source as $k => $v) {
30-
$this->source = [$k => $v . ($this->source[$k] ?? '')];
31-
}
32-
33-
return $this;
34-
}
35-
36-
public function getJsonApiErrors(): array
37-
{
38-
$members = [];
39-
40-
if ($this->message) {
41-
$members['detail'] = $this->message;
42-
}
43-
44-
if ($this->source) {
45-
$members['source'] = $this->source;
46-
}
47-
48-
return [
49-
[
50-
'title' => 'Bad Request',
51-
'status' => $this->getJsonApiStatus(),
52-
...$members,
53-
],
54-
];
55-
}
10+
use SingleError;
5611

5712
public function getJsonApiStatus(): string
5813
{
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
namespace Tobyz\JsonApiServer\Exception\Concerns;
4+
5+
use ReflectionClass;
6+
7+
trait SingleError
8+
{
9+
protected ?array $source = null;
10+
11+
public function setSource(?array $source): static
12+
{
13+
$this->source = $source;
14+
15+
return $this;
16+
}
17+
18+
public function prependSource(array $source): static
19+
{
20+
foreach ($source as $k => $v) {
21+
$this->source[$k] = $v . ($this->source[$k] ?? '');
22+
}
23+
24+
return $this;
25+
}
26+
27+
public function getJsonApiErrors(): array
28+
{
29+
$members = [];
30+
31+
if ($this->message) {
32+
$members['detail'] = $this->message;
33+
}
34+
35+
if ($this->source) {
36+
$members['source'] = $this->source;
37+
}
38+
39+
return [
40+
[
41+
'status' => $this->getJsonApiStatus(),
42+
'title' => $this->getErrorTitle(),
43+
'detail' => $this->getMessage(),
44+
...$members,
45+
],
46+
];
47+
}
48+
49+
protected function getErrorTitle(): string
50+
{
51+
$class = (new ReflectionClass($this))->getShortName();
52+
$words = preg_split('/(?=[A-Z])/', $class);
53+
54+
if (end($words) === 'Exception') {
55+
array_pop($words);
56+
}
57+
58+
return trim(implode(' ', $words));
59+
}
60+
}

src/Exception/ConflictException.php

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,11 @@
33
namespace Tobyz\JsonApiServer\Exception;
44

55
use DomainException;
6-
use Tobyz\JsonApiServer\ErrorProviderInterface;
6+
use Tobyz\JsonApiServer\Exception\Concerns\SingleError;
77

8-
class ConflictException extends DomainException implements ErrorProviderInterface
8+
class ConflictException extends DomainException implements ErrorProviderInterface, Sourceable
99
{
10-
public function getJsonApiErrors(): array
11-
{
12-
return [
13-
[
14-
'title' => 'Conflict',
15-
'status' => $this->getJsonApiStatus(),
16-
...$this->message ? ['detail' => $this->message] : [],
17-
],
18-
];
19-
}
10+
use SingleError;
2011

2112
public function getJsonApiStatus(): string
2213
{

src/ErrorProviderInterface.php renamed to src/Exception/ErrorProviderInterface.php

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
11
<?php
22

3-
namespace Tobyz\JsonApiServer;
4-
5-
use JsonApiPhp\JsonApi\Error;
3+
namespace Tobyz\JsonApiServer\Exception;
64

75
interface ErrorProviderInterface
86
{
97
/**
108
* Get JSON:API error objects that represent this error.
11-
*
12-
* @return Error[]
139
*/
1410
public function getJsonApiErrors(): array;
1511

src/Exception/ForbiddenException.php

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,11 @@
33
namespace Tobyz\JsonApiServer\Exception;
44

55
use DomainException;
6-
use Tobyz\JsonApiServer\ErrorProviderInterface;
6+
use Tobyz\JsonApiServer\Exception\Concerns\SingleError;
77

8-
class ForbiddenException extends DomainException implements ErrorProviderInterface
8+
class ForbiddenException extends DomainException implements ErrorProviderInterface, Sourceable
99
{
10-
public function getJsonApiErrors(): array
11-
{
12-
return [
13-
[
14-
'title' => 'Forbidden',
15-
'status' => $this->getJsonApiStatus(),
16-
...$this->message ? ['detail' => $this->message] : [],
17-
],
18-
];
19-
}
10+
use SingleError;
2011

2112
public function getJsonApiStatus(): string
2213
{

src/Exception/InternalServerErrorException.php

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,13 @@
33
namespace Tobyz\JsonApiServer\Exception;
44

55
use RuntimeException;
6-
use Tobyz\JsonApiServer\ErrorProviderInterface;
6+
use Tobyz\JsonApiServer\Exception\Concerns\SingleError;
77

8-
class InternalServerErrorException extends RuntimeException implements ErrorProviderInterface
8+
class InternalServerErrorException extends RuntimeException implements
9+
ErrorProviderInterface,
10+
Sourceable
911
{
10-
public function getJsonApiErrors(): array
11-
{
12-
return [
13-
[
14-
'title' => 'Internal Server Error',
15-
'status' => $this->getJsonApiStatus(),
16-
],
17-
];
18-
}
12+
use SingleError;
1913

2014
public function getJsonApiStatus(): string
2115
{

src/Exception/MethodNotAllowedException.php

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,14 @@
22

33
namespace Tobyz\JsonApiServer\Exception;
44

5-
use DomainException as DomainExceptionAlias;
6-
use Tobyz\JsonApiServer\ErrorProviderInterface;
5+
use DomainException;
6+
use Tobyz\JsonApiServer\Exception\Concerns\SingleError;
77

8-
class MethodNotAllowedException extends DomainExceptionAlias implements ErrorProviderInterface
8+
class MethodNotAllowedException extends DomainException implements
9+
ErrorProviderInterface,
10+
Sourceable
911
{
10-
public function getJsonApiErrors(): array
11-
{
12-
return [
13-
[
14-
'title' => 'Method Not Allowed',
15-
'status' => $this->getJsonApiStatus(),
16-
],
17-
];
18-
}
12+
use SingleError;
1913

2014
public function getJsonApiStatus(): string
2115
{

0 commit comments

Comments
 (0)