@@ -55,26 +55,26 @@ public function validateObject(object $object): void
5555 /**
5656 * Creates a {@see ValidationFailed} exception from the given rule failures, populated with error messages.
5757 *
58- * @param array<string,Rule []> $failingRules
58+ * @param array<string,FailingRule []> $failingRules
5959 * @param class-string|null $targetClass
6060 */
6161 public function createValidationFailureException (array $ failingRules , null |object |string $ subject = null , ?string $ targetClass = null ): ValidationFailed
6262 {
6363 return new ValidationFailed (
64- $ failingRules ,
65- $ subject ,
66- Arr \map_iterable ($ failingRules , function (array $ rules , string $ field ) {
67- return Arr \map_iterable ($ rules , fn (Rule $ rule ) => $ this ->getErrorMessage ($ rule , $ field ));
64+ failingRules: $ failingRules ,
65+ subject: $ subject ,
66+ errorMessages: Arr \map_iterable ($ failingRules , function (array $ rules , string $ field ) {
67+ return Arr \map_iterable ($ rules , fn (FailingRule $ rule ) => $ this ->getErrorMessage ($ rule , $ field ));
6868 }),
69- $ targetClass ,
69+ targetClass: $ targetClass ,
7070 );
7171 }
7272
7373 /**
7474 * Validates the specified `$values` for the corresponding public properties on the specified `$class`, using built-in PHP types and attribute rules.
7575 *
7676 * @param ClassReflector|class-string $class
77- * @return Rule[]
77+ * @return array<string,FailingRule[]>
7878 */
7979 public function validateValuesForClass (ClassReflector |string $ class , ?array $ values , string $ prefix = '' ): array
8080 {
@@ -125,7 +125,7 @@ class: $property->getType()->asClass(),
125125 /**
126126 * Validates `$value` against the specified `$property`, using built-in PHP types and attribute rules.
127127 *
128- * @return Rule []
128+ * @return FailingRule []
129129 */
130130 public function validateValueForProperty (PropertyReflector $ property , mixed $ value ): array
131131 {
@@ -148,14 +148,19 @@ public function validateValueForProperty(PropertyReflector $property, mixed $val
148148 $ rules [] = new IsEnum (enum: $ property ->getType ()->getName (), orNull: $ property ->isNullable ());
149149 }
150150
151- return $ this ->validateValue ($ value , $ rules );
151+ $ key = $ property ->getAttribute (TranslationKey::class)?->key;
152+
153+ return Arr \map_iterable (
154+ array: $ this ->validateValue ($ value , $ rules ),
155+ map: fn (FailingRule $ rule ) => $ rule ->withKey ($ key ),
156+ );
152157 }
153158
154159 /**
155160 * Validates the specified `$value` against the specified set of `$rules`. If a rule is a closure, it may return a string as a validation error.
156161 *
157162 * @param Rule|array<Rule|(Closure(mixed $value):string|false)>|(Closure(mixed $value):string|false) $rules
158- * @return Rule []
163+ * @return FailingRule []
159164 */
160165 public function validateValue (mixed $ value , Closure |Rule |array $ rules ): array
161166 {
@@ -169,7 +174,7 @@ public function validateValue(mixed $value, Closure|Rule|array $rules): array
169174 $ rule = $ this ->convertToRule ($ rule , $ value );
170175
171176 if (! $ rule ->isValid ($ value )) {
172- $ failingRules [] = $ rule ;
177+ $ failingRules [] = new FailingRule ( $ rule, value: $ value ) ;
173178 }
174179 }
175180
@@ -206,21 +211,18 @@ public function validateValues(iterable $values, array $rules): array
206211 /**
207212 * Gets a localized validation error message for the specified rule.
208213 */
209- public function getErrorMessage (Rule $ rule , ?string $ field = null ): string
214+ public function getErrorMessage (Rule | FailingRule $ rule , ?string $ field = null ): string
210215 {
211216 if ($ rule instanceof HasErrorMessage) {
212217 return $ rule ->getErrorMessage ();
213218 }
214219
215- $ ruleTranslationKey = str ($ rule ::class)
216- ->classBasename ()
217- ->snake ()
218- ->replaceEvery ([ // those are snake case issues that we manually fix for consistency
219- 'i_pv6 ' => 'ipv6 ' ,
220- 'i_pv4 ' => 'ipv4 ' ,
221- 'reg_ex ' => 'regex ' ,
222- ])
223- ->toString ();
220+ $ ruleTranslationKey = $ this ->getTranslationKey ($ rule );
221+
222+ if ($ rule instanceof FailingRule) {
223+ $ field ??= $ rule ->field ;
224+ $ rule = $ rule ->rule ;
225+ }
224226
225227 $ variables = [
226228 'field ' => $ this ->getFieldName ($ ruleTranslationKey , $ field ),
@@ -233,6 +235,30 @@ public function getErrorMessage(Rule $rule, ?string $field = null): string
233235 return $ this ->translator ->translate ("validation_error. {$ ruleTranslationKey }" , ...$ variables );
234236 }
235237
238+ private function getTranslationKey (Rule |FailingRule $ rule ): string
239+ {
240+ $ key = '' ;
241+
242+ if ($ rule instanceof FailingRule && $ rule ->key ) {
243+ $ key .= $ rule ->key ;
244+ }
245+
246+ if ($ rule instanceof FailingRule) {
247+ $ rule = $ rule ->rule ;
248+ }
249+
250+ return str ($ rule ::class)
251+ ->classBasename ()
252+ ->snake ()
253+ ->replaceEvery ([ // those are snake case issues that we manually fix for consistency
254+ 'i_pv6 ' => 'ipv6 ' ,
255+ 'i_pv4 ' => 'ipv4 ' ,
256+ 'reg_ex ' => 'regex ' ,
257+ ])
258+ ->when ($ key !== '' , fn ($ s ) => $ s ->append ('. ' , $ key ))
259+ ->toString ();
260+ }
261+
236262 private function getFieldName (string $ key , ?string $ field = null ): string
237263 {
238264 $ translatedField = $ this ->translator ->translate ("validation_field. {$ key }" );
0 commit comments