8000 [Feature] Show validation failed data in error meta (#289) · jpaniorte/laravel-json-api@699ad18 · GitHub
[go: up one dir, main page]

Skip to content

Commit 699ad18

Browse files
[Feature] Show validation failed data in error meta (cloudcreativity#289)
Adds an opt-in feature that adds Laravel validation failure data to the `meta` member of JSON API error objects. Closes cloudcreativity#263
1 parent 8f98340 commit 699ad18

File tree

15 files changed

+665
-58
lines changed

15 files changed

+665
-58
lines changed

docs/basics/validators.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -811,3 +811,83 @@ class Validators extends AbstractValidators
811811
}
812812
}
813813
```
814+
815+
## Failed Rules
816+
817+
This package makes it possible to include a machine-readable reason why a value failed validation within
818+
the JSON API error object's `meta` member. This is an opt-in feature because it is not standard practice
819+
for Laravel to JSON encode validation failure information with validation error messages.
820+
821+
For example, if a value fails to pass the `between` rule, then by default this package will return the
822+
following response content:
823+
824+
```json
825+
{
826+
"errors": [
827+
{
828+
"status": "422",
829+
"title": "Unprocessable Entity",
830+
"detail": "The value must be between 1 and 10.",
831+
"source": {
832+
"pointer": "/data/attributes/value"
833+
}
834+
}
835+
]
836+
}
837+
```
838+
839+
If you opt-in to showing failed meta, the response content will be:
840+
841+
```json
842+
{
843+
"errors": [
844+
{
845+
"status": "422",
846+
"title": "Unprocessable Entity",
847+
"detail": "The value must be between 1 and 10.",
848+
"source": {
849+
"pointer": "/data/attributes/value"
850+
},
851+
"meta": {
852+
"failed": {
853+
"rule": "between",
854+
"options": [
855+
"1",
856+
"10"
857+
]
858+
}
859+
}
860+
}
861+
]
862+
}
863+
```
864+
865+
The rule name will be the dash-case version of the Laravel rule. For example, `before_or_equal` will
866+
be `before-or-equal`. If the rule is a rule object, we use the dash-case of the class basename.
867+
For example, `CloudCreativity\LaravelJsonApi\Rules\DateTimeIso8601` will be `date-time-iso8601`.
868+
869+
The `options` member will only exist if the rule has options. We intentionally omit rule options
870+
for the `exists` and `unique` rules as the options for these database rules reveal information
871+
about your database setup.
872+
873+
To opt-in to this feature, add the following to the `register` method of your `AppServiceProvider`:
874+
875+
```php
876+
<?php
877+
878+
namespace App\Providers;
879+
880+
use CloudCreativity\LaravelJsonApi\LaravelJsonApi;
881+
use Illuminate\Support\ServiceProvider;
882+
883+
class AppServiceProvider extends ServiceProvider
884+
{
885+
public function register()
886+
{
887+
LaravelJsonApi::showValidatorFailures();
888+
}
889+
890+
// ...
891+
892+
}
893+
```

resources/lang/en/errors.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,4 +123,10 @@
123123
'detail' => 'The request query parameters are invalid.',
124124
'code' => '',
125125
],
126+
127+
'failed_validator' => [
128+
'title' => 'Unprocessable Entity',
129+
'detail' => 'The document was well-formed but contains semantic errors.',
130+
'code' => '',
131+
],
126132
];

src/Exceptions/ValidationException.php

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
namespace CloudCreativity\LaravelJsonApi\Exceptions;
2020

21+
use CloudCreativity\LaravelJsonApi\Contracts\Validation\ValidatorInterface;
22+
use CloudCreativity\LaravelJsonApi\Utils\Helpers;
2123
use Exception;
2224
use Neomerx\JsonApi\Contracts\Document\ErrorInterface;
2325
use Neomerx\JsonApi\Exceptions\ErrorCollection;
@@ -31,6 +33,25 @@
3133
class ValidationException extends JsonApiException
3234
{
3335

36+
/**
37+
* @var ValidatorInterface|null
38+
*/
39+
private $validator;
40+
41+
/**
42+
* Create a validation exception from a validator.
43+
*
44+
* @param ValidatorInterface $validator
45+
* @return ValidationException
46+
*/
47+
public static function create(ValidatorInterface $validator): self
48+
{
49+
$ex = new self($validator->getErrors());
50+
$ex->validator = $validator;
51+
52+
return $ex;
53+
}
54+
3455
/**
3556
* ValidationException constructor.
3657
*
@@ -40,8 +61,18 @@ class ValidationException extends JsonApiException
4061
*/
4162
public function __construct($errors, $defaultHttpCode = self::DEFAULT_HTTP_CODE, Exception $previous = null)
4263
{
43-
$errors = MutableErrorCollection::cast($errors);
64+
parent::__construct(
65+
$errors,
66+
Helpers::httpErrorStatus($errors, $defaultHttpCode),
67+
$previous
68+
);
69+
}
4470

45-
parent::__construct($errors, $errors->getHttpStatus($defaultHttpCode), $previous);
71+
/**
72+
* @return ValidatorInterface|null
73+
*/
74+
public function getValidator(): ?ValidatorInterface
75+
{
76+
return $this->validator;
4677
}
4778
}

src/Factories/Factory.php

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,7 @@ public function createCodec(ContainerInterface $container, Encoding $encoding, ?
447447
* @param array $messages
448448
* @param array $customAttributes
449449
* @param \Closure|null $callback
450+
* a closure for creating an error, that will be bound to the error translator.
450451
* @return ValidatorInterface
451452
*/
452453
public function createValidator(
@@ -457,9 +458,11 @@ public function createValidator(
457458
\Closure $callback = null
458459
): ValidatorInterface
459460
{
461+
$translator = $this->createErrorTranslator();
462+
460463
return new Validation\Validator(
461464
$this->makeValidator($data, $rules, $messages, $customAttributes),
462-
$this->createErrorTranslator(),
465+
$translator,
463466
$callback
464467
);
465468
}
@@ -484,10 +487,11 @@ public function createResourceValidator(
484487
$rules,
485488
$messages,
486489
$customAttributes,
487-
function ($key, $detail, ErrorTranslator $translator) use ($resource) {
488-
return $translator->invalidResource(
490+
function ($key, $detail, $failed) use ($resource) {
491+
return $this->invalidResource(
489492
$resource->pointer($key, '/data'),
490-
$detail
493+
$detail,
494+
$failed
491495
);
492496
}
493497
);
@@ -514,10 +518,11 @@ public function createRelationshipValidator(
514518
$rules,
515519
$messages,
516520
$customAttributes,
517-
function ($key, $detail, ErrorTranslator $translator) use ($resource) {
518-
return $translator->invalidResource(
521+
function ($key, $detail, $failed) use ($resource) {
522+
return $this->invalidResource(
519523
$resource->pointerForRelationship($key, '/data'),
520-
$detail
524+
$detail,
525+
$failed
521526
);
522527
}
523528
);
@@ -541,8 +546,8 @@ public function createDeleteValidator(
541546
$rules,
542547
$messages,
543548
$customAttributes,
544-
function ($key, $detail, ErrorTranslator $translator) {
545-
return $translator->resourceCannotBeDeleted($detail);
549+
function ($key, $detail) {
550+
return $this->resourceCannotBeDeleted($detail);
546551
}
547552
);
548553
}
@@ -567,8 +572,8 @@ public function createQueryValidator(
567572
$rules,
568573
$messages,
569574
$customAttributes,
570-
function ($key, $detail, ErrorTranslator $translator) {
571-
return $translator->invalidQueryParameter($key, $detail);
575+
function ($key, $detail, $failed) {
576+
return $this->invalidQueryParameter($key, $detail, $failed);
572577
}
573578
);
574579
}

src/Http/Requests/ValidatedRequest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,10 @@ protected function passes($validator)
304304
*/
305305
protected function failedValidation($validator)
306306
{
307+
if ($validator instanceof ValidatorInterface) {
308+
throw ValidationException::create($validator);
309+
}
310+
307311
throw new ValidationException($validator->getErrors());
308312
}
309313

src/LaravelJsonApi.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ class LaravelJsonApi
4141
*/
4242
public static $queueBindings = true;
4343

44+
/**
45+
* Indicates if Laravel validator failed data is added to JSON API error objects.
46+
*
47+
* @var bool
48+
*/
49+
public static $validationFailures = false;
50+
4451
/**
4552
* Set the default API name.
4653
*
@@ -77,4 +84,14 @@ public static function skipQueueBindings(): self
7784

7885
return new self();
7986
}
87+
88+
/**
89+
* @return LaravelJsonApi
90+
*/
91+
public static function showValidatorFailures(): self
92+
{
93+
self::$validationFailures = true;
94+
95+
return new self();
96+
}
8097
}

src/Utils/AbstractErrorBag.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
* Class AbstractErrorBag
3232
*
3333
* @package CloudCreativity\LaravelJsonApi
34+
* @deprecated 2.0.0 use the error translator instead.
3435
*/
3536
abstract class AbstractErrorBag implements Countable, IteratorAggregate, MessageProvider, Arrayable
3637
{

src/Utils/ErrorBag.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
* Class ErrorBag
3030
*
3131
* @package CloudCreativity\LaravelJsonApi
32+
* @deprecated 2.0.0 use the error translator instead.
3233
*/
3334
class ErrorBag extends AbstractErrorBag
3435
{

src/Utils/Helpers.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,13 +157,17 @@ public static function isJsonApi($request)
157157
* code SHOULD be used in the response. For instance, 400 Bad Request might be appropriate for multiple
158158
* 4xx errors or 500 Internal Server Error might be appropriate for multiple 5xx errors.
159159
*
160-
* @param iterable $errors
160+
* @param iterable|ErrorInterface $errors
161161
* @param int $default
162162
* @return int
163163
* @see https://jsonapi.org/format/#errors
164164
*/
165-
public static function httpErrorStatus(iterable $errors, int $default = SymfonyResponse::HTTP_BAD_REQUEST): int
165+
public static function httpErrorStatus($errors, int $default = SymfonyResponse::HTTP_BAD_REQUEST): int
166166
{
167+
if ($errors instanceof ErrorInterface) {
168+
$errors = [$errors];
169+
}
170+
167171
$statuses = collect($errors)->reject(function (ErrorInterface $error) {
168172
return is_null($error->getStatus());
169173
})->map(function (ErrorInterface $error) {

0 commit comments

Comments
 (0)
0