Skip to content

Commit 43da702

Browse files
committed
Refactor validation mappings and tests
* Adjust validation for nested and partial input objects. * Refactor metadata handling in `InputValidator`. * Add test for `partialInputObjectsCollectionValidation`. * Remove test `testOnlyPassedFieldsValidated`. * Update validation constraints in YAML configuration files.
1 parent cd19c3e commit 43da702

File tree

8 files changed

+127
-47
lines changed

8 files changed

+127
-47
lines changed

src/Validator/InputValidator.php

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -124,13 +124,27 @@ private function createValidator(MetadataFactory $metadataFactory): ValidatorInt
124124
return $builder->getValidator();
125125
}
126126

127+
private function getMetadata(ValidationNode $rootObject): ObjectMetadata
128+
{
129+
// Return existing metadata if present
130+
if ($this->metadataFactory->hasMetadataFor($rootObject)) {
131+
return $this->metadataFactory->getMetadataFor($rootObject);
132+
}
133+
134+
// Create new metadata and add it to the factory
135+
$metadata = new ObjectMetadata($rootObject);
136+
$this->metadataFactory->addMetadata($metadata);
137+
138+
return $metadata;
139+
}
140+
127141
/**
128142
* Creates a composition of ValidationNode objects from args
129143
* and simultaneously applies to them validation constraints.
130144
*/
131145
private function buildValidationTree(ValidationNode $rootObject, iterable $fields, array $classValidation, array $inputData): ValidationNode
132146
{
133-
$metadata = new ObjectMetadata($rootObject);
147+
$metadata = $this->getMetadata($rootObject);
134148

135149
if (!empty($classValidation)) {
136150
$this->applyClassValidation($metadata, $classValidation);
@@ -140,13 +154,7 @@ private function buildValidationTree(ValidationNode $rootObject, iterable $field
140154
$property = $arg['name'] ?? $name;
141155
$config = static::normalizeConfig($arg['validation'] ?? []);
142156

143-
if (!array_key_exists($property, $inputData)) {
144-
// This field was not provided in the inputData. Do not attempt to validate it.
145-
continue;
146-
}
147-
148157
if (isset($config['cascade']) && isset($inputData[$property])) {
149-
$groups = $config['cascade'];
150158
$argType = $this->unclosure($arg['type']);
151159

152160
/** @var ObjectType|InputObjectType $type */
@@ -159,18 +167,29 @@ private function buildValidationTree(ValidationNode $rootObject, iterable $field
159167
}
160168

161169
$valid = new Valid();
170+
$groups = $config['cascade'];
162171

163172
if (!empty($groups)) {
164173
$valid->groups = $groups;
165174
}
166175

176+
// Apply the Assert/Valid constraint for a recursive validation.
177+
// For more details see https://symfony.com/doc/current/reference/constraints/Valid.html
167178
$metadata->addPropertyConstraint($property, $valid);
179+
180+
// Skip the rest as the validation was delegated to the nested object.
181+
continue;
168182
} else {
169183
$rootObject->$property = $inputData[$property] ?? null;
170184
}
171185

186+
if ($metadata->hasPropertyMetadata($property)) {
187+
continue;
188+
}
189+
172190
$config = static::normalizeConfig($config);
173191

192+
// Apply validation constraints for the property
174193
foreach ($config as $key => $value) {
175194
switch ($key) {
176195
case 'link':
@@ -200,17 +219,16 @@ private function buildValidationTree(ValidationNode $rootObject, iterable $field
200219
}
201220

202221
break;
203-
case 'constraints':
222+
case 'constraints': // Add constraint from the yml config
204223
$metadata->addPropertyConstraints($property, $value);
205224
break;
206225
case 'cascade':
226+
// Cascade validation was already handled recursively.
207227
break;
208228
}
209229
}
210230
}
211231

212-
$this->metadataFactory->addMetadata($metadata);
213-
214232
return $rootObject;
215233
}
216234

tests/Functional/App/config/validator/mapping/Address.types.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@ Address:
1414
- Choice:
1515
groups: ['group1']
1616
choices: ['New York', 'Berlin', 'Tokyo']
17+
country:
18+
type: Country
19+
validation: cascade
1720
zipCode:
1821
type: Int!
1922
validation:
2023
- Expression: "service_validator.isZipCodeValid(value)"
2124
period:
22-
type: Period!
25+
type: Period
2326
validation: cascade
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Country:
2+
type: input-object
3+
config:
4+
fields:
5+
name:
6+
type: String
7+
validation:
8+
- NotBlank:
9+
allowNull: true
10+
officialLanguage:
11+
type: String
12+
validation:
13+
- Choice: ['en', 'de', 'fr']

tests/Functional/App/config/validator/mapping/Mutation.types.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -139,10 +139,10 @@ Mutation:
139139
cascade:
140140
groups: ['group2']
141141

142-
onlyPassedFieldsValidation:
142+
partialInputObjectsCollectionValidation:
143143
type: Boolean
144-
resolve: '@=m("mutation_mock", args)'
144+
resolve: "@=m('mutation_mock', args)"
145145
args:
146-
person:
146+
addresses:
147+
type: '[Address]'
147148
validation: cascade
148-
type: Person!

tests/Functional/App/config/validator/mapping/Period.types.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ Period:
33
config:
44
fields:
55
startDate:
6-
type: String!
6+
type: String
77
validation:
88
- Date: ~
99
- Overblog\GraphQLBundle\Tests\Functional\App\Validator\AtLeastOneOf:
@@ -12,7 +12,7 @@ Period:
1212
message: "Year should be GreaterThanOrEqual -100."
1313
includeInternalMessages: false
1414
endDate:
15-
type: String!
15+
type: String
1616
validation:
1717
- Expression: "this.getParent().getName() === 'Address'"
1818
- Date: ~

tests/Functional/App/config/validator/mapping/Person.types.yml

Lines changed: 0 additions & 14 deletions
This file was deleted.

tests/Functional/Validator/ExpectedErrors.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,34 @@ final class ExpectedErrors
7979
],
8080
];
8181

82+
public const PARTIAL_INPUT_OBJECTS_COLLECTION = [
83+
'message' => 'validation',
84+
'locations' => [['line' => 3, 'column' => 17]],
85+
'path' => ['partialInputObjectsCollectionValidation'],
86+
'extensions' => [
87+
'validation' => [
88+
'addresses[0].country.officialLanguage' => [
89+
0 => [
90+
'message' => 'The value you selected is not a valid choice.',
91+
'code' => '8e179f1b-97aa-4560-a02f-2a8b42e49df7'
92+
],
93+
],
94+
'addresses[1].country.name' => [
95+
0 => [
96+
'message' => 'This value should not be blank.',
97+
'code' => 'c1051bb4-d103-4f74-8988-acbcafc7fdc3'
98+
],
99+
],
100+
'addresses[1].period.endDate' => [
101+
0 => [
102+
'message' => 'This value should be greater than "2000-01-01".',
103+
'code' => '778b7ae0-84d3-481a-9dec-35fdb64b1d78'
104+
]
105+
]
106+
]
107+
]
108+
];
109+
82110
public static function simpleValidation(string $fieldName): array
83111
{
84112
return [

tests/Functional/Validator/InputValidatorTest.php

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -87,22 +87,6 @@ public function testLinkedConstraintsValidationPasses(): void
8787
$this->assertTrue($result['data']['linkedConstraintsValidation']);
8888
}
8989

90-
public function testOnlyPassedFieldsValidated(): void
91-
{
92-
$query = '
93-
mutation {
94-
onlyPassedFieldsValidation(
95-
person: { firstName: "Joe" }
96-
)
97-
}
98-
';
99-
100-
$result = $this->executeGraphQLRequest($query);
101-
102-
$this->assertTrue(empty($result['errors']));
103-
$this->assertTrue($result['data']['onlyPassedFieldsValidation']);
104-
}
105-
10690
public function testLinkedConstraintsValidationFails(): void
10791
{
10892
$query = '
@@ -393,4 +377,52 @@ public function testAutoValidationAutoThrowWithGroupsFails(): void
393377
$this->assertSame(ExpectedErrors::cascadeWithGroups('autoValidationAutoThrowWithGroups'), $result['errors'][0]);
394378
$this->assertNull($result['data']['autoValidationAutoThrowWithGroups']);
395379
}
380+
381+
public function testPartialInputObjectsCollectionValidation(): void
382+
{
383+
$query = '
384+
mutation {
385+
partialInputObjectsCollectionValidation(
386+
addresses: [
387+
{
388+
street: "Washington Street"
389+
city: "Berlin"
390+
zipCode: 10000
391+
# Country is present, but the language is invalid
392+
country: {
393+
name: "Germany"
394+
officialLanguage: "ru"
395+
}
396+
# Period is completely missing, skip validation
397+
},
398+
{
399+
street: "Washington Street"
400+
city: "New York"
401+
zipCode: 10000
402+
# Country is partially present
403+
country: {
404+
name: "" # Name should not be blank
405+
# language is missing
406+
}
407+
period: {
408+
startDate: "2000-01-01"
409+
endDate: "1990-01-01"
410+
}
411+
},
412+
{
413+
street: "Washington Street"
414+
city: "New York"
415+
zipCode: 10000
416+
country: {} # Empty input object, skip validation
417+
period: {} # Empty input object, skip validation
418+
}
419+
]
420+
)
421+
}
422+
';
423+
424+
$result = $this->executeGraphQLRequest($query);
425+
$this->assertSame(ExpectedErrors::PARTIAL_INPUT_OBJECTS_COLLECTION, $result['errors'][0]);
426+
$this->assertNull($result['data']['partialInputObjectsCollectionValidation']);
427+
}
396428
}

0 commit comments

Comments
 (0)