From 757eb1d198bc5d927e3fc64a5f75c9fd0bb9938f Mon Sep 17 00:00:00 2001 From: Enno Woortmann Date: Thu, 20 Apr 2023 17:50:58 +0200 Subject: [PATCH] Fix issue #70 Merged properties of a composition are filled up with the provided values without further validation execution after the single composition branches are validated separately. Consequently,changes applied to the values from filters are missing. The new added method _getModifiedValues sets up a diff with all changed values from the nested composition branch. All modified values are applied to the merged property afterwards so changes from filters are kept. --- .../AbstractComposedValueProcessor.php | 63 ++++++++++++++++ src/Templates/Model.phptpl | 2 +- src/Templates/Validator/ComposedItem.phptpl | 8 ++ tests/Issues/Issue/Issue70Test.php | 74 +++++++++++++++++++ .../Issues/70/filterInCompositionInArray.json | 33 +++++++++ 5 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 tests/Issues/Issue/Issue70Test.php create mode 100644 tests/Schema/Issues/70/filterInCompositionInArray.json 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