From 7b86ec04929670f8dac4e556ad5ca0cb9b151e34 Mon Sep 17 00:00:00 2001 From: mgalexandre Date: Wed, 8 Jul 2026 10:54:30 +0100 Subject: [PATCH 1/3] feat(validation): include error messages in ValidationFailed exception message --- .../src/Exceptions/ValidationFailed.php | 12 ++++++++-- packages/validation/tests/ValidatorTest.php | 23 +++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/validation/src/Exceptions/ValidationFailed.php b/packages/validation/src/Exceptions/ValidationFailed.php index b9b7e25cf..7612028de 100644 --- a/packages/validation/src/Exceptions/ValidationFailed.php +++ b/packages/validation/src/Exceptions/ValidationFailed.php @@ -8,6 +8,8 @@ use Tempest\Validation\FailingRule; use Tempest\Validation\Internal\MessageRule; +use function Tempest\Support\Json\encode; + final class ValidationFailed extends Exception { /** @@ -23,10 +25,16 @@ public function __construct( private(set) array $errorMessages = [], private(set) ?string $targetClass = null, ) { - parent::__construct(match (true) { + $message = match (true) { is_null($subject) => 'Validation failed.', default => sprintf('Validation failed for %s.', is_object($subject) ? $subject::class : $subject), - }); + }; + + if ($errorMessages !== []) { + $message .= ' ' . encode($errorMessages); + } + + parent::__construct($message); } /** diff --git a/packages/validation/tests/ValidatorTest.php b/packages/validation/tests/ValidatorTest.php index 130d314ce..f2cf7ae84 100644 --- a/packages/validation/tests/ValidatorTest.php +++ b/packages/validation/tests/ValidatorTest.php @@ -133,6 +133,29 @@ public function test_validation_failed_can_be_created_from_messages(): void $this->assertSame('Email domain is not allowed', $this->validator->getErrorMessage($validationFailed->failingRules['email'][1])); } + public function test_validation_failed_message_includes_error_messages(): void + { + $validationFailed = ValidationFailed::withMessages([ + 'credential' => 'Passkey not valid', + 'email' => [ + 'Email is already taken', + 'Email domain is not allowed', + ], + ]); + + $this->assertSame( + 'Validation failed. {"credential":["Passkey not valid"],"email":["Email is already taken","Email domain is not allowed"]}', + $validationFailed->getMessage(), + ); + } + + public function test_validation_failed_message_omits_error_messages_when_empty(): void + { + $validationFailed = new ValidationFailed(failingRules: []); + + $this->assertSame('Validation failed.', $validationFailed->getMessage()); + } + public function test_closure_passes_with_null_response(): void { $validator = $this->validator; From 28b71b4b81da79a5d6ad9cfd5f124555037a7999 Mon Sep 17 00:00:00 2001 From: mgalexandre Date: Thu, 9 Jul 2026 10:37:59 +0100 Subject: [PATCH 2/3] feat(validation): expose validation errors via ProvidesContext and a readable message --- .../src/Exceptions/ValidationFailed.php | 16 +++++++++++----- packages/validation/tests/ValidatorTest.php | 16 +++++++++++++++- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/packages/validation/src/Exceptions/ValidationFailed.php b/packages/validation/src/Exceptions/ValidationFailed.php index 7612028de..393e325db 100644 --- a/packages/validation/src/Exceptions/ValidationFailed.php +++ b/packages/validation/src/Exceptions/ValidationFailed.php @@ -5,12 +5,11 @@ namespace Tempest\Validation\Exceptions; use Exception; +use Tempest\Core\ProvidesContext; use Tempest\Validation\FailingRule; use Tempest\Validation\Internal\MessageRule; -use function Tempest\Support\Json\encode; - -final class ValidationFailed extends Exception +final class ValidationFailed extends Exception implements ProvidesContext { /** * @template TKey of array-key @@ -30,13 +29,20 @@ public function __construct( default => sprintf('Validation failed for %s.', is_object($subject) ? $subject::class : $subject), }; - if ($errorMessages !== []) { - $message .= ' ' . encode($errorMessages); + foreach ($errorMessages as $field => $messages) { + $message .= PHP_EOL . sprintf('- %s: %s', $field, implode('; ', $messages)); } parent::__construct($message); } + public function context(): iterable + { + return array_filter([ + 'errors' => $this->errorMessages, + ]); + } + /** * @param array> $messages */ diff --git a/packages/validation/tests/ValidatorTest.php b/packages/validation/tests/ValidatorTest.php index f2cf7ae84..febdf4515 100644 --- a/packages/validation/tests/ValidatorTest.php +++ b/packages/validation/tests/ValidatorTest.php @@ -144,7 +144,11 @@ public function test_validation_failed_message_includes_error_messages(): void ]); $this->assertSame( - 'Validation failed. {"credential":["Passkey not valid"],"email":["Email is already taken","Email domain is not allowed"]}', + implode(PHP_EOL, [ + 'Validation failed.', + '- credential: Passkey not valid', + '- email: Email is already taken; Email domain is not allowed', + ]), $validationFailed->getMessage(), ); } @@ -156,6 +160,16 @@ public function test_validation_failed_message_omits_error_messages_when_empty() $this->assertSame('Validation failed.', $validationFailed->getMessage()); } + public function test_validation_failed_provides_error_messages_as_context(): void + { + $validationFailed = ValidationFailed::withMessages([ + 'email' => 'Email is already taken', + ]); + + $this->assertSame(['errors' => ['email' => ['Email is already taken']]], $validationFailed->context()); + $this->assertSame([], new ValidationFailed(failingRules: [])->context()); + } + public function test_closure_passes_with_null_response(): void { $validator = $this->validator; From d01e9b1fbdf5c39e8a08691d90f203b75b1a16a1 Mon Sep 17 00:00:00 2001 From: mgalexandre Date: Tue, 14 Jul 2026 15:21:19 +0100 Subject: [PATCH 3/3] chore(validation): inline string concatenation into sprintf call --- packages/validation/src/Exceptions/ValidationFailed.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/validation/src/Exceptions/ValidationFailed.php b/packages/validation/src/Exceptions/ValidationFailed.php index 393e325db..33a8fe003 100644 --- a/packages/validation/src/Exceptions/ValidationFailed.php +++ b/packages/validation/src/Exceptions/ValidationFailed.php @@ -30,7 +30,7 @@ public function __construct( }; foreach ($errorMessages as $field => $messages) { - $message .= PHP_EOL . sprintf('- %s: %s', $field, implode('; ', $messages)); + $message .= sprintf('%s- %s: %s', PHP_EOL, $field, implode('; ', $messages)); } parent::__construct($message);