diff --git a/README.md b/README.md index 6b9816ff..18aa0019 100644 --- a/README.md +++ b/README.md @@ -559,7 +559,7 @@ The Seam API parses these params with the corresponding [parser]. [reference implementation]: https://github.com/seamapi/url-search-params-serializer [parser]: https://github.com/seamapi/url-search-params-parser -#### Errors +### Error Handling Every exception the SDK raises implements `Seam\SeamException`, so it can be caught as a group. An API error is a `Seam\HttpApiError` carrying @@ -567,16 +567,31 @@ caught as a group. An API error is a `Seam\HttpApiError` carrying `Seam\HttpUnauthorizedError` and `Seam\HttpInvalidInputError` as the two specific cases worth catching on their own. +#### Validation errors + +When the API rejects a request because a parameter is invalid, it throws an +`HttpInvalidInputError`. Look up messages for a parameter you are already +rendering, for example a field in a form: + ```php -use Seam\HttpApiError; use Seam\HttpInvalidInputError; try { - $seam->devices->get(device_id: $device_id); + $seam->devices->list(device_ids: ["not-a-uuid"]); } catch (HttpInvalidInputError $error) { - print_r($error->getValidationErrorMessages("device_id")); -} catch (HttpApiError $error) { - print $error->getErrorCode(); + print_r($error->getValidationErrorMessages("device_ids")); +} +``` + +Or read every parameter that failed validation to summarize the request: + +```php +foreach ($error->validation_errors as $validation_error) { + printf( + "%s: %s\n", + $validation_error->parameter_name, + implode(", ", $validation_error->error_messages), + ); } ``` diff --git a/src/HttpInvalidInputError.php b/src/HttpInvalidInputError.php index 1ff84ac6..6d4d1762 100644 --- a/src/HttpInvalidInputError.php +++ b/src/HttpInvalidInputError.php @@ -7,7 +7,14 @@ */ class HttpInvalidInputError extends HttpApiError { - private object $validationErrors; + private object $rawValidationErrors; + + /** + * Validation errors, one entry per failed request parameter. + * + * @var list + */ + public readonly array $validation_errors; public function __construct( object $error, @@ -16,7 +23,21 @@ public function __construct( ) { parent::__construct($error, $statusCode, $requestId); $this->errorCode = "invalid_input"; - $this->validationErrors = $error->validation_errors ?? (object) []; + $this->rawValidationErrors = $error->validation_errors ?? (object) []; + + $validationErrors = []; + foreach ( + get_object_vars($this->rawValidationErrors) + as $paramName => $_ + ) { + if ($paramName !== "_errors") { + $validationErrors[] = new ValidationError( + $paramName, + $this->getValidationErrorMessages($paramName), + ); + } + } + $this->validation_errors = $validationErrors; } /** @@ -27,6 +48,6 @@ public function __construct( */ public function getValidationErrorMessages(string $paramName): array { - return $this->validationErrors->{$paramName}->_errors ?? []; + return $this->rawValidationErrors->{$paramName}->_errors ?? []; } } diff --git a/src/ValidationError.php b/src/ValidationError.php new file mode 100644 index 00000000..2fa503a3 --- /dev/null +++ b/src/ValidationError.php @@ -0,0 +1,17 @@ +getValidationErrorMessages("device_ids"), ); + $this->assertEquals( + [ + new ValidationError("device_ids", [ + "Expected array, received number", + ]), + ], + $error->validation_errors, + ); } } + public function testValidationErrorsExcludeRequestWideErrors(): void + { + $error = new HttpInvalidInputError( + (object) [ + "validation_errors" => (object) [ + "_errors" => ["Request is invalid"], + "device_ids" => (object) [ + "_errors" => ["Invalid device IDs"], + ], + ], + ], + 400, + null, + ); + + $this->assertEquals( + [new ValidationError("device_ids", ["Invalid device IDs"])], + $error->validation_errors, + ); + } + public function testValidationMessagesAreEmptyForAnUnknownParam(): void { try {