Skip to content

Commit 30eb113

Browse files
committed
Redirect before generating a merged property if
* no composition element contains a nested schema (in this case redirect to null) * only one composition element contains a nested schema (in this case redirect to the already created class) If all elements of a composition provide the same type and the property defining the composition has no type definition transfer the type information so the property is typed correctly
1 parent a469d0e commit 30eb113

File tree

10 files changed

+201
-14
lines changed

10 files changed

+201
-14
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ As an optional parameter you can set up a *GeneratorConfiguration* object to con
6666
$generator = new Generator(
6767
(new GeneratorConfiguration())
6868
->setNamespacePrefix('\MyApp\Model')
69-
->setImmutable(true)
69+
->setImmutable(false)
7070
);
7171

7272
$generator

docs/source/gettingStarted.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ As an optional parameter you can set up a *GeneratorConfiguration* object to con
4141
$generator = new Generator(
4242
(new GeneratorConfiguration())
4343
->setNamespacePrefix('\MyApp\Model')
44-
->setImmutable(true)
44+
->setImmutable(false)
4545
);
4646
4747
$generator

src/Model/Property/Property.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,6 @@ public function getTypeHint(bool $outputType = false): string
123123
return $input;
124124
}, $input));
125125

126-
127126
return $input ?? 'mixed';
128127
}
129128

src/PropertyProcessor/ComposedValue/AbstractComposedValueProcessor.php

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ protected function generateValidators(PropertyInterface $property, array $proper
6767
}
6868

6969
$compositionProperties = $this->getCompositionProperties($property, $propertyData);
70+
71+
$this->transferPropertyType($property, $compositionProperties);
72+
7073
$availableAmount = count($compositionProperties);
7174

7275
$property->addValidator(
@@ -143,8 +146,28 @@ protected function getCompositionProperties(PropertyInterface $property, array $
143146
}
144147

145148
/**
146-
* TODO: no nested properties --> cancel, only one --> use original model
149+
* Check if the provided property can inherit a single type from the composition properties.
147150
*
151+
* @param PropertyInterface $property
152+
* @param CompositionPropertyDecorator[] $compositionProperties
153+
*/
154+
private function transferPropertyType(PropertyInterface $property, array $compositionProperties)
155+
{
156+
$compositionPropertyTypes = array_unique(
157+
array_map(
158+
function (CompositionPropertyDecorator $property): string {
159+
return $property->getType();
160+
},
161+
$compositionProperties
162+
)
163+
);
164+
165+
if (count($compositionPropertyTypes) === 1 && !($this instanceof NotProcessor)) {
166+
$property->setType($compositionPropertyTypes[0]);
167+
}
168+
}
169+
170+
/**
148171
* Gather all nested object properties and merge them together into a single merged property
149172
*
150173
* @param PropertyInterface $property
@@ -160,6 +183,15 @@ private function createMergedProperty(
160183
array $compositionProperties,
161184
array $propertyData
162185
): ?PropertyInterface {
186+
$redirectToProperty = $this->redirectMergedProperty($compositionProperties);
187+
if ($redirectToProperty === null || $redirectToProperty instanceof PropertyInterface) {
188+
if ($redirectToProperty) {
189+
$property->addTypeHintDecorator(new CompositionTypeHintDecorator($redirectToProperty));
190+
}
191+
192+
return $redirectToProperty;
193+
}
194+
163195
$mergedClassName = $this->schemaProcessor
164196
->getGeneratorConfiguration()
165197
->getClassNameGenerator()
@@ -196,13 +228,40 @@ private function createMergedProperty(
196228
->setNestedSchema($mergedPropertySchema);
197229
}
198230

231+
/**
232+
* Check if multiple $compositionProperties contain nested schemas. Only in this case a merged property must be
233+
* created. If no nested schemas are detected null will be returned. If only one $compositionProperty contains a
234+
* nested schema the $compositionProperty will be used as a replacement for the merged property.
235+
*
236+
* Returns false if a merged property must be created.
237+
*
238+
* @param CompositionPropertyDecorator[] $compositionProperties
239+
*
240+
* @return PropertyInterface|null|false
241+
*/
242+
private function redirectMergedProperty(array $compositionProperties)
243+
{
244+
$redirectToProperty = null;
245+
foreach ($compositionProperties as $property) {
246+
if ($property->getNestedSchema()) {
247+
if ($redirectToProperty !== null) {
248+
return false;
249+
}
250+
251+
$redirectToProperty = $property;
252+
}
253+
}
254+
255+
return $redirectToProperty;
256+
}
257+
199258
/**
200259
* @param Schema $mergedPropertySchema
201-
* @param PropertyInterface[] $properties
260+
* @param PropertyInterface[] $compositionProperties
202261
*/
203-
protected function transferPropertiesToMergedSchema(Schema $mergedPropertySchema, array $properties): void
262+
private function transferPropertiesToMergedSchema(Schema $mergedPropertySchema, array $compositionProperties): void
204263
{
205-
foreach ($properties as $property) {
264+
foreach ($compositionProperties as $property) {
206265
if (!$property->getNestedSchema()) {
207266
continue;
208267
}

src/PropertyProcessor/Property/AbstractPropertyProcessor.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,10 +189,17 @@ protected function addComposedValueValidator(PropertyInterface $property, array
189189
}
190190

191191
$property->addTypeHintDecorator(new TypeHintTransferDecorator($composedProperty));
192+
193+
if (!$property->getType() && $composedProperty->getType()) {
194+
$property->setType($composedProperty->getType(), $composedProperty->getType(true));
195+
}
192196
}
193197
}
194198

195199
/**
200+
* If the type of a property containing a composition is defined outside of the composition make sure each
201+
* composition which doesn't define a type inherits the type
202+
*
196203
* @param array $propertyData
197204
* @param string $composedValueKeyword
198205
*

src/Templates/Model.phptpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ class {{ class }} implements \PHPModelGenerator\Interfaces\JSONModelInterface
102102
* @return {{ property.getTypeHint(true) }}{% if viewHelper.implicitNull(property) %}|null{% endif %}
103103
*/
104104
public function get{{ viewHelper.ucfirst(property.getAttribute()) }}()
105-
{% if property.getType(true) %}: {% if not property.isRequired() %}?{% endif %}{{ property.getType(true) }}{% endif %}
105+
{% if property.getType(true) %}: {% if viewHelper.implicitNull(property) %}?{% endif %}{{ property.getType(true) }}{% endif %}
106106
{
107107
return $this->{{ property.getAttribute() }};
108108
}

tests/Basic/BasicSchemaGenerationTest.php

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use PHPModelGenerator\Interfaces\SerializationInterface;
1010
use PHPModelGenerator\Model\GeneratorConfiguration;
1111
use PHPModelGenerator\Tests\AbstractPHPModelGeneratorTest;
12+
use ReflectionMethod;
1213

1314
/**
1415
* Class BasicSchemaGenerationTest
@@ -17,18 +18,38 @@
1718
*/
1819
class BasicSchemaGenerationTest extends AbstractPHPModelGeneratorTest
1920
{
20-
public function testGetterAndSetterAreGeneratedForMutableObjects(): void
21+
/**
22+
* @dataProvider implicitNullDataProvider
23+
*
24+
* @param bool $implicitNull
25+
* @param bool $nullable
26+
*/
27+
public function testGetterAndSetterAreGeneratedForMutableObjects(bool $implicitNull): void
2128
{
2229
$className = $this->generateClassFromFile(
2330
'BasicSchema.json',
24-
(new GeneratorConfiguration())->setImmutable(false)
31+
(new GeneratorConfiguration())->setImmutable(false),
32+
false,
33+
$implicitNull
2534
);
2635

2736
$object = new $className(['property' => 'Hello']);
2837

2938
$this->assertTrue(is_callable([$object, 'getProperty']));
3039
$this->assertTrue(is_callable([$object, 'setProperty']));
3140
$this->assertSame('Hello', $object->getProperty());
41+
42+
$this->assertSame($object, $object->setProperty('Bye'));
43+
$this->assertSame('Bye', $object->getProperty());
44+
45+
// test if the property is typed correctly
46+
$returnType = (new ReflectionMethod($object, 'getProperty'))->getReturnType();
47+
$this->assertSame('string', $returnType->getName());
48+
$this->assertSame($implicitNull, $returnType->allowsNull());
49+
50+
$setType = (new ReflectionMethod($object, 'setProperty'))->getParameters()[0]->getType();
51+
$this->assertSame('string', $setType->getName());
52+
$this->assertSame($implicitNull, $setType->allowsNull());
3253
}
3354

3455
public function testGetterAndSetterAreNotGeneratedByDefault(): void

tests/ComposedValue/ComposedAllOfTest.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use PHPModelGenerator\Exception\ValidationException;
66
use PHPModelGenerator\Tests\AbstractPHPModelGeneratorTest;
7+
use ReflectionMethod;
78
use stdClass;
89

910
/**
@@ -55,6 +56,7 @@ public function propertyLevelAllOfSchemaFileDataProvider(): array
5556
{
5657
return [
5758
'Property level composition' => ['ExtendedPropertyDefinition.json'],
59+
'Property level composition 2' => ['ComposedPropertyDefinition.json'],
5860
'Multiple objects' => ['ReferencedObjectSchema.json'],
5961
'Empty all of' => ['EmptyAllOf.json'],
6062
];
@@ -102,6 +104,64 @@ public function testAllOfTypePropertyHasTypeAnnotation(): void
102104
$this->assertCount(4, $this->getGeneratedFiles());
103105
}
104106

107+
/**
108+
* @dataProvider validComposedPropertyDataProvider
109+
*
110+
* @param int|null $propertyValue
111+
*/
112+
public function testComposedPropertyDefinitionWithValidValues(?int $propertyValue): void
113+
{
114+
$className = $this->generateClassFromFile('ComposedPropertyDefinition.json');
115+
116+
$object = new $className(['property' => $propertyValue]);
117+
$this->assertSame($propertyValue, $object->getProperty());
118+
119+
// check if no merged property is created
120+
$this->assertCount(1, $this->getGeneratedFiles());
121+
122+
// test if the property is typed correctly
123+
$returnType = (new ReflectionMethod($object, 'getProperty'))->getReturnType();
124+
$this->assertSame('int', $returnType->getName());
125+
$this->assertTrue($returnType->allowsNull());
126+
}
127+
128+
public function validComposedPropertyDataProvider(): array
129+
{
130+
return [
131+
'null' => [null],
132+
'int 5' => [5],
133+
'int 10' => [10],
134+
];
135+
}
136+
137+
/**
138+
* @dataProvider invalidComposedPropertyDataProvider
139+
*
140+
* @param $propertyValue
141+
* @param string $exceptionMessage
142+
*/
143+
public function testComposedPropertyDefinitionWithInvalidValuesThrowsAnException(
144+
$propertyValue,
145+
string $exceptionMessage
146+
): void {
147+
$this->expectException(ValidationException::class);
148+
$this->expectExceptionMessage($exceptionMessage);
149+
150+
$className = $this->generateClassFromFile('ComposedPropertyDefinition.json');
151+
152+
new $className(['property' => $propertyValue]);
153+
}
154+
155+
public function invalidComposedPropertyDataProvider(): array
156+
{
157+
return [
158+
'one match - int 4' => [4, 'Invalid value for property declined by composition constraint'],
159+
'one match - int 11' => [11, 'Invalid value for property declined by composition constraint'],
160+
'int -1' => [-1, 'Invalid value for property declined by composition constraint'],
161+
'int 20' => [20, 'Invalid value for property declined by composition constraint'],
162+
];
163+
}
164+
105165
/**
106166
* @dataProvider validExtendedPropertyDataProvider
107167
*
@@ -114,6 +174,14 @@ public function testExtendedPropertyDefinitionWithValidValues($propertyValue): v
114174
$object = new $className(['property' => $propertyValue]);
115175
// cast expected to float as an int is casted to an float internally for a number property
116176
$this->assertSame(is_int($propertyValue) ? (float) $propertyValue : $propertyValue, $object->getProperty());
177+
178+
// check if no merged property is created
179+
$this->assertCount(1, $this->getGeneratedFiles());
180+
181+
// test if the property is typed correctly
182+
$returnType = (new ReflectionMethod($object, 'getProperty'))->getReturnType();
183+
$this->assertSame('float', $returnType->getName());
184+
$this->assertTrue($returnType->allowsNull());
117185
}
118186

119187
public function validExtendedPropertyDataProvider(): array

tests/ComposedValue/ComposedAnyOfTest.php

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,21 +123,35 @@ public function testValidProvidedAnyOfTypePropertyIsValid($propertyValue): void
123123
* @param string $schema
124124
* @param string $annotationPattern
125125
*/
126-
public function testAnyOfTypePropertyHasTypeAnnotation(string $schema, string $annotationPattern): void
126+
public function testAnyOfTypePropertyHasTypeAnnotation(string $schema, string $annotationPattern, int $generatedClasses): void
127127
{
128128
$className = $this->generateClassFromFile($schema);
129129

130130
$object = new $className([]);
131131
$this->assertRegExp($annotationPattern, $this->getPropertyType($object, 'property'));
132132
$this->assertRegExp($annotationPattern, $this->getMethodReturnType($object, 'getProperty'));
133+
134+
$this->assertCount($generatedClasses, $this->getGeneratedFiles());
133135
}
134136

135137
public function annotationDataProvider(): array
136138
{
137139
return [
138-
'Multiple scalar types' => ['AnyOfType.json', '/string\|int\|bool/'],
139-
'Object with scalar type' => ['ReferencedObjectSchema.json', '/string\|Composed[\w]*_Merged_[\w]*/'],
140-
'Multiple objects' => ['ReferencedObjectSchema2.json', '/ComposedAnyOfTest[\w]*_Merged_[\w]*/']
140+
'Multiple scalar types (no merged property)' => [
141+
'AnyOfType.json',
142+
'/string\|int\|bool\|null/',
143+
1,
144+
],
145+
'Object with scalar type (no merged property - redirect to generated object)' => [
146+
'ReferencedObjectSchema.json',
147+
'/string\|ComposedAnyOfTest[\w]*Property[\w]*\|null/',
148+
2,
149+
],
150+
'Multiple objects (merged property created)' => [
151+
'ReferencedObjectSchema2.json',
152+
'/ComposedAnyOfTest[\w]*_Merged_[\w]*\|null/',
153+
4,
154+
],
141155
];
142156
}
143157

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"type": "object",
3+
"properties": {
4+
"property": {
5+
"allOf": [
6+
{
7+
"type": "integer",
8+
"minimum": 0,
9+
"maximum": 10
10+
},
11+
{
12+
"type": "integer",
13+
"minimum": 5,
14+
"maximum": 15
15+
}
16+
]
17+
}
18+
}
19+
}

0 commit comments

Comments
 (0)