diff --git a/src/Resolvers/DataValidationMessagesAndAttributesResolver.php b/src/Resolvers/DataValidationMessagesAndAttributesResolver.php index 2770ab03..00113d44 100644 --- a/src/Resolvers/DataValidationMessagesAndAttributesResolver.php +++ b/src/Resolvers/DataValidationMessagesAndAttributesResolver.php @@ -9,16 +9,16 @@ class DataValidationMessagesAndAttributesResolver { public function __construct( - protected DataConfig $dataConfig, + protected DataConfig $dataConfig, protected DataMorphClassResolver $dataMorphClassResolver, ) { } public function execute( - string $class, - array $fullPayload, + string $class, + array $fullPayload, ValidationPath $path, - array $nestingChain = [], + array $nestingChain = [], ): array { $dataClass = $this->dataConfig->getDataClass($class); @@ -74,15 +74,44 @@ public function execute( continue; } - $collected = $this->execute( - $dataProperty->type->dataClass, - $fullPayload, - $propertyPath->property('*'), - [...$nestingChain, $dataProperty->type->dataClass], - ); + $collectedDataClass = $this->dataConfig->getDataClass($dataProperty->type->dataClass); + + if ($collectedDataClass->isAbstract && $collectedDataClass->propertyMorphable) { + $items = Arr::get($fullPayload, $propertyPath->get(), []); + + foreach ($items as $index => $item) { + $morphedClass = $this->dataMorphClassResolver->execute( + $collectedDataClass, + [$item], + ); - $messages[] = $collected['messages']; - $attributes[] = $collected['attributes']; + if (! $morphedClass) { + $morphedClass = $dataProperty->type->dataClass; + } + + $collected = $this->execute( + $morphedClass, + $fullPayload, + $propertyPath->property($index), + [...$nestingChain, $dataProperty->type->dataClass, $morphedClass], + ); + + $messages[] = $collected['messages']; + $attributes[] = $collected['attributes']; + } + + } else { + $collected = $this->execute( + $dataProperty->type->dataClass, + $fullPayload, + $propertyPath->property('*'), + [...$nestingChain, $dataProperty->type->dataClass], + ); + + $messages[] = $collected['messages']; + $attributes[] = $collected['attributes']; + + } } } diff --git a/tests/ValidationTest.php b/tests/ValidationTest.php index 8184308c..5d241a7d 100644 --- a/tests/ValidationTest.php +++ b/tests/ValidationTest.php @@ -2549,11 +2549,11 @@ class TestDataWithMergedRuleset extends Data { public function __construct( #[Max(10)] - public string $array_rules, + public string $array_rules, #[Max(10)] - public string $string_rules, + public string $string_rules, #[WithoutValidation] - public string $without_validation, + public string $without_validation, public TestNestedDataWithMergedRules $nested ) { } @@ -2613,7 +2613,7 @@ enum TestValidationPropertyMorphableEnum: string { case A = 'a'; case B = 'b'; - }; + } abstract class TestValidationAbstractPropertyMorphableData extends Data implements PropertyMorphableData { @@ -2699,7 +2699,7 @@ public function __construct( public ?DataCollection $nestedCollection, ) { } - }; + } DataValidationAsserter::for(TestValidationNestedPropertyMorphableData::class) ->assertErrors([ @@ -2739,8 +2739,8 @@ public function __construct( #[PropertyForMorph] public TestValidationPropertyMorphableEnum $variant, #[Max(1)] - public int $abstract_integer, - public string $abstract_string, + public int $abstract_integer, + public string $abstract_string, ) { } @@ -2770,10 +2770,10 @@ public static function attributes() class TestValidationCustomMessagePropertyMorphableDataA extends TestValidationCustomMessageAbstractPropertyMorphableData { public function __construct( - int $abstract_integer, - string $abstract_string, + int $abstract_integer, + string $abstract_string, #[Max(1)] - public int $concrete_integer, + public int $concrete_integer, public string $concrete_string, ) { parent::__construct(TestValidationPropertyMorphableEnum::A, $abstract_integer, $abstract_string); @@ -2796,7 +2796,7 @@ public static function attributes() } } - DataValidationAsserter::for(TestValidationCustomMessageAbstractPropertyMorphableData::class) + DataValidationAsserter::for(TestValidationCustomMessagePropertyMorphableDataA::class) ->assertErrors([ 'variant' => 'a', 'abstract_integer' => 2, @@ -2808,4 +2808,150 @@ public static function attributes() 'abstract_string' => ['The [Abstract String] field is required.'], ]); }); + + it('can validate collections of property-morphable data with custom messages and attributes', function () { + class TestCustomMessagesInCollectionOfPropertyMorphableData extends Data + { + public function __construct( + + /** + * @var array + */ + public array $items, + ) { + } + } + + abstract class TestCustomMessageOfPropertyMorphableData extends Data implements PropertyMorphableData + { + public function __construct( + #[PropertyForMorph] + public TestValidationPropertyMorphableEnum $variant, + #[Max(1)] + public int $abstract_integer, + public string $abstract_string, + ) { + } + + public static function morph(array $properties): ?string + { + return match ($properties['variant']) { + TestValidationPropertyMorphableEnum::A => TestCustomMessagePropertyMorphableDataA::class, + TestValidationPropertyMorphableEnum::B => TestCustomMessagePropertyMorphableDataB::class, + default => null, + }; + } + + public static function messages() + { + return [ + 'abstract_integer.max' => 'Abstract class integer test message.', + ]; + } + + public static function attributes() + { + return [ + 'abstract_string' => '[Abstract String]', + ]; + } + } + + class TestCustomMessagePropertyMorphableDataA extends TestCustomMessageOfPropertyMorphableData + { + public function __construct( + int $abstract_integer, + string $abstract_string, + #[Max(1)] + public int $concrete_integer, + public string $concrete_string, + ) { + parent::__construct(TestValidationPropertyMorphableEnum::A, $abstract_integer, $abstract_string); + } + + public static function messages() + { + return [ + ...parent::messages(), + 'concrete_integer.max' => 'Concrete class A integer test message.', + ]; + } + + public static function attributes() + { + return [ + ...parent::attributes(), + 'concrete_string' => '[Concrete String A]', + ]; + } + } + + class TestCustomMessagePropertyMorphableDataB extends TestCustomMessageOfPropertyMorphableData + { + public function __construct( + int $abstract_integer, + string $abstract_string, + #[Max(1)] + public int $concrete_integer, + public string $concrete_string, + ) { + parent::__construct(TestValidationPropertyMorphableEnum::B, $abstract_integer, $abstract_string); + } + + public static function messages() + { + return [ + ...parent::messages(), + 'abstract_integer.max' => 'Concrete Class B override of Abstract class integer test message.', + 'concrete_integer.max' => 'Concrete class B integer test message.', + ]; + } + + public static function attributes() + { + return [ + ...parent::attributes(), + 'concrete_string' => '[Concrete String B]', + ]; + } + } + + + DataValidationAsserter::for(TestCustomMessagesInCollectionOfPropertyMorphableData::class) + ->assertErrors([ + 'items' => [ + [ + 'variant' => 'a', + 'abstract_integer' => 2, + 'concrete_integer' => 2, + ], + [ + 'variant' => 'invalid', + 'abstract_integer' => 2, + 'concrete_integer' => 2, + ], + [ + 'variant' => 'b', + 'abstract_integer' => 2, + 'concrete_integer' => 2, + ], + ]], [ + 'items.0.concrete_integer' => ['Concrete class A integer test message.'], + 'items.0.concrete_string' => ['The [Concrete String A] field is required.'], + 'items.0.abstract_integer' => ['Abstract class integer test message.'], + 'items.0.abstract_string' => ['The [Abstract String] field is required.'], + + 'items.1.variant' => [ + 'The selected items.1.variant is invalid.', + 'The selected items.1.variant is invalid for morph.', + ], + 'items.1.abstract_integer' => ['Abstract class integer test message.'], + 'items.1.abstract_string' => ['The [Abstract String] field is required.'], + + 'items.2.concrete_integer' => ['Concrete class B integer test message.'], + 'items.2.concrete_string' => ['The [Concrete String B] field is required.'], + 'items.2.abstract_integer' => ['Concrete Class B override of Abstract class integer test message.'], + 'items.2.abstract_string' => ['The [Abstract String] field is required.'], + ]); + }); });