Skip to content

Commit 7bebdc2

Browse files
committed
Passthrough a value through the ObjectInstantiationDecorator instrad of adding a validation error to keep validation order.
Instead of throwing an invalid type, requires object got object error in case of an invalid class execute an instanceof validation. Add nested object schema test cases Schema Dependency documentation
1 parent 477ed38 commit 7bebdc2

File tree

9 files changed

+289
-34
lines changed

9 files changed

+289
-34
lines changed

docs/source/complexTypes/object.rst

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,64 @@ As stated above the dependency declaration is not bidirectional. If the presence
343343
Schema Dependencies
344344
^^^^^^^^^^^^^^^^^^^
345345

346-
Schema dependencies are currently not supported.
346+
Schema dependencies allow you to define a schema which must be fulfilled if a given property is present. The schema provided for the property must be either an object schema, a composition schema or a reference to an object schema.
347+
348+
.. code-block:: json
349+
350+
{
351+
"type": "object",
352+
"$id": "CreditCardOwner"
353+
"properties": {
354+
"credit_card": {
355+
"type": "integer"
356+
}
357+
},
358+
"dependencies": {
359+
"credit_card": {
360+
"properties": {
361+
"billing_address": {
362+
"type": "string"
363+
},
364+
"date_of_birth": {
365+
"type": "string"
366+
}
367+
},
368+
"required": [
369+
"date_of_birth"
370+
]
371+
}
372+
}
373+
}
374+
375+
The properties of the dependant schema will be transferred to the base model during the model generation process. If the property which defines the dependency isn't present they will not be required by the base model.
376+
377+
Generated interface:
378+
379+
.. code-block:: php
380+
381+
// class CreditCardOwner
382+
// base properties
383+
public function setCreditCard(?int $creditCard): self;
384+
public function getCreditCard(): ?int;
385+
386+
// inherited properties
387+
public function setBillingAddress($billingAddress): self;
388+
public function getBillingAddress();
389+
public function setDateOfBirth($dateOfBirth): self;
390+
public function getDateOfBirth();
391+
392+
.. hint::
393+
394+
Basically this means your base object gets getters and setters for the additional properties transferred from the schema dependency but this getters and setters won't perform any validation. If you require type checks and validations performed on the properties define them in your main schema as not required properties and require them as a property dependency.
395+
396+
Possible exceptions:
397+
398+
.. code-block:: none
399+
400+
Invalid schema which is dependant on credit_card:
401+
- Missing required value for date_of_birth
402+
403+
Multiple violations against the schema dependency may be included.
347404

348405
Pattern Properties
349406
------------------
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace PHPModelGenerator\Model\Validator;
6+
7+
use PHPModelGenerator\Model\Property\PropertyInterface;
8+
9+
/**
10+
* Class InstanceOfValidator
11+
*
12+
* @package PHPModelGenerator\Model\Validator
13+
*/
14+
class InstanceOfValidator extends PropertyValidator
15+
{
16+
/**
17+
* InstanceOfValidator constructor.
18+
*
19+
* @param PropertyInterface $property
20+
*/
21+
public function __construct(PropertyInterface $property)
22+
{
23+
parent::__construct(
24+
sprintf('is_object($value) && !($value instanceof %s)', $property->getType()),
25+
sprintf(
26+
'Invalid class for %s. Requires %s, got " . get_class($value) . "',
27+
$property->getName(),
28+
$property->getType()
29+
)
30+
);
31+
}
32+
}

src/PropertyProcessor/Property/AbstractPropertyProcessor.php

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,10 @@
77
use PHPModelGenerator\Exception\SchemaException;
88
use PHPModelGenerator\Model\Property\PropertyInterface;
99
use PHPModelGenerator\Model\Schema;
10-
use PHPModelGenerator\Model\Validator;
1110
use PHPModelGenerator\Model\Validator\PropertyDependencyValidator;
1211
use PHPModelGenerator\Model\Validator\PropertyValidator;
1312
use PHPModelGenerator\Model\Validator\RequiredPropertyValidator;
1413
use PHPModelGenerator\Model\Validator\SchemaDependencyValidator;
15-
use PHPModelGenerator\Model\Validator\TypeCheckValidator;
1614
use PHPModelGenerator\PropertyProcessor\ComposedValueProcessorFactory;
1715
use PHPModelGenerator\PropertyProcessor\Decorator\SchemaNamespaceTransferDecorator;
1816
use PHPModelGenerator\PropertyProcessor\Decorator\TypeHint\TypeHintTransferDecorator;
@@ -149,28 +147,16 @@ function ($dependency, $index) use (&$propertyDependency): void {
149147
private function transferDependendPropertiesToBaseSchema(Schema $dependencySchema): void
150148
{
151149
foreach ($dependencySchema->getProperties() as $property) {
152-
$typeCheckValidator = null;
153-
154-
$transferProperty = (clone $property)
155-
->setRequired(false)
156-
->filterValidators(function (Validator $validator) use (&$typeCheckValidator): bool {
157-
if (is_a($validator->getValidator(), TypeCheckValidator::class)) {
158-
$typeCheckValidator = $validator->getValidator();
159-
150+
$this->schema->addProperty(
151+
// validators and types must not be transferred as any value is acceptable for the property if the
152+
// property defining the dependency isn't present
153+
(clone $property)
154+
->setRequired(false)
155+
->setType('')
156+
->filterValidators(function (): bool {
160157
return false;
161-
}
162-
163-
return !is_a($validator->getValidator(), RequiredPropertyValidator::class);
164-
});
165-
166-
// rebuild the type check validator as the origin property may be required but the transferred property
167-
// must be optional
168-
if ($typeCheckValidator !== null) {
169-
preg_match('/is_([^(]+)/', $typeCheckValidator->getCheck(), $matches);
170-
$transferProperty->addValidator(new TypeCheckValidator($matches[1], $transferProperty));
171-
}
172-
173-
$this->schema->addProperty($transferProperty);
158+
})
159+
);
174160
}
175161
}
176162

@@ -249,7 +235,7 @@ protected function inheritPropertyType(array $propertyData, string $composedValu
249235
}
250236

251237
/**
252-
* Inherit the type pf a property into all composed components of a conditional composition
238+
* Inherit the type of a property into all composed components of a conditional composition
253239
*
254240
* @param array $propertyData
255241
*

src/PropertyProcessor/Property/ObjectProcessor.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use PHPModelGenerator\Exception\SchemaException;
88
use PHPModelGenerator\Model\Property\PropertyInterface;
9+
use PHPModelGenerator\Model\Validator\InstanceOfValidator;
910
use PHPModelGenerator\PropertyProcessor\Decorator\Property\ObjectInstantiationDecorator;
1011
use PHPModelGenerator\PropertyProcessor\Decorator\SchemaNamespaceTransferDecorator;
1112

@@ -66,6 +67,6 @@ public function process(string $propertyName, array $propertyData): PropertyInte
6667
->setType($schema->getClassName())
6768
->setNestedSchema($schema);
6869

69-
return $property;
70+
return $property->addValidator(new InstanceOfValidator($property), 3);
7071
}
7172
}

src/Templates/Decorator/ObjectInstantiationDecorator.phptpl

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,5 @@
33
$param
44
{% if generatorConfiguration.collectErrors() %}, $this->errorRegistry{% endif %}
55
)
6-
: (function () use ($param) {
7-
{{ viewHelper.validationError(exceptionMessage) }}
8-
})()
6+
: $param
97
)

tests/Basic/SchemaDependencyTest.php

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,12 @@ public function validSchemaDependencyDataProvider(): array
3232
{
3333
return [
3434
'No properties provided' => [[]],
35+
'Only dependant property provided with different type' => [['billing_address' => true]],
3536
'Only dependant property provided 1' => [['billing_address' => '555 Debitors Lane']],
3637
'Only dependant property provided 2' => [['date_of_birth' => '01-01-1990']],
3738
'Only dependant property provided 3' => [['billing_address' => '555 Debitors Lane', 'date_of_birth' => '01-01-1990']],
39+
'Dependant property nulled 1' => [['billing_address' => null]],
40+
'Dependant property nulled 2' => [['date_of_birth' => null]],
3841
'All properties provided' => [['credit_card' => 12345, 'billing_address' => '555 Debitors Lane', 'date_of_birth' => '01-01-1990']],
3942
'Only required properties provided' => [['credit_card' => 12345, 'date_of_birth' => '01-01-1990']],
4043
];
@@ -108,9 +111,12 @@ public function validSchemaDependencyReferenceDataProvider(): array
108111
{
109112
return [
110113
'No properties provided' => [[]],
114+
'Only dependant property provided with different type' => [['name' => true]],
111115
'Only dependant property provided 1' => [['name' => 'Hannes']],
112116
'Only dependant property provided 2' => [['age' => 42]],
113117
'Only dependant property provided 3' => [['name' => 'Hannes', 'age' => 42]],
118+
'Dependant property nulled 1' => [['name' => null]],
119+
'Dependant property nulled 2' => [['age' => null]],
114120
'All properties provided' => [['credit_card' => 12345, 'name' => 'Hannes', 'age' => 42]],
115121
'Only required properties provided' => [['credit_card' => 12345, 'name' => 'Hannes']],
116122
];
@@ -184,9 +190,12 @@ public function validSchemaDependencyCompositionDataProvider(): array
184190
{
185191
return [
186192
'No properties provided' => [[]],
193+
'Only dependant property provided with different type' => [['name' => 42]],
187194
'Only dependant property provided 1' => [['name' => 'Hannes']],
188195
'Only dependant property provided 2' => [['age' => 42]],
189196
'Only dependant property provided 3' => [['name' => 'Hannes', 'age' => 42]],
197+
'Dependant property nulled 1' => [['name' => null]],
198+
'Dependant property nulled 2' => [['age' => null]],
190199
'All properties provided' => [['credit_card' => 12345, 'name' => 'Hannes', 'age' => 42]],
191200
];
192201
}
@@ -240,8 +249,9 @@ public function invalidSchemaDependencyCompositionDataProvider(): array
240249
\* Invalid type for name. Requires string, got NULL
241250
- Composition element #2: Valid
242251
ERROR
252+
,
243253
],
244-
'invalid data type' => [
254+
'invalid data type' => [
245255
['credit_card' => 12345, 'name' => false, 'age' => 42],
246256
<<<ERROR
247257
Invalid schema which is dependant on credit_card:
@@ -251,7 +261,116 @@ public function invalidSchemaDependencyCompositionDataProvider(): array
251261
\* Invalid type for name. Requires string, got boolean
252262
- Composition element #2: Valid
253263
ERROR
254-
]
264+
,
265+
],
266+
];
267+
}
268+
269+
/**
270+
* @dataProvider validSchemaDependencyNestedObejctDataProvider
271+
*
272+
* @param array $propertyValue
273+
*/
274+
public function testSchemaDependencyNestedObject(array $propertyValue): void
275+
{
276+
$className = $this->generateClassFromFile('NestedObjectSchemaDependency.json');
277+
278+
$object = new $className($propertyValue);
279+
$this->assertSame($propertyValue['credit_card'] ?? null, $object->getCreditCard());
280+
$this->assertSame($propertyValue['billing_address'] ?? null, $object->getBillingAddress());
281+
282+
if (isset($propertyValue['owner'])) {
283+
$this->assertSame($propertyValue['owner']['name'] ?? null, $object->getOwner()->getName());
284+
$this->assertSame($propertyValue['owner']['age'] ?? null, $object->getOwner()->getAge());
285+
} else {
286+
$this->assertNull($object->getOwner());
287+
}
288+
}
289+
290+
public function validSchemaDependencyNestedObejctDataProvider(): array
291+
{
292+
return [
293+
'No properties provided' => [[]],
294+
'Only dependant properties provided with different type' => [['billing_address' => 42]],
295+
'Only dependant properties provided 1' => [['billing_address' => '555 Debitors Lane']],
296+
'Only dependant properties provided 2' => [['owner' => ['name' => 'Hannes', 'age' => 42]]],
297+
'Only dependant properties provided 3' => [[
298+
'owner' => ['name' => 'Hannes', 'age' => 42],
299+
'billing_address' => '555 Debitors Lane',
300+
]],
301+
'Dependant property nulled 1' => [['billing_address' => null]],
302+
'Dependant property nulled 2' => [['owner' => null]],
303+
'All properties provided' => [[
304+
'credit_card' => 12345,
305+
'owner' => ['name' => 'Hannes', 'age' => 42],
306+
'billing_address' => '555 Debitors Lane',
307+
]],
308+
'Only required properties provided' => [[
309+
'credit_card' => 12345,
310+
'owner' => ['name' => 'Hannes'],
311+
'billing_address' => '555 Debitors Lane',
312+
]],
313+
];
314+
}
315+
316+
/**
317+
* @dataProvider invalidSchemaDependencyNestedObjectDataProvider
318+
*
319+
* @param array $propertyValue
320+
* @param string $message
321+
*/
322+
public function testInvalidSchemaDependencyNestedObject(
323+
array $propertyValue,
324+
string $message
325+
): void {
326+
$this->expectException(ErrorRegistryException::class);
327+
$this->expectExceptionMessageMatches("/$message/m");
328+
329+
$className = $this->generateClassFromFile(
330+
'NestedObjectSchemaDependency.json',
331+
(new GeneratorConfiguration())->setCollectErrors(true)
332+
);
333+
334+
new $className($propertyValue);
335+
}
336+
337+
public function invalidSchemaDependencyNestedObjectDataProvider(): array
338+
{
339+
return [
340+
'required attribute not provided 1' => [
341+
['credit_card' => 12345],
342+
<<<ERROR
343+
Invalid schema which is dependant on credit_card:
344+
- Missing required value for billing_address
345+
- Invalid type for billing_address. Requires string, got NULL
346+
- Missing required value for owner
347+
- Invalid type for owner. Requires object, got NULL
348+
ERROR
349+
],
350+
'required attribute not provided 2' => [
351+
['credit_card' => 12345, 'owner' => ['age' => 42]],
352+
<<<ERROR
353+
Invalid schema which is dependant on credit_card:
354+
- Missing required value for billing_address
355+
- Invalid type for billing_address. Requires string, got NULL
356+
- Missing required value for name
357+
- Invalid type for name. Requires string, got NULL
358+
ERROR
359+
],
360+
'invalid data type' => [
361+
[
362+
'credit_card' => 12345,
363+
'owner' => [
364+
'name' => false,
365+
'age' => 42,
366+
],
367+
'billing_address' => '555 Debitors Lane',
368+
],
369+
<<<ERROR
370+
Invalid schema which is dependant on credit_card:
371+
- Invalid type for name. Requires string, got boolean
372+
ERROR
373+
],
255374
];
256375
}
257376
}

tests/Objects/ObjectPropertyTest.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,27 @@ public function invalidPropertyTypeDataProvider(): array
8181
'bool' => [true],
8282
'float' => [0.92],
8383
'int' => [2],
84-
'object' => [new stdClass()],
8584
'string' => ['1']
8685
];
8786
}
8887

88+
/**
89+
* @throws FileSystemException
90+
* @throws RenderException
91+
* @throws SchemaException
92+
*/
93+
public function testInvalidPropertyObjectClassThrowsAnException(): void
94+
{
95+
$this->expectException(ValidationException::class);
96+
$this->expectExceptionMessageMatches(
97+
'/Invalid class for property. Requires ObjectPropertyTest_.*, got stdClass/'
98+
);
99+
100+
$className = $this->generateClassFromFile('ObjectProperty.json');
101+
102+
new $className(['property' => new stdClass()]);
103+
}
104+
89105
/**
90106
* @dataProvider validInputProviderObjectLevelValidation
91107
*

0 commit comments

Comments
 (0)