diff --git a/packages/validation/src/Exceptions/ValidationFailed.php b/packages/validation/src/Exceptions/ValidationFailed.php index b9b7e25cf..33a8fe003 100644 --- a/packages/validation/src/Exceptions/ValidationFailed.php +++ b/packages/validation/src/Exceptions/ValidationFailed.php @@ -5,10 +5,11 @@ namespace Tempest\Validation\Exceptions; use Exception; +use Tempest\Core\ProvidesContext; use Tempest\Validation\FailingRule; use Tempest\Validation\Internal\MessageRule; -final class ValidationFailed extends Exception +final class ValidationFailed extends Exception implements ProvidesContext { /** * @template TKey of array-key @@ -23,10 +24,23 @@ 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), - }); + }; + + foreach ($errorMessages as $field => $messages) { + $message .= sprintf('%s- %s: %s', PHP_EOL, $field, implode('; ', $messages)); + } + + parent::__construct($message); + } + + public function context(): iterable + { + return array_filter([ + 'errors' => $this->errorMessages, + ]); } /** diff --git a/packages/validation/tests/ValidatorTest.php b/packages/validation/tests/ValidatorTest.php index 130d314ce..febdf4515 100644 --- a/packages/validation/tests/ValidatorTest.php +++ b/packages/validation/tests/ValidatorTest.php @@ -133,6 +133,43 @@ 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( + implode(PHP_EOL, [ + '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_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;