Skip to content

Commit 4e9ede1

Browse files
authored
Merge pull request #13 from wol-soft/OptimizeMergedProperties
Optimize merged properties
2 parents 5a00115 + 65d50fc commit 4e9ede1

File tree

16 files changed

+429
-46
lines changed

16 files changed

+429
-46
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/combinedSchemas/mergedProperty.rst

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
Merged Property
22
===============
33

4-
If multiple subschemas are combined with `oneOf`, `anyOf` or `allOf` and the subschemas contain multiple nested objects all properties of the nested objects will be merged together in a single object.
5-
For example we combine two objects with `allOf`:
4+
If multiple subschemas are combined with `oneOf`, `anyOf` or `allOf` and the subschemas contain multiple nested objects all properties of the nested objects will be merged together in a single object representing all composition elements.
5+
6+
If the composition is used on object level no merged property will be generated as the object itself works as a merged property holding all properties of the nested objects from the composition subschemas.
7+
8+
For example we combine two objects with `allOf` for an object property:
69

710
.. code-block:: json
811
@@ -58,3 +61,44 @@ Generated interface:
5861
public function setName(?string $name): self
5962
public function getAge(): ?int
6063
public function setAge(?int $name): self
64+
65+
If your composition is defined on object level the object will gain access to all properties of the combined schemas:
66+
67+
.. code-block:: json
68+
69+
{
70+
"$id": "CEO",
71+
"type": "object",
72+
"allOf": [
73+
{
74+
"type": "object",
75+
"properties": {
76+
"name": {
77+
"type": "string"
78+
}
79+
}
80+
},
81+
{
82+
"type": "object",
83+
"properties": {
84+
"age": {
85+
"type": "integer"
86+
}
87+
}
88+
}
89+
]
90+
}
91+
92+
This schema will generate three classes as no merged property is created. The main class will be `CEO` and two classes will be generated to validate the subschemas combined with the `allOf` independent:
93+
94+
* Ceo.php
95+
* Ceo_Ceo5e4a82e39edc3.php
96+
* Ceo_Ceo5e4a82e39fe37.php
97+
98+
.. code-block:: php
99+
100+
# class CEO
101+
public function getName(): ?string
102+
public function setName(?string $name): self
103+
public function getAge(): ?int
104+
public function setAge(?int $name): self

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/BaseProperty.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace PHPModelGenerator\Model\Property;
6+
7+
/**
8+
* Class BaseProperty
9+
*
10+
* @package PHPModelGenerator\Model\Property
11+
*/
12+
class BaseProperty extends Property
13+
{
14+
}

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: 90 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use PHPModelGenerator\PropertyProcessor\PropertyMetaDataCollection;
2020
use PHPModelGenerator\PropertyProcessor\PropertyFactory;
2121
use PHPModelGenerator\PropertyProcessor\PropertyProcessorFactory;
22+
use PHPModelGenerator\SchemaProcessor\SchemaProcessor;
2223
use PHPModelGenerator\Utils\RenderHelper;
2324

2425
/**
@@ -28,7 +29,29 @@
2829
*/
2930
abstract class AbstractComposedValueProcessor extends AbstractValueProcessor
3031
{
32+
/** @var PropertyInterface[] */
3133
private static $generatedMergedProperties = [];
34+
/** @var bool */
35+
private $rootLevelComposition;
36+
37+
/**
38+
* AbstractComposedValueProcessor constructor.
39+
*
40+
* @param PropertyMetaDataCollection $propertyMetaDataCollection
41+
* @param SchemaProcessor $schemaProcessor
42+
* @param Schema $schema
43+
* @param bool $rootLevelComposition
44+
*/
45+
public function __construct(
46+
PropertyMetaDataCollection $propertyMetaDataCollection,
47+
SchemaProcessor $schemaProcessor,
48+
Schema $schema,
49+
bool $rootLevelComposition
50+
) {
51+
parent::__construct($propertyMetaDataCollection, $schemaProcessor, $schema, '');
52+
53+
$this->rootLevelComposition = $rootLevelComposition;
54+
}
3255

3356
/**
3457
* @inheritdoc
@@ -44,6 +67,9 @@ protected function generateValidators(PropertyInterface $property, array $proper
4467
}
4568

4669
$compositionProperties = $this->getCompositionProperties($property, $propertyData);
70+
71+
$this->transferPropertyType($property, $compositionProperties);
72+
4773
$availableAmount = count($compositionProperties);
4874

4975
$property->addValidator(
@@ -62,7 +88,7 @@ protected function generateValidators(PropertyInterface $property, array $proper
6288
// Otherwise (eg. a NotProcessor) the value must be proposed before the validation
6389
'postPropose' => $this instanceof ComposedPropertiesInterface,
6490
'mergedProperty' =>
65-
$this instanceof MergedComposedPropertiesInterface
91+
!$this->rootLevelComposition && $this instanceof MergedComposedPropertiesInterface
6692
? $this->createMergedProperty($property, $compositionProperties, $propertyData)
6793
: null,
6894
'onlyForDefinedValues' =>
@@ -120,12 +146,32 @@ protected function getCompositionProperties(PropertyInterface $property, array $
120146
}
121147

122148
/**
123-
* 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.
124150
*
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+
/**
125171
* Gather all nested object properties and merge them together into a single merged property
126172
*
127173
* @param PropertyInterface $property
128-
* @param CompositionPropertyDecorator[] $properties
174+
* @param CompositionPropertyDecorator[] $compositionProperties
129175
* @param array $propertyData
130176
*
131177
* @return PropertyInterface|null
@@ -134,9 +180,18 @@ protected function getCompositionProperties(PropertyInterface $property, array $
134180
*/
135181
private function createMergedProperty(
136182
PropertyInterface $property,
137-
array $properties,
183+
array $compositionProperties,
138184
array $propertyData
139185
): ?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+
140195
$mergedClassName = $this->schemaProcessor
141196
->getGeneratorConfiguration()
142197
->getClassNameGenerator()
@@ -156,7 +211,7 @@ private function createMergedProperty(
156211
$mergedProperty = new Property('MergedProperty', $mergedClassName);
157212
self::$generatedMergedProperties[$mergedClassName] = $mergedProperty;
158213

159-
$this->transferPropertiesToMergedSchema($mergedPropertySchema, $properties);
214+
$this->transferPropertiesToMergedSchema($mergedPropertySchema, $compositionProperties);
160215

161216
$this->schemaProcessor->generateClassFile(
162217
$this->schemaProcessor->getCurrentClassPath(),
@@ -173,13 +228,40 @@ private function createMergedProperty(
173228
->setNestedSchema($mergedPropertySchema);
174229
}
175230

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+
176258
/**
177259
* @param Schema $mergedPropertySchema
178-
* @param PropertyInterface[] $properties
260+
* @param PropertyInterface[] $compositionProperties
179261
*/
180-
protected function transferPropertiesToMergedSchema(Schema $mergedPropertySchema, array $properties): void
262+
private function transferPropertiesToMergedSchema(Schema $mergedPropertySchema, array $compositionProperties): void
181263
{
182-
foreach ($properties as $property) {
264+
foreach ($compositionProperties as $property) {
183265
if (!$property->getNestedSchema()) {
184266
continue;
185267
}

src/PropertyProcessor/ComposedValueProcessorFactory.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,19 @@
1313
*/
1414
class ComposedValueProcessorFactory implements ProcessorFactoryInterface
1515
{
16+
/** @var bool */
17+
private $rootLevelComposition;
18+
19+
/**
20+
* ComposedValueProcessorFactory constructor.
21+
*
22+
* @param bool $rootLevelComposition is the composed value on object root level (true) or on property level (false)?
23+
*/
24+
public function __construct(bool $rootLevelComposition)
25+
{
26+
$this->rootLevelComposition = $rootLevelComposition;
27+
}
28+
1629
/**
1730
* @inheritdoc
1831
*
@@ -29,6 +42,6 @@ public function getProcessor(
2942
throw new SchemaException("Unsupported composed value type $type");
3043
}
3144

32-
return new $processor($propertyMetaDataCollection, $schemaProcessor, $schema);
45+
return new $processor($propertyMetaDataCollection, $schemaProcessor, $schema, $this->rootLevelComposition);
3346
}
3447
}

src/PropertyProcessor/Property/AbstractPropertyProcessor.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace PHPModelGenerator\PropertyProcessor\Property;
66

77
use PHPModelGenerator\Exception\SchemaException;
8+
use PHPModelGenerator\Model\Property\BaseProperty;
89
use PHPModelGenerator\Model\Property\PropertyInterface;
910
use PHPModelGenerator\Model\Schema;
1011
use PHPModelGenerator\Model\Validator\EnumValidator;
@@ -161,7 +162,7 @@ private function transferDependendPropertiesToBaseSchema(Schema $dependencySchem
161162
protected function addComposedValueValidator(PropertyInterface $property, array $propertyData): void
162163
{
163164
$composedValueKeywords = ['allOf', 'anyOf', 'oneOf', 'not', 'if'];
164-
$propertyFactory = new PropertyFactory(new ComposedValueProcessorFactory());
165+
$propertyFactory = new PropertyFactory(new ComposedValueProcessorFactory($property instanceof BaseProperty));
165166

166167
foreach ($composedValueKeywords as $composedValueKeyword) {
167168
if (!isset($propertyData[$composedValueKeyword])) {
@@ -188,10 +189,17 @@ protected function addComposedValueValidator(PropertyInterface $property, array
188189
}
189190

190191
$property->addTypeHintDecorator(new TypeHintTransferDecorator($composedProperty));
192+
193+
if (!$property->getType() && $composedProperty->getType()) {
194+
$property->setType($composedProperty->getType(), $composedProperty->getType(true));
195+
}
191196
}
192197
}
193198

194199
/**
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+
*
195203
* @param array $propertyData
196204
* @param string $composedValueKeyword
197205
*

src/PropertyProcessor/Property/BaseProcessor.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
use PHPModelGenerator\Exception\Object\MaxPropertiesException;
1212
use PHPModelGenerator\Exception\Object\MinPropertiesException;
1313
use PHPModelGenerator\Exception\SchemaException;
14-
use PHPModelGenerator\Model\Property\Property;
14+
use PHPModelGenerator\Model\Property\BaseProperty;
1515
use PHPModelGenerator\Model\Property\PropertyInterface;
1616
use PHPModelGenerator\Model\Validator;
1717
use PHPModelGenerator\Model\Validator\AbstractComposedPropertyValidator;
@@ -53,7 +53,7 @@ public function process(string $propertyName, array $propertyData): PropertyInte
5353
->setUpDefinitionDictionary($propertyData, $this->schemaProcessor, $this->schema);
5454

5555
// create a property which is used to gather composed properties validators.
56-
$property = new Property($propertyName, static::TYPE);
56+
$property = new BaseProperty($propertyName, static::TYPE);
5757
$this->generateValidators($property, $propertyData);
5858

5959
$this->addPropertyNamesValidator($propertyData);

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
* {% if property.getTypeHint(true) %}@return {{ property.getTypeHint(true) }}{% 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/AbstractPHPModelGeneratorTest.php

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -403,12 +403,12 @@ public function namespaceDataProvider(): array
403403
/**
404404
* Get the annotated type for an object property
405405
*
406-
* @param object $object
406+
* @param string|object $object
407407
* @param string $property
408408
*
409409
* @return string
410410
*/
411-
protected function getPropertyType(object $object, string $property): string
411+
protected function getPropertyType($object, string $property): string
412412
{
413413
$matches = [];
414414
preg_match(
@@ -423,12 +423,12 @@ protected function getPropertyType(object $object, string $property): string
423423
/**
424424
* Get the annotated return type for an object method
425425
*
426-
* @param object $object
426+
* @param string|object $object
427427
* @param string $method
428428
*
429429
* @return string
430430
*/
431-
protected function getMethodReturnType(object $object, string $method): string
431+
protected function getMethodReturnType($object, string $method): string
432432
{
433433
$matches = [];
434434
preg_match(
@@ -440,6 +440,11 @@ protected function getMethodReturnType(object $object, string $method): string
440440
return $matches[1];
441441
}
442442

443+
protected function getGeneratedFiles(): array
444+
{
445+
return $this->generatedFiles;
446+
}
447+
443448
/**
444449
* Generate a unique name for a class
445450
*

0 commit comments

Comments
 (0)