diff --git a/src/PropertyProcessor/ComposedValue/AbstractComposedValueProcessor.php b/src/PropertyProcessor/ComposedValue/AbstractComposedValueProcessor.php index 13bade1..9d9bbb2 100644 --- a/src/PropertyProcessor/ComposedValue/AbstractComposedValueProcessor.php +++ b/src/PropertyProcessor/ComposedValue/AbstractComposedValueProcessor.php @@ -5,6 +5,7 @@ namespace PHPModelGenerator\PropertyProcessor\ComposedValue; use PHPModelGenerator\Exception\SchemaException; +use PHPModelGenerator\Model\MethodInterface; use PHPModelGenerator\Model\Property\CompositionPropertyDecorator; use PHPModelGenerator\Model\Property\Property; use PHPModelGenerator\Model\Property\PropertyInterface; @@ -117,6 +118,10 @@ function () use (&$resolvedCompositions, $property, $compositionProperties, $pro ), 100 ); + + if (!$this->schema->hasMethod('_getModifiedValues')) { + $this->addGetModifiedValuesMethodToSchema($compositionProperties); + } } /** @@ -339,6 +344,64 @@ function () use ($property, $mergedPropertySchema): void { } } + /** + * Add a method to the schema to gather values from a nested object which are modified. This is required to adopt + * filter changes to the values which are passed into a merged property + * + * @param CompositionPropertyDecorator[] $compositionProperties + */ + private function addGetModifiedValuesMethodToSchema(array $compositionProperties): void + { + $this->schema->addMethod('_getModifiedValues', new class ($compositionProperties) implements MethodInterface { + /** @var CompositionPropertyDecorator[] $compositionProperties */ + private $compositionProperties; + + public function __construct(array $compositionProperties) + { + $this->compositionProperties = $compositionProperties; + } + + public function getCode(): string + { + $defaultValueMap = []; + $propertyAccessors = []; + foreach ($this->compositionProperties as $compositionProperty) { + if (!$compositionProperty->getNestedSchema()) { + continue; + } + + foreach ($compositionProperty->getNestedSchema()->getProperties() as $property) { + $propertyAccessors[$property->getName()] = 'get' . ucfirst($property->getAttribute()); + + if ($property->getDefaultValue() !== null) { + $defaultValueMap[] = $property->getName(); + } + } + } + + return sprintf(' + private function _getModifiedValues(array $originalModelData, object $nestedCompositionObject): array { + $modifiedValues = []; + $defaultValueMap = %s; + + foreach (%s as $key => $accessor) { + if ((isset($originalModelData[$key]) || in_array($key, $defaultValueMap)) + && method_exists($nestedCompositionObject, $accessor) + && ($modifiedValue = $nestedCompositionObject->$accessor()) !== ($originalModelData[$key] ?? !$modifiedValue) + ) { + $modifiedValues[$key] = $modifiedValue; + } + } + + return $modifiedValues; + }', + var_export($defaultValueMap, true), + var_export($propertyAccessors, true) + ); + } + }); + } + /** * @param int $composedElements The amount of elements which are composed together * diff --git a/src/Templates/Model.phptpl b/src/Templates/Model.phptpl index 8b77391..dc0e1b1 100644 --- a/src/Templates/Model.phptpl +++ b/src/Templates/Model.phptpl @@ -20,7 +20,7 @@ declare(strict_types = 1); {% if schema.getDescription() %} * {{ schema.getDescription() }} *{% endif %} * This is an auto-implemented class implemented by the php-json-schema-model-generator. - * If you need to implement something in this class use inheritance. Else you will loose your changes if the classes + * If you need to implement something in this class use inheritance. Else you will lose your changes if the classes * are re-generated. */ class {{ class }} {% if schema.getInterfaces() %}implements {{ viewHelper.joinClassNames(schema.getInterfaces()) }}{% endif %} diff --git a/src/Templates/Validator/ComposedItem.phptpl b/src/Templates/Validator/ComposedItem.phptpl index 04c1cee..ba424b2 100644 --- a/src/Templates/Validator/ComposedItem.phptpl +++ b/src/Templates/Validator/ComposedItem.phptpl @@ -12,6 +12,7 @@ $validatorComponentIndex = 0; $originalModelData = $value; $proposedValue = null; + $modifiedValues = []; {% if not generatorConfiguration.isImmutable() %} $originalPropertyValidationState = $this->_propertyValidationState ?? []; @@ -90,6 +91,9 @@ $proposedValue = $proposedValue ?? $value; {% endif %} + if (is_object($value)) { + $modifiedValues = array_merge($modifiedValues, $this->_getModifiedValues($originalModelData, $value)); + } {% if not generatorConfiguration.isImmutable() %} {% if not generatorConfiguration.collectErrors() %} if (isset($validatorIndex)) { @@ -118,6 +122,10 @@ {% if mergedProperty %} if (is_object($proposedValue)) { + if ($modifiedValues) { + $value = array_merge($value, $modifiedValues); + } + {{ viewHelper.resolvePropertyDecorator(mergedProperty) }} } else { $value = $proposedValue; diff --git a/tests/Issues/Issue/Issue70Test.php b/tests/Issues/Issue/Issue70Test.php new file mode 100644 index 0000000..714b34b --- /dev/null +++ b/tests/Issues/Issue/Issue70Test.php @@ -0,0 +1,74 @@ +generateClassFromFileTemplate( + 'filterInCompositionInArray.json', + [$filter], + (new GeneratorConfiguration())->addFilter($this->getFilter()) + ); + + $object = new $className($input); + + $this->assertCount(1, $object->getItems()); + $this->assertSame('Hello', $object->getItems()[0]->getTitle()); + $this->assertSame($expectedOutput, $object->getItems()[0]->getProperty()); + } + + public function validInputDataProvider(): array + { + return [ + 'basic filter - default value' => ['trim', ['items' => [['title' => 'Hello']]], 'now'], + 'basic filter - custom value - not modified' => ['trim', ['items' => [['title' => 'Hello', 'property' => 'later']]], 'later'], + 'basic filter - custom value - modified' => ['trim', ['items' => [['title' => 'Hello', 'property' => ' later ']]], 'later'], + 'basic filter - null' => ['trim', ['items' => [['title' => 'Hello', 'property' => null]]], null], + 'transforming filter - default value' => ['countChars', ['items' => [['title' => 'Hello']]], 3], + 'transforming filter - transformed value' => ['countChars', ['items' => [['title' => 'Hello', 'property' => 5]]], 5], + 'transforming filter - custom value' => ['countChars', ['items' => [['title' => 'Hello', 'property' => 'Hello World']]], 11], + 'transforming filter - null' => ['countChars', ['items' => [['title' => 'Hello', 'property' => null]]], null], + ]; + } + + public function getFilter(): TransformingFilterInterface + { + return new class () implements TransformingFilterInterface { + public function getAcceptedTypes(): array + { + return ['string', 'null']; + } + + public function getToken(): string + { + return 'countChars'; + } + + public function getFilter(): array + { + return [Issue70Test::class, 'filter']; + } + + public function getSerializer(): array + { + return [Issue70Test::class, 'filter']; + } + }; + } + + public static function filter(?string $input): ?int + { + return $input === null ? null : strlen($input); + } +} diff --git a/tests/Schema/Issues/70/filterInCompositionInArray.json b/tests/Schema/Issues/70/filterInCompositionInArray.json new file mode 100644 index 0000000..d22b6cd --- /dev/null +++ b/tests/Schema/Issues/70/filterInCompositionInArray.json @@ -0,0 +1,33 @@ +{ + "type": "object", + "properties": { + "items": { + "type": "array", + "items":{ + "allOf": [ + { + "type": "object", + "properties": { + "title": { + "type": "string" + } + }, + "required": [ + "title" + ] + }, + { + "type": "object", + "properties": { + "property": { + "type": "string", + "filter": "%s", + "default": "now" + } + } + } + ] + } + } + } +} \ No newline at end of file