From 2cba1adb8362ce3b181242d247779d8571affb76 Mon Sep 17 00:00:00 2001 From: Enno Woortmann Date: Thu, 16 Jul 2020 19:44:47 +0200 Subject: [PATCH 1/4] Added additional property post processor to handle additional properties --- src/Model/Property/Property.php | 4 +- src/Model/Schema.php | 2 +- .../Validator/AdditionalItemsValidator.php | 19 -- .../AdditionalPropertiesValidator.php | 20 ++ .../TypeHint/ArrayTypeHintDecorator.php | 4 +- .../TypeHint/CompositionTypeHintDecorator.php | 5 +- .../Decorator/TypeHint/TypeHintDecorator.php | 2 +- .../TypeHint/TypeHintDecoratorInterface.php | 3 +- .../TypeHint/TypeHintTransferDecorator.php | 4 +- ...itionalPropertiesAccessorPostProcessor.php | 204 ++++++++++++++++++ .../PostProcessor/RenderedMethod.php | 1 + .../AdditionalPropertiesSerializer.phptpl | 18 ++ .../Templates/GetAdditionalProperty.phptpl | 11 + .../PostProcessor/Templates/Populate.phptpl | 2 +- .../Templates/RemoveAdditionalProperty.phptpl | 37 ++++ .../Templates/SetAdditionalProperty.phptpl | 40 ++++ src/Templates/Model.phptpl | 2 +- .../Validator/AdditionalProperties.phptpl | 13 ++ 18 files changed, 359 insertions(+), 32 deletions(-) create mode 100644 src/SchemaProcessor/PostProcessor/AdditionalPropertiesAccessorPostProcessor.php create mode 100644 src/SchemaProcessor/PostProcessor/Templates/AdditionalPropertiesSerializer.phptpl create mode 100644 src/SchemaProcessor/PostProcessor/Templates/GetAdditionalProperty.phptpl create mode 100644 src/SchemaProcessor/PostProcessor/Templates/RemoveAdditionalProperty.phptpl create mode 100644 src/SchemaProcessor/PostProcessor/Templates/SetAdditionalProperty.phptpl diff --git a/src/Model/Property/Property.php b/src/Model/Property/Property.php index 63368ef..515c858 100644 --- a/src/Model/Property/Property.php +++ b/src/Model/Property/Property.php @@ -122,9 +122,9 @@ public function getTypeHint(bool $outputType = false): string $input = [$this->type, $this->outputType]; } - $input = join('|', array_map(function (string $input) { + $input = join('|', array_map(function (string $input) use ($outputType): string { foreach ($this->typeHintDecorators as $decorator) { - $input = $decorator->decorate($input); + $input = $decorator->decorate($input, $outputType); } return $input; diff --git a/src/Model/Schema.php b/src/Model/Schema.php index a9a10ee..f2eac22 100644 --- a/src/Model/Schema.php +++ b/src/Model/Schema.php @@ -156,7 +156,7 @@ public function getBaseValidators(): array * * @return $this */ - public function addBaseValidator(PropertyValidatorInterface $baseValidator) + public function addBaseValidator(PropertyValidatorInterface $baseValidator): self { $this->baseValidators[] = $baseValidator; diff --git a/src/Model/Validator/AdditionalItemsValidator.php b/src/Model/Validator/AdditionalItemsValidator.php index e1f5313..18eefa3 100644 --- a/src/Model/Validator/AdditionalItemsValidator.php +++ b/src/Model/Validator/AdditionalItemsValidator.php @@ -23,23 +23,4 @@ class AdditionalItemsValidator extends AdditionalPropertiesValidator protected const ADDITIONAL_PROPERTIES_KEY = 'additionalItems'; protected const EXCEPTION_CLASS = InvalidAdditionalTupleItemsException::class; - - /** - * AdditionalItemsValidator constructor. - * - * @param SchemaProcessor $schemaProcessor - * @param Schema $schema - * @param JsonSchema $propertiesStructure - * @param string $propertyName - * - * @throws SchemaException - */ - public function __construct( - SchemaProcessor $schemaProcessor, - Schema $schema, - JsonSchema $propertiesStructure, - string $propertyName - ) { - parent::__construct($schemaProcessor, $schema, $propertiesStructure, $propertyName); - } } diff --git a/src/Model/Validator/AdditionalPropertiesValidator.php b/src/Model/Validator/AdditionalPropertiesValidator.php index 41f5fa0..c3d2d5b 100644 --- a/src/Model/Validator/AdditionalPropertiesValidator.php +++ b/src/Model/Validator/AdditionalPropertiesValidator.php @@ -31,6 +31,8 @@ class AdditionalPropertiesValidator extends PropertyTemplateValidator /** @var PropertyInterface */ private $validationProperty; + /** @var bool */ + private $collectAdditionalProperties = false; /** * AdditionalPropertiesValidator constructor. @@ -69,6 +71,8 @@ public function __construct( ), 'generatorConfiguration' => $schemaProcessor->getGeneratorConfiguration(), 'viewHelper' => new RenderHelper($schemaProcessor->getGeneratorConfiguration()), + // by default don't collect additional property data + 'collectAdditionalProperties' => &$this->collectAdditionalProperties, ], static::EXCEPTION_CLASS, [$propertyName ?? $schema->getClassName(), '&$invalidProperties'] @@ -85,6 +89,22 @@ public function getCheck(): string return parent::getCheck(); } + /** + * @param bool $collectAdditionalProperties + */ + public function setCollectAdditionalProperties(bool $collectAdditionalProperties): void + { + $this->collectAdditionalProperties = $collectAdditionalProperties; + } + + /** + * @return PropertyInterface + */ + public function getValidationProperty(): PropertyInterface + { + return $this->validationProperty; + } + /** * Initialize all variables which are required to execute a property names validator * diff --git a/src/PropertyProcessor/Decorator/TypeHint/ArrayTypeHintDecorator.php b/src/PropertyProcessor/Decorator/TypeHint/ArrayTypeHintDecorator.php index 459cb05..94e5e6b 100644 --- a/src/PropertyProcessor/Decorator/TypeHint/ArrayTypeHintDecorator.php +++ b/src/PropertyProcessor/Decorator/TypeHint/ArrayTypeHintDecorator.php @@ -29,10 +29,10 @@ public function __construct(PropertyInterface $nestedProperty) /** * @inheritdoc */ - public function decorate(string $input): string + public function decorate(string $input, bool $outputType = false): string { return implode('|', array_map(function (string $typeHint): string { return "{$typeHint}[]"; - }, explode('|', $this->nestedProperty->getTypeHint()))); + }, explode('|', $this->nestedProperty->getTypeHint($outputType)))); } } diff --git a/src/PropertyProcessor/Decorator/TypeHint/CompositionTypeHintDecorator.php b/src/PropertyProcessor/Decorator/TypeHint/CompositionTypeHintDecorator.php index 585c89a..e74fa2c 100644 --- a/src/PropertyProcessor/Decorator/TypeHint/CompositionTypeHintDecorator.php +++ b/src/PropertyProcessor/Decorator/TypeHint/CompositionTypeHintDecorator.php @@ -29,8 +29,9 @@ public function __construct(PropertyInterface $nestedProperty) /** * @inheritdoc */ - public function decorate(string $input): string + public function decorate(string $input, bool $outputType = false): string { - return (new TypeHintDecorator(explode('|', $this->nestedProperty->getTypeHint())))->decorate($input); + return (new TypeHintDecorator(explode('|', $this->nestedProperty->getTypeHint($outputType)))) + ->decorate($input, $outputType); } } diff --git a/src/PropertyProcessor/Decorator/TypeHint/TypeHintDecorator.php b/src/PropertyProcessor/Decorator/TypeHint/TypeHintDecorator.php index 62e70ba..7fd6589 100644 --- a/src/PropertyProcessor/Decorator/TypeHint/TypeHintDecorator.php +++ b/src/PropertyProcessor/Decorator/TypeHint/TypeHintDecorator.php @@ -27,7 +27,7 @@ public function __construct(array $types) /** * @inheritdoc */ - public function decorate(string $input): string + public function decorate(string $input, bool $outputType = false): string { return implode('|', array_unique(array_filter(array_merge(explode('|', $input), $this->types)))); } diff --git a/src/PropertyProcessor/Decorator/TypeHint/TypeHintDecoratorInterface.php b/src/PropertyProcessor/Decorator/TypeHint/TypeHintDecoratorInterface.php index e7468cb..4fc92d4 100644 --- a/src/PropertyProcessor/Decorator/TypeHint/TypeHintDecoratorInterface.php +++ b/src/PropertyProcessor/Decorator/TypeHint/TypeHintDecoratorInterface.php @@ -15,8 +15,9 @@ interface TypeHintDecoratorInterface * Decorate a given string * * @param string $input The input getting decorated + * @param bool $outputType * * @return string */ - public function decorate(string $input): string; + public function decorate(string $input, bool $outputType = false): string; } diff --git a/src/PropertyProcessor/Decorator/TypeHint/TypeHintTransferDecorator.php b/src/PropertyProcessor/Decorator/TypeHint/TypeHintTransferDecorator.php index a043749..5485727 100644 --- a/src/PropertyProcessor/Decorator/TypeHint/TypeHintTransferDecorator.php +++ b/src/PropertyProcessor/Decorator/TypeHint/TypeHintTransferDecorator.php @@ -29,8 +29,8 @@ public function __construct(PropertyInterface $property) /** * @inheritdoc */ - public function decorate(string $input): string + public function decorate(string $input, bool $outputType = false): string { - return $this->property->getTypeHint(); + return $this->property->getTypeHint($outputType); } } diff --git a/src/SchemaProcessor/PostProcessor/AdditionalPropertiesAccessorPostProcessor.php b/src/SchemaProcessor/PostProcessor/AdditionalPropertiesAccessorPostProcessor.php new file mode 100644 index 0000000..f819b75 --- /dev/null +++ b/src/SchemaProcessor/PostProcessor/AdditionalPropertiesAccessorPostProcessor.php @@ -0,0 +1,204 @@ +getJsonSchema()->getJson(); + + if (!isset($json['additionalProperties']) || $json['additionalProperties'] === false) { + return; + } + + $validationProperty = null; + foreach ($schema->getBaseValidators() as $validator) { + if (is_a($validator, AdditionalPropertiesValidator::class)) { + $validator->setCollectAdditionalProperties(true); + $validationProperty = $validator->getValidationProperty(); + } + } + + $this->addAdditionalPropertiesCollectionProperty($schema, $validationProperty); + $this->addGetAdditionalPropertyMethod($schema, $generatorConfiguration, $validationProperty); + + if ($generatorConfiguration->hasSerializationEnabled()) { + $this->addSerializeAdditionalPropertiesMethod($schema, $generatorConfiguration, $validationProperty); + } + + if (!$generatorConfiguration->isImmutable()) { + $this->addSetAdditionalPropertyMethod($schema, $generatorConfiguration, $validationProperty); + $this->addRemoveAdditionalPropertyMethod($schema, $generatorConfiguration); + } + } + + private function addAdditionalPropertiesCollectionProperty( + Schema $schema, + ?PropertyInterface $validationProperty + ): void { + $additionalPropertiesCollectionProperty = (new Property( + 'additionalProperties', + 'array', + new JsonSchema(__FILE__, []), + 'Collect all additional properties provided to the schema' + )) + ->setDefaultValue([]) + ->setReadOnly(true); + + if ($validationProperty) { + $additionalPropertiesCollectionProperty->addTypeHintDecorator( + new ArrayTypeHintDecorator($validationProperty) + ); + } + + $schema->addProperty($additionalPropertiesCollectionProperty); + } + + private function addSerializeAdditionalPropertiesMethod( + Schema $schema, + GeneratorConfiguration $generatorConfiguration, + ?PropertyInterface $validationProperty + ): void { + $transformingFilterValidator = null; + + if ($validationProperty) { + foreach ($validationProperty->getValidators() as $validator) { + $validator = $validator->getValidator(); + + if ($validator instanceof FilterValidator && + $validator->getFilter() instanceof TransformingFilterInterface + ) { + $transformingFilterValidator = $validator; + [$serializerClass, $serializerMethod] = $validator->getFilter()->getSerializer(); + } + } + } + + $schema->addUsedClass(SerializedValue::class); + $schema->addMethod( + 'serializeAdditionalProperties', + new RenderedMethod( + $schema, + $generatorConfiguration, + 'AdditionalPropertiesSerializer.phptpl', + [ + 'serializerClass' => $serializerClass ?? null, + 'serializerMethod' => $serializerMethod ?? null, + 'serializerOptions' => $transformingFilterValidator + ? var_export($transformingFilterValidator->getFilterOptions(), true) + : [], + ] + ) + ); + } + + private function addSetAdditionalPropertyMethod( + Schema $schema, + GeneratorConfiguration $generatorConfiguration, + ?PropertyInterface $validationProperty + ): void { + $objectProperties = preg_replace( + '(\d+\s=>)', + '', + var_export( + array_map(function (PropertyInterface $property): string { + return $property->getName(); + }, $schema->getProperties()), + true + ) + ); + + $schema->addUsedClass(RegularPropertyAsAdditionalPropertyException::class); + $schema->addMethod( + 'setAdditionalProperty', + new RenderedMethod( + $schema, + $generatorConfiguration, + 'SetAdditionalProperty.phptpl', + [ + 'validationProperty' => $validationProperty, + 'objectProperties' => $objectProperties, + ] + ) + ); + } + + private function addRemoveAdditionalPropertyMethod( + Schema $schema, + GeneratorConfiguration $generatorConfiguration + ): void { + $minPropertyValidator = null; + $json = $schema->getJsonSchema()->getJson(); + if (isset($json['minProperties'])) { + $minPropertyValidator = new PropertyValidator( + sprintf( + '%s < %d', + 'array_keys($this->rawModelDataInput) - 1', + $json['minProperties'] + ), + MinPropertiesException::class, + [$schema->getClassName(), $json['minProperties']] + ); + } + + $schema->addMethod( + 'removeAdditionalProperty', + new RenderedMethod( + $schema, + $generatorConfiguration, + 'RemoveAdditionalProperty.phptpl', + ['minPropertyValidator' => $minPropertyValidator] + ) + ); + } + + private function addGetAdditionalPropertyMethod( + Schema $schema, + GeneratorConfiguration $generatorConfiguration, + ?PropertyInterface $validationProperty + ): void { + $schema->addMethod( + 'getAdditionalProperty', + new RenderedMethod( + $schema, + $generatorConfiguration, + 'GetAdditionalProperty.phptpl', + [ + 'validationProperty' => $validationProperty + // type hint always with null as a non existent property may be requested + ? (clone $validationProperty)->addTypeHintDecorator(new TypeHintDecorator(['null'])) + : null + ] + ) + ); + } +} diff --git a/src/SchemaProcessor/PostProcessor/RenderedMethod.php b/src/SchemaProcessor/PostProcessor/RenderedMethod.php index b4c7dcd..e8aff1a 100644 --- a/src/SchemaProcessor/PostProcessor/RenderedMethod.php +++ b/src/SchemaProcessor/PostProcessor/RenderedMethod.php @@ -57,6 +57,7 @@ public function getCode(): string $this->template, array_merge( [ + 'true' => true, 'schema' => $this->schema, 'viewHelper' => new RenderHelper($this->generatorConfiguration), 'generatorConfiguration' => $this->generatorConfiguration, diff --git a/src/SchemaProcessor/PostProcessor/Templates/AdditionalPropertiesSerializer.phptpl b/src/SchemaProcessor/PostProcessor/Templates/AdditionalPropertiesSerializer.phptpl new file mode 100644 index 0000000..f17ce13 --- /dev/null +++ b/src/SchemaProcessor/PostProcessor/Templates/AdditionalPropertiesSerializer.phptpl @@ -0,0 +1,18 @@ +/** + * serialize all additional properties with the merge value strategy so all values stored in $this->additionalProperties + * will be added to the top level of the serialization result + */ +protected function serializeAdditionalProperties(): SerializedValue +{ + {% if serializerClass %} + $serializedValues = []; + foreach ($this->additionalProperties as $key => $value) { + $serializedValues[$key] = \{{ serializerClass }}::{{ serializerMethod }}($value, {{ serializerOptions }}); + } + {% endif %} + + return new SerializedValue( + {% if serializerClass %}$serializedValues{% else %}$this->additionalProperties{% endif %}, + SerializedValue::STRATEGY_MERGE_VALUE + ); +} diff --git a/src/SchemaProcessor/PostProcessor/Templates/GetAdditionalProperty.phptpl b/src/SchemaProcessor/PostProcessor/Templates/GetAdditionalProperty.phptpl new file mode 100644 index 0000000..1668391 --- /dev/null +++ b/src/SchemaProcessor/PostProcessor/Templates/GetAdditionalProperty.phptpl @@ -0,0 +1,11 @@ +/** + * Get the value of an additional property. If the requested additional property doesn't exists null will be returned + * + * @param string $property The key of the additional property + * + * {% if validationProperty %}{% if viewHelper.getTypeHintAnnotation(validationProperty, true) %}@return {{ viewHelper.getTypeHintAnnotation(validationProperty, true) }}{% endif %}{% endif %} + */ +public function getAdditionalProperty(string $property){% if validationProperty %}{% if validationProperty.getType(true) %}: ?{{ validationProperty.getType(true) }}{% endif %}{% endif %} +{ + return $this->additionalProperties[$property] ?? null; +} diff --git a/src/SchemaProcessor/PostProcessor/Templates/Populate.phptpl b/src/SchemaProcessor/PostProcessor/Templates/Populate.phptpl index 5d38bf8..9f1b1a0 100644 --- a/src/SchemaProcessor/PostProcessor/Templates/Populate.phptpl +++ b/src/SchemaProcessor/PostProcessor/Templates/Populate.phptpl @@ -6,7 +6,7 @@ * * @return self * - * @throws \Exception + * {% if schema.getBaseValidators() %}@throws {% if generatorConfiguration.collectErrors() %}{{ viewHelper.getSimpleClassName(generatorConfiguration.getErrorRegistryClass()) }}{% else %}ValidationException{% endif %}{% endif %} */ public function populate(array $modelData): self { diff --git a/src/SchemaProcessor/PostProcessor/Templates/RemoveAdditionalProperty.phptpl b/src/SchemaProcessor/PostProcessor/Templates/RemoveAdditionalProperty.phptpl new file mode 100644 index 0000000..bb21501 --- /dev/null +++ b/src/SchemaProcessor/PostProcessor/Templates/RemoveAdditionalProperty.phptpl @@ -0,0 +1,37 @@ +/** + * Removes an additional property from the object. Returns true if the property has been removed, + * false if the requested additional property doesn't exist + * + * @param string $property The property key of the additional property + * + * @return bool + * + * {% if minPropertyValidator %}@throws {% if generatorConfiguration.collectErrors() %}{{ viewHelper.getSimpleClassName(generatorConfiguration.getErrorRegistryClass()) }}{% else %}ValidationException{% endif %}{% endif %} + */ +public function removeAdditionalProperty(string $property): bool +{ + if (!array_key_exists($property, $this->additionalProperties)) { + return false; + } + + {% if minPropertyValidator %} + {% if generatorConfiguration.collectErrors() %} + $this->errorRegistry = new {{ viewHelper.getSimpleClassName(generatorConfiguration.getErrorRegistryClass()) }}(); + {% endif%} + + if ({{ minPropertyValidator.getCheck() }}) { + {{ viewHelper.validationError(minPropertyValidator) }} + } + + {% if generatorConfiguration.collectErrors() %} + if (count($this->errorRegistry->getErrors())) { + throw $this->errorRegistry; + } + {% endif %} + {% endif %} + + unset($this->rawModelDataInput[$property]); + unset($this->additionalProperties[$property]); + + return true; +} diff --git a/src/SchemaProcessor/PostProcessor/Templates/SetAdditionalProperty.phptpl b/src/SchemaProcessor/PostProcessor/Templates/SetAdditionalProperty.phptpl new file mode 100644 index 0000000..4df12c8 --- /dev/null +++ b/src/SchemaProcessor/PostProcessor/Templates/SetAdditionalProperty.phptpl @@ -0,0 +1,40 @@ +/** + * Adds or overwrites an additional property on the object + * + * @param string $property The property key of the additional property + * @param {{ viewHelper.getTypeHintAnnotation(validationProperty) }} $value The new value of the additional property + * + * @return self + * + * {% if schema.getBaseValidators() %}@throws {% if generatorConfiguration.collectErrors() %}{{ viewHelper.getSimpleClassName(generatorConfiguration.getErrorRegistryClass()) }}{% else %}ValidationException{% endif %}{% endif %} + * @throws RegularPropertyAsAdditionalPropertyException + */ +public function setAdditionalProperty( + string $property, + {% if validationProperty.getType() %}{% if viewHelper.isPropertyNullable(validationProperty, true) %}?{% endif %}{{ validationProperty.getType() }} {% endif %}$value +): self { + if (in_array($property, {{ objectProperties }})) { + throw new RegularPropertyAsAdditionalPropertyException($value, $property, self::class); + } + + {% if schema.getBaseValidators() %} + {% if generatorConfiguration.collectErrors() %} + $this->errorRegistry = new {{ viewHelper.getSimpleClassName(generatorConfiguration.getErrorRegistryClass()) }}(); + {% endif %} + + $addedProperty = [$property => $value]; + $this->executeBaseValidators($addedProperty); + + {% if generatorConfiguration.collectErrors() %} + if (count($this->errorRegistry->getErrors())) { + throw $this->errorRegistry; + } + {% endif %} + {% else %} + $this->additionalProperties[$property] = $value; + {% endif %} + + $this->rawModelDataInput[$property] = $value; + + return $this; +} diff --git a/src/Templates/Model.phptpl b/src/Templates/Model.phptpl index 78cf514..c7730ca 100644 --- a/src/Templates/Model.phptpl +++ b/src/Templates/Model.phptpl @@ -82,7 +82,7 @@ class {{ class }} {% if schema.getInterfaces() %}implements {{ viewHelper.joinCl {% foreach schema.getBaseValidators() as validator %} {{ validator.getValidatorSetUp() }} - if ({{ validator.getCheck() }}) { + if ({{ validator.getCheck() }}) { {{ viewHelper.validationError(validator) }} } {% endforeach %} diff --git a/src/Templates/Validator/AdditionalProperties.phptpl b/src/Templates/Validator/AdditionalProperties.phptpl index 19c6018..4960adf 100644 --- a/src/Templates/Validator/AdditionalProperties.phptpl +++ b/src/Templates/Validator/AdditionalProperties.phptpl @@ -2,6 +2,9 @@ {% if generatorConfiguration.collectErrors() %} $originalErrorRegistry = $this->errorRegistry; {% endif %} + {% if collectAdditionalProperties %} + $rollbackValues = $this->additionalProperties; + {% endif %} foreach (array_diff(array_keys($properties), {{ additionalProperties }}) as $propertyKey) { try { @@ -25,6 +28,10 @@ $invalidProperties[$propertyKey] = $this->errorRegistry->getErrors(); } {% endif %} + + {% if collectAdditionalProperties %} + $this->additionalProperties[$propertyKey] = $value; + {% endif %} } catch (\Exception $e) { // collect all errors concerning invalid property names isset($invalidProperties[$propertyKey]) @@ -37,5 +44,11 @@ $this->errorRegistry = $originalErrorRegistry; {% endif %} + {% if collectAdditionalProperties %} + if (!empty($invalidProperties)) { + $this->additionalProperties = $rollbackValues; + } + {% endif %} + return !empty($invalidProperties); })() \ No newline at end of file From e36079e87bf95630310a9bd32dd933ecd9fba62f Mon Sep 17 00:00:00 2001 From: Enno Woortmann Date: Thu, 16 Jul 2020 19:50:12 +0200 Subject: [PATCH 2/4] Docblocks --- ...itionalPropertiesAccessorPostProcessor.php | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/SchemaProcessor/PostProcessor/AdditionalPropertiesAccessorPostProcessor.php b/src/SchemaProcessor/PostProcessor/AdditionalPropertiesAccessorPostProcessor.php index f819b75..876b372 100644 --- a/src/SchemaProcessor/PostProcessor/AdditionalPropertiesAccessorPostProcessor.php +++ b/src/SchemaProcessor/PostProcessor/AdditionalPropertiesAccessorPostProcessor.php @@ -27,7 +27,7 @@ class AdditionalPropertiesAccessorPostProcessor implements PostProcessorInterface { /** - * Add serialization support to the provided schema + * Add methods to handle additional properties to the provided schema * * @param Schema $schema * @param GeneratorConfiguration $generatorConfiguration @@ -61,6 +61,12 @@ public function process(Schema $schema, GeneratorConfiguration $generatorConfigu } } + /** + * Adds an array property to the schema which holds all additional properties + * + * @param Schema $schema + * @param PropertyInterface|null $validationProperty + */ private function addAdditionalPropertiesCollectionProperty( Schema $schema, ?PropertyInterface $validationProperty @@ -83,6 +89,14 @@ private function addAdditionalPropertiesCollectionProperty( $schema->addProperty($additionalPropertiesCollectionProperty); } + /** + * Adds a custom serialization function to the schema to merge all additional properties into the serialization + * result on serializations + * + * @param Schema $schema + * @param GeneratorConfiguration $generatorConfiguration + * @param PropertyInterface|null $validationProperty + */ private function addSerializeAdditionalPropertiesMethod( Schema $schema, GeneratorConfiguration $generatorConfiguration, @@ -121,6 +135,13 @@ private function addSerializeAdditionalPropertiesMethod( ); } + /** + * Adds a method to add or update an additional property + * + * @param Schema $schema + * @param GeneratorConfiguration $generatorConfiguration + * @param PropertyInterface|null $validationProperty + */ private function addSetAdditionalPropertyMethod( Schema $schema, GeneratorConfiguration $generatorConfiguration, @@ -152,6 +173,12 @@ private function addSetAdditionalPropertyMethod( ); } + /** + * Adds a method to remove an additional property from the object via property key + * + * @param Schema $schema + * @param GeneratorConfiguration $generatorConfiguration + */ private function addRemoveAdditionalPropertyMethod( Schema $schema, GeneratorConfiguration $generatorConfiguration @@ -181,6 +208,13 @@ private function addRemoveAdditionalPropertyMethod( ); } + /** + * Adds a method to get a single additional property via property key + * + * @param Schema $schema + * @param GeneratorConfiguration $generatorConfiguration + * @param PropertyInterface|null $validationProperty + */ private function addGetAdditionalPropertyMethod( Schema $schema, GeneratorConfiguration $generatorConfiguration, From dc8fbaf3e8e4dd1346b9e8840b770aee32e475a0 Mon Sep 17 00:00:00 2001 From: Enno Woortmann Date: Mon, 20 Jul 2020 17:52:46 +0200 Subject: [PATCH 3/4] Added additional property post processor tests and docs --- docs/source/complexTypes/object.rst | 8 + docs/source/generator/postProcessor.rst | 66 +++- docs/source/gettingStarted.rst | 4 + docs/source/nonStandardExtensions/filter.rst | 3 - .../Property/MultiTypeProcessor.php | 10 +- .../Property/NullProcessor.php | 5 +- ...itionalPropertiesAccessorPostProcessor.php | 21 +- tests/AbstractPHPModelGeneratorTest.php | 20 +- ...nalPropertiesAccessorPostProcessorTest.php | 338 ++++++++++++++++++ .../PopulatePostProcessorTest.php | 10 +- .../AdditionalProperties.json | 18 + .../AdditionalPropertiesFalse.json | 4 + .../AdditionalPropertiesMultiType.json | 6 + .../AdditionalPropertiesNotDefined.json | 3 + ...dditionalPropertiesTransformingFilter.json | 15 + 15 files changed, 510 insertions(+), 21 deletions(-) create mode 100644 tests/PostProcessor/AdditionalPropertiesAccessorPostProcessorTest.php create mode 100644 tests/Schema/AdditionalPropertiesAccessorPostProcessorTest/AdditionalProperties.json create mode 100644 tests/Schema/AdditionalPropertiesAccessorPostProcessorTest/AdditionalPropertiesFalse.json create mode 100644 tests/Schema/AdditionalPropertiesAccessorPostProcessorTest/AdditionalPropertiesMultiType.json create mode 100644 tests/Schema/AdditionalPropertiesAccessorPostProcessorTest/AdditionalPropertiesNotDefined.json create mode 100644 tests/Schema/AdditionalPropertiesAccessorPostProcessorTest/AdditionalPropertiesTransformingFilter.json diff --git a/docs/source/complexTypes/object.rst b/docs/source/complexTypes/object.rst index 4c43f07..e54efb4 100644 --- a/docs/source/complexTypes/object.rst +++ b/docs/source/complexTypes/object.rst @@ -184,6 +184,10 @@ Additional Properties Using the keyword `additionalProperties` the object can be limited to not contain any additional properties by providing `false`. If a schema is provided all additional properties must be valid against the provided schema. Simple checks like 'must provide a string' are possible as well as checks like 'must contain an object with a specific structure'. +.. hint:: + + If you define constraints via `additionalProperties` you may want to use the `AdditionalPropertiesAccessorPostProcessor <../generator/postProcessor.html#additionalpropertiesaccessorpostprocessor>`__ to access and modify your additional properties. + .. code-block:: json { @@ -243,6 +247,10 @@ The thrown exception will be a *PHPModelGenerator\\Exception\\Object\\InvalidAdd // get the value provided to the property public function getProvidedValue() +.. warning:: + + The validation of additional properties is independently from the `implicit null <../gettingStarted.html#implicit-null>`__ setting. If you require your additional properties to accept null define a `multi type `__ with explicit null. + Recursive Objects ----------------- diff --git a/docs/source/generator/postProcessor.rst b/docs/source/generator/postProcessor.rst index 58b7b03..83fa473 100644 --- a/docs/source/generator/postProcessor.rst +++ b/docs/source/generator/postProcessor.rst @@ -19,6 +19,11 @@ Builtin Post Processors PopulatePostProcessor ^^^^^^^^^^^^^^^^^^^^^ +.. code-block:: php + + $generator = new ModelGenerator(); + $generator->addPostProcessor(new PopulatePostProcessor()); + The **PopulatePostProcessor** adds a populate method to your generated model. The populate method accepts an array which might contain any subset of the model's properties. All properties present in the provided array will be validated according to the validation rules from the JSON-Schema. If all values are valid the properties will be updated otherwise an exception will be thrown (if error collection is enabled an exception containing all violations, otherwise on the first occurring error, compare `collecting errors <../gettingStarted.html#collect-errors-vs-early-return>`__). Also basic model constraints like `minProperties`, `maxProperties` or `propertyNames` will be validated as the provided array may add additional properties to the model. If the model is updated also the values which can be fetched via `getRawModelDataInput` will be updated. .. code-block:: json @@ -27,7 +32,7 @@ The **PopulatePostProcessor** adds a populate method to your generated model. Th "$id": "example", "type": "object", "properties": { - "value": { + "example": { "type": "string" } } @@ -74,6 +79,65 @@ Now let's have a look at the behaviour of the generated model: If the **PopulatePostProcessor** is added to your model generator the populate method will be added to the model independently of the `immutable setting <../gettingStarted.html#immutable-classes>`__. +AdditionalPropertiesAccessorPostProcessor +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: php + + $generator = new ModelGenerator(); + $generator->addPostProcessor(new AdditionalPropertiesAccessorPostProcessor(true)); + +The **AdditionalPropertiesAccessorPostProcessor** adds methods to your model to work with `additional properties <../complexTypes/object.html#additional-properties>`__ on your objects. By default the post processor only adds methods to objects from a schema which defines constraints for additional properties. If the first constructor parameter *$addForModelsWithoutAdditionalPropertiesDefinition* is set to true the methods will also be added to objects generated from a schema which doesn't define additional properties constraints. If the *additionalProperties* keyword in a schema is set to false the methods will never be added. + +Added methods +~~~~~~~~~~~~~ + +.. code-block:: json + + { + "$id": "example", + "type": "object", + "properties": { + "example": { + "type": "string" + } + }, + "additionalProperties": { + "type": "string" + } + } + +Generated interface with the **AdditionalPropertiesAccessorPostProcessor**: + +.. code-block:: php + + public function getRawModelDataInput(): array; + + public function setExample(float $example): self; + public function getExample(): float; + + public function getAdditionalProperties(): array; + public function getAdditionalProperty(string $property): ?string; + public function setAdditionalProperty(string $property, string $value): self; + public function removeAdditionalProperty(string $property): bool; + +.. note:: + + The methods **setAdditionalProperty** and **removeAdditionalProperty** are only added if the `immutable setting <../gettingStarted.html#immutable-classes>`__ is set to false. + +**getAdditionalProperties**: This method returns all additional properties which are currently part of the model as key-value pairs where the key is the property name and the value the current value stored in the model. All other properties which are part of the object (in this case the property *example*) will not be included. In opposite to the *getRawModelDataInput* the values provided via this method are the processed values. This means if the schema provides an object-schema for additional properties an array of object instances will be returned. If the additional properties schema contains `filter <../nonStandardExtensions/filter.html>`__ the filtered (and in case of transforming filter transformed) values will be returned. + +**getAdditionalProperty**: Returns the current value of a single additional property. If the requested property doesn't exist null will be returned. Returns as well as *getAdditionalProperties* the processed values. + +**setAdditionalProperty**: Adds or updates an additional property. Performs all necessary validations like property names or min and max properties validations will be performed. If the additional properties are processed via a transforming filter an already transformed value will be accepted. If a property which is regularly defined in the schema a *RegularPropertyAsAdditionalPropertyException* will be thrown. If the change is valid and performed also the output of *getRawModelDataInput* will be updated. + +**removeAdditionalProperty**: Removes an existing additional property from the model. Returns true if the additional property has been removed, false otherwise (if no additional property with the requested key exists). May throw a *MinPropertiesException* if the change would result in an invalid model state. If the change is valid and performed also the output of *getRawModelDataInput* will be updated. + +Serialization +~~~~~~~~~~~~~ + +By default additional properties are not included in serialized models. If the **AdditionalPropertiesAccessorPostProcessor** is applied and `serialization <../gettingStarted.html#serialization-methods>`__ is enabled the additional properties will be merged into the serialization result. If the additional properties are processed via a transforming filter each value will be serialized via the serialisation method of the transforming filter. + Custom Post Processors ---------------------- diff --git a/docs/source/gettingStarted.rst b/docs/source/gettingStarted.rst index c25c25e..ba81d6b 100644 --- a/docs/source/gettingStarted.rst +++ b/docs/source/gettingStarted.rst @@ -247,6 +247,10 @@ The generated class will implement the interface **PHPModelGenerator\\Interfaces Additionally the class will implement the PHP builtin interface **\JsonSerializable** which allows the direct usage of the generated classes in a custom json_encode. +.. warning:: + + If you provide `additional properties `__ you may want to use the `AdditionalPropertiesAccessorPostProcessor `__ as the additional properties by default aren't included into the serialization result. + Output generation process ^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/source/nonStandardExtensions/filter.rst b/docs/source/nonStandardExtensions/filter.rst index 1afec4d..0fbac1e 100644 --- a/docs/source/nonStandardExtensions/filter.rst +++ b/docs/source/nonStandardExtensions/filter.rst @@ -188,9 +188,6 @@ Let's have a look how the generated model behaves: .. code-block:: php - // valid, the name will be NULL as the name is not required - $family = new Person([]); - // A valid example $family = new Family(['members' => [null, null]]]); $family->getMembers(); // returns an empty array diff --git a/src/PropertyProcessor/Property/MultiTypeProcessor.php b/src/PropertyProcessor/Property/MultiTypeProcessor.php index 93efe8b..3f89ab6 100644 --- a/src/PropertyProcessor/Property/MultiTypeProcessor.php +++ b/src/PropertyProcessor/Property/MultiTypeProcessor.php @@ -89,10 +89,12 @@ public function process(string $propertyName, JsonSchema $propertySchema): Prope $property->addTypeHintDecorator( new TypeHintDecorator( - array_map(function (PropertyInterface $subProperty): string { - return $subProperty->getTypeHint(); - }, - $subProperties) + array_map( + function (PropertyInterface $subProperty): string { + return $subProperty->getTypeHint(); + }, + $subProperties + ) ) ); diff --git a/src/PropertyProcessor/Property/NullProcessor.php b/src/PropertyProcessor/Property/NullProcessor.php index 7002e64..34d82a1 100644 --- a/src/PropertyProcessor/Property/NullProcessor.php +++ b/src/PropertyProcessor/Property/NullProcessor.php @@ -6,6 +6,7 @@ use PHPModelGenerator\Model\Property\PropertyInterface; use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; +use PHPModelGenerator\PropertyProcessor\Decorator\TypeHint\TypeHintDecorator; /** * Class NullProcessor @@ -26,6 +27,8 @@ class NullProcessor extends AbstractTypedValueProcessor */ public function process(string $propertyName, JsonSchema $propertySchema): PropertyInterface { - return (parent::process($propertyName, $propertySchema))->setType(''); + return (parent::process($propertyName, $propertySchema)) + ->setType('') + ->addTypeHintDecorator(new TypeHintDecorator(['null'])); } } diff --git a/src/SchemaProcessor/PostProcessor/AdditionalPropertiesAccessorPostProcessor.php b/src/SchemaProcessor/PostProcessor/AdditionalPropertiesAccessorPostProcessor.php index 876b372..5e77da1 100644 --- a/src/SchemaProcessor/PostProcessor/AdditionalPropertiesAccessorPostProcessor.php +++ b/src/SchemaProcessor/PostProcessor/AdditionalPropertiesAccessorPostProcessor.php @@ -26,6 +26,21 @@ */ class AdditionalPropertiesAccessorPostProcessor implements PostProcessorInterface { + /** @var bool */ + private $addForModelsWithoutAdditionalPropertiesDefinition; + + /** + * AdditionalPropertiesAccessorPostProcessor constructor. + * + * @param bool $addForModelsWithoutAdditionalPropertiesDefinition By default the additional properties accessor + * methods will be added only to schemas defining additionalProperties constraints as these models expect additional + * properties. If set to true the accessor methods will be generated for models which don't define + * additionalProperties constraints. + */ + public function __construct(bool $addForModelsWithoutAdditionalPropertiesDefinition = false) + { + $this->addForModelsWithoutAdditionalPropertiesDefinition = $addForModelsWithoutAdditionalPropertiesDefinition; + } /** * Add methods to handle additional properties to the provided schema * @@ -36,7 +51,9 @@ public function process(Schema $schema, GeneratorConfiguration $generatorConfigu { $json = $schema->getJsonSchema()->getJson(); - if (!isset($json['additionalProperties']) || $json['additionalProperties'] === false) { + if ((!$this->addForModelsWithoutAdditionalPropertiesDefinition && !isset($json['additionalProperties'])) + || (isset($json['additionalProperties']) && $json['additionalProperties'] === false) + ) { return; } @@ -189,7 +206,7 @@ private function addRemoveAdditionalPropertyMethod( $minPropertyValidator = new PropertyValidator( sprintf( '%s < %d', - 'array_keys($this->rawModelDataInput) - 1', + 'count($this->rawModelDataInput) - 1', $json['minProperties'] ), MinPropertiesException::class, diff --git a/tests/AbstractPHPModelGeneratorTest.php b/tests/AbstractPHPModelGeneratorTest.php index 48524c4..2806af6 100644 --- a/tests/AbstractPHPModelGeneratorTest.php +++ b/tests/AbstractPHPModelGeneratorTest.php @@ -30,7 +30,8 @@ abstract class AbstractPHPModelGeneratorTest extends TestCase { protected const EXTERNAL_JSON_DIRECTORIES = []; - protected const POST_PROCESSORS = []; + + protected $modifyModelGenerator = null; private $names = []; @@ -241,8 +242,8 @@ public function getClassName( } $generator = new ModelGenerator($generatorConfiguration); - foreach (static::POST_PROCESSORS as $postProcessor) { - $generator->addPostProcessor(new $postProcessor()); + if (is_callable($this->modifyModelGenerator)) { + ($this->modifyModelGenerator)($generator); } $generatedFiles = $generator->generateModels( @@ -449,7 +450,7 @@ protected function getMethodReturnTypeAnnotation($object, string $method): strin $matches ); - return $matches[1]; + return $matches[1] ?? ''; } /** @@ -457,24 +458,25 @@ protected function getMethodReturnTypeAnnotation($object, string $method): strin * * @param string|object $object * @param string $method + * @param int $parameter * * @return string */ - protected function getMethodParameterTypeAnnotation($object, string $method): string + protected function getMethodParameterTypeAnnotation($object, string $method, int $parameter = 0): string { $matches = []; - preg_match( + preg_match_all( '/@param\s+([^\s]*)\s?\$/', (new ReflectionClass($object))->getMethod($method)->getDocComment(), $matches ); - return $matches[1]; + return $matches[1][$parameter]; } - protected function getParameterType($object, string $method): ?ReflectionType + protected function getParameterType($object, string $method, int $parameter = 0): ?ReflectionType { - return (new ReflectionClass($object))->getMethod($method)->getParameters()[0]->getType(); + return (new ReflectionClass($object))->getMethod($method)->getParameters()[$parameter]->getType(); } protected function getReturnType($object, string $method): ?ReflectionType diff --git a/tests/PostProcessor/AdditionalPropertiesAccessorPostProcessorTest.php b/tests/PostProcessor/AdditionalPropertiesAccessorPostProcessorTest.php new file mode 100644 index 0000000..0bf934f --- /dev/null +++ b/tests/PostProcessor/AdditionalPropertiesAccessorPostProcessorTest.php @@ -0,0 +1,338 @@ +modifyModelGenerator = function (ModelGenerator $generator) use ( + $addForModelsWithoutAdditionalPropertiesDefinition + ) { + $generator->addPostProcessor( + new AdditionalPropertiesAccessorPostProcessor($addForModelsWithoutAdditionalPropertiesDefinition) + ); + }; + } + + /** + * @dataProvider additionalPropertiesAccessorPostProcessorConfigurationDataProvider + * + * @param bool $addForModelsWithoutAdditionalPropertiesDefinition + */ + public function testAdditionalPropertiesAccessorsAreNotGeneratedForAdditionalPropertiesFalse( + bool $addForModelsWithoutAdditionalPropertiesDefinition + ): void { + $this->addPostProcessor($addForModelsWithoutAdditionalPropertiesDefinition); + + $className = $this->generateClassFromFile('AdditionalPropertiesFalse.json'); + + $object = new $className(); + + $this->assertFalse(is_callable([$object, 'getAdditionalProperties'])); + $this->assertFalse(is_callable([$object, 'getAdditionalProperty'])); + $this->assertFalse(is_callable([$object, 'setAdditionalProperty'])); + $this->assertFalse(is_callable([$object, 'removeAdditionalProperty'])); + } + + /** + * @dataProvider additionalPropertiesAccessorPostProcessorConfigurationDataProvider + * + * @param bool $addForModelsWithoutAdditionalPropertiesDefinition + */ + public function testAdditionalPropertiesAccessorsDependOnConfigurationForAdditionalPropertiesNotDefined( + bool $addForModelsWithoutAdditionalPropertiesDefinition + ): void { + $this->addPostProcessor($addForModelsWithoutAdditionalPropertiesDefinition); + + $className = $this->generateClassFromFile('AdditionalPropertiesNotDefined.json'); + + $object = new $className(); + + $this->assertSame( + $addForModelsWithoutAdditionalPropertiesDefinition, + is_callable([$object, 'getAdditionalProperties']) + ); + $this->assertSame( + $addForModelsWithoutAdditionalPropertiesDefinition, + is_callable([$object, 'getAdditionalProperty']) + ); + $this->assertFalse(is_callable([$object, 'setAdditionalProperty'])); + $this->assertFalse(is_callable([$object, 'removeAdditionalProperty'])); + + if ($addForModelsWithoutAdditionalPropertiesDefinition) { + $this->assertSame('array', $this->getMethodReturnTypeAnnotation($object, 'getAdditionalProperties')); + $returnType = $this->getReturnType($object, 'getAdditionalProperties'); + $this->assertSame('array', $returnType->getName()); + $this->assertFalse($returnType->allowsNull()); + + $this->assertEmpty($this->getMethodReturnTypeAnnotation($object, 'getAdditionalProperty')); + $this->assertNull($this->getReturnType($object, 'getAdditionalProperty')); + } + } + + /** + * @dataProvider additionalPropertiesAccessorPostProcessorConfigurationDataProvider + * + * @param bool $addForModelsWithoutAdditionalPropertiesDefinition + */ + public function testAdditionalPropertiesAccessorsAreGeneratedForAdditionalProperties( + bool $addForModelsWithoutAdditionalPropertiesDefinition + ): void { + $this->addPostProcessor($addForModelsWithoutAdditionalPropertiesDefinition); + + $className = $this->generateClassFromFile('AdditionalProperties.json'); + + $object = new $className(['property1' => 'Hello', 'property2' => 'World']); + + $this->assertTrue(is_callable([$object, 'getAdditionalProperties'])); + $this->assertTrue(is_callable([$object, 'getAdditionalProperty'])); + $this->assertFalse(is_callable([$object, 'setAdditionalProperty'])); + $this->assertFalse(is_callable([$object, 'removeAdditionalProperty'])); + + $this->assertSame(['property1' => 'Hello', 'property2' => 'World'], $object->getAdditionalProperties()); + $this->assertSame('Hello', $object->getAdditionalProperty('property1')); + $this->assertSame('World', $object->getAdditionalProperty('property2')); + $this->assertNull($object->getAdditionalProperty('property3')); + } + + public function additionalPropertiesAccessorPostProcessorConfigurationDataProvider(): array + { + return [ + 'Add also for models without additional properties definition' => [true], + 'Add only for models with additional properties definition' => [false], + ]; + } + + public function testAdditionalPropertiesModifiersAreGeneratedForMutableObjects(): void { + $this->addPostProcessor(true); + + $className = $this->generateClassFromFile( + 'AdditionalProperties.json', + (new GeneratorConfiguration())->setImmutable(false) + ); + + $object = new $className(['property1' => ' Hello ', 'property2' => 'World']); + + $this->assertTrue(is_callable([$object, 'getAdditionalProperties'])); + $this->assertTrue(is_callable([$object, 'getAdditionalProperty'])); + $this->assertTrue(is_callable([$object, 'setAdditionalProperty'])); + $this->assertTrue(is_callable([$object, 'removeAdditionalProperty'])); + + // test adding a new additional property + $object->setAdditionalProperty('property3', ' Good night '); + $this->assertSame( + ['property1' => 'Hello', 'property2' => 'World', 'property3' => 'Good night'], + $object->getAdditionalProperties() + ); + $this->assertSame( + ['property1' => ' Hello ', 'property2' => 'World', 'property3' => ' Good night '], + $object->getRawModelDataInput() + ); + $this->assertSame('Good night', $object->getAdditionalProperty('property3')); + + // test removing an additional property + $this->assertTrue($object->removeAdditionalProperty('property2')); + $this->assertFalse($object->removeAdditionalProperty('property2')); + $this->assertSame( + ['property1' => 'Hello', 'property3' => 'Good night'], + $object->getAdditionalProperties() + ); + $this->assertSame( + ['property1' => ' Hello ', 'property3' => ' Good night '], + $object->getRawModelDataInput() + ); + + // test update an existing additional property + $object->setAdditionalProperty('property3', ' !Good night! '); + $this->assertSame( + ['property1' => 'Hello', 'property3' => '!Good night!'], + $object->getAdditionalProperties() + ); + $this->assertSame( + ['property1' => ' Hello ', 'property3' => ' !Good night! '], + $object->getRawModelDataInput() + ); + $this->assertSame('!Good night!', $object->getAdditionalProperty('property3')); + + // test typing + $this->assertSame('string[]', $this->getMethodReturnTypeAnnotation($object, 'getAdditionalProperties')); + $returnType = $this->getReturnType($object, 'getAdditionalProperties'); + $this->assertSame('array', $returnType->getName()); + $this->assertFalse($returnType->allowsNull()); + + $this->assertSame('string|null', $this->getMethodReturnTypeAnnotation($object, 'getAdditionalProperty')); + $returnType = $this->getReturnType($object, 'getAdditionalProperty'); + $this->assertSame('string', $returnType->getName()); + $this->assertTrue($returnType->allowsNull()); + + $this->assertSame('string', $this->getMethodParameterTypeAnnotation($object, 'setAdditionalProperty', 1)); + $parameterType = $this->getParameterType($object, 'setAdditionalProperty', 1); + $this->assertSame('string', $parameterType->getName()); + $this->assertFalse($parameterType->allowsNull()); + } + + /** + * @dataProvider invalidAdditionalPropertyDataProvider + * + * @param string $expectedException + * @param string $expectedExceptionMessage + * @param string $action + * @param array $items + */ + public function testInvalidAdditionalPropertyThrowsAnException( + string $expectedException, + string $expectedExceptionMessage, + string $action, + array $items + ): void { + $this->expectException($expectedException); + $this->expectExceptionMessage($expectedExceptionMessage); + + $this->addPostProcessor(true); + + $className = $this->generateClassFromFile( + 'AdditionalProperties.json', + (new GeneratorConfiguration())->setImmutable(false)->setCollectErrors(false) + ); + + $object = new $className(['property1' => ' Hello ', 'property2' => 'World']); + + foreach ($items as $property => $value) { + $action === 'remove' + ? $object->removeAdditionalProperty($value) + : $object->setAdditionalProperty($property, $value); + } + } + + public function invalidAdditionalPropertyDataProvider(): array + { + return [ + 'regular object property' => [ + RegularPropertyAsAdditionalPropertyException::class, + "Couldn't add regular property name as additional property to object ", + 'add', + ['name' => 'Hannes'], + ], + 'min properties violation' => [ + MinPropertiesException::class, + 'must not contain less than 2 properties', + 'remove', + ['property1'] + ], + 'max properties violation' => [ + MaxPropertiesException::class, + 'must not contain more than 4 properties', + 'add', + ['property3' => 'Bye', 'property4' => 'Ciao', 'property5' => 'fails'] + ], + 'Invalid property name' => [ + InvalidPropertyNamesException::class, + "contains properties with invalid names", + 'add', + ['property name with spaces' => 'should fail'] + ], + 'Invalid property value' => [ + InvalidAdditionalPropertiesException::class, + "Value for additional property must not be longer than 15", + 'add', + ['property2' => 'My much too long property value will fail the validation'] + ], + ]; + } + + public function testAdditionalPropertiesAreSerialized(): void + { + $this->addPostProcessor(true); + + $className = $this->generateClassFromFile( + 'AdditionalPropertiesTransformingFilter.json', + (new GeneratorConfiguration())->setSerialization(true)->setImmutable(false) + ); + + $object = new $className(['name' => 'Late autumn', 'start' => '2020-10-10']); + $this->assertInstanceOf(DateTime::class, $object->getAdditionalProperty('start')); + $this->assertInstanceOf(DateTime::class, $object->getAdditionalProperties()['start']); + + $object->setAdditionalProperty('end', '2020-12-12'); + $this->assertInstanceOf(DateTime::class, $object->getAdditionalProperty('end')); + $this->assertInstanceOf(DateTime::class, $object->getAdditionalProperties()['end']); + + $this->assertSame(['name' => 'Late autumn', 'start' => '20201010', 'end' => '20201212'], $object->toArray()); + + // test adding a transformed value + $object->setAdditionalProperty('now', new DateTime()); + $this->assertInstanceOf(DateTime::class, $object->getAdditionalProperty('now')); + + // test typing + $this->assertSame('DateTime[]', $this->getMethodReturnTypeAnnotation($object, 'getAdditionalProperties')); + $returnType = $this->getReturnType($object, 'getAdditionalProperties'); + $this->assertSame('array', $returnType->getName()); + $this->assertFalse($returnType->allowsNull()); + + $this->assertSame('DateTime|null', $this->getMethodReturnTypeAnnotation($object, 'getAdditionalProperty')); + $returnType = $this->getReturnType($object, 'getAdditionalProperty'); + $this->assertSame('DateTime', $returnType->getName()); + $this->assertTrue($returnType->allowsNull()); + + $this->assertSame( + 'string|DateTime', + $this->getMethodParameterTypeAnnotation($object, 'setAdditionalProperty', 1) + ); + + $this->assertNull($this->getParameterType($object, 'setAdditionalProperty', 1)); + } + + public function testMultiTypeAdditionalProperties(): void + { + $this->addPostProcessor(true); + + $className = $this->generateClassFromFile( + 'AdditionalPropertiesMultiType.json', + (new GeneratorConfiguration())->setImmutable(false) + ); + + $object = new $className(['property1' => 'Hello', 'property2' => null]); + $this->assertNull($object->getAdditionalProperty('property2')); + + $object->setAdditionalProperty('property1', null); + $this->assertNull($object->getAdditionalProperty('property1')); + + // test typing + $this->assertSame( + 'string[]|int[]|null[]', + $this->getMethodReturnTypeAnnotation($object, 'getAdditionalProperties') + ); + $returnType = $this->getReturnType($object, 'getAdditionalProperties'); + $this->assertSame('array', $returnType->getName()); + $this->assertFalse($returnType->allowsNull()); + + $this->assertSame( + 'string|int|null', + $this->getMethodReturnTypeAnnotation($object, 'getAdditionalProperty') + ); + $this->assertNull($this->getReturnType($object, 'getAdditionalProperty')); + + $this->assertSame( + 'string|int|null', + $this->getMethodParameterTypeAnnotation($object, 'setAdditionalProperty', 1) + ); + $this->assertNull($this->getParameterType($object, 'setAdditionalProperty', 1)); + } +} diff --git a/tests/PostProcessor/PopulatePostProcessorTest.php b/tests/PostProcessor/PopulatePostProcessorTest.php index bed118f..cd11c96 100644 --- a/tests/PostProcessor/PopulatePostProcessorTest.php +++ b/tests/PostProcessor/PopulatePostProcessorTest.php @@ -9,12 +9,20 @@ use PHPModelGenerator\Exception\Object\MaxPropertiesException; use PHPModelGenerator\Exception\String\PatternException; use PHPModelGenerator\Model\GeneratorConfiguration; +use PHPModelGenerator\ModelGenerator; use PHPModelGenerator\SchemaProcessor\PostProcessor\PopulatePostProcessor; use PHPModelGenerator\Tests\AbstractPHPModelGeneratorTest; class PopulatePostProcessorTest extends AbstractPHPModelGeneratorTest { - protected const POST_PROCESSORS = [PopulatePostProcessor::class]; + public function setUp(): void + { + parent::setUp(); + + $this->modifyModelGenerator = function (ModelGenerator $generator) { + $generator->addPostProcessor(new PopulatePostProcessor()); + }; + } public function testPopulateMethod(): void { diff --git a/tests/Schema/AdditionalPropertiesAccessorPostProcessorTest/AdditionalProperties.json b/tests/Schema/AdditionalPropertiesAccessorPostProcessorTest/AdditionalProperties.json new file mode 100644 index 0000000..910f68d --- /dev/null +++ b/tests/Schema/AdditionalPropertiesAccessorPostProcessorTest/AdditionalProperties.json @@ -0,0 +1,18 @@ +{ + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "additionalProperties": { + "type": "string", + "filter": "trim", + "maxLength": 15 + }, + "propertyNames": { + "pattern": "^[a-z0-9]*$" + }, + "minProperties": 2, + "maxProperties": 4 +} \ No newline at end of file diff --git a/tests/Schema/AdditionalPropertiesAccessorPostProcessorTest/AdditionalPropertiesFalse.json b/tests/Schema/AdditionalPropertiesAccessorPostProcessorTest/AdditionalPropertiesFalse.json new file mode 100644 index 0000000..b1367ae --- /dev/null +++ b/tests/Schema/AdditionalPropertiesAccessorPostProcessorTest/AdditionalPropertiesFalse.json @@ -0,0 +1,4 @@ +{ + "type": "object", + "additionalProperties": false +} \ No newline at end of file diff --git a/tests/Schema/AdditionalPropertiesAccessorPostProcessorTest/AdditionalPropertiesMultiType.json b/tests/Schema/AdditionalPropertiesAccessorPostProcessorTest/AdditionalPropertiesMultiType.json new file mode 100644 index 0000000..26f0d3c --- /dev/null +++ b/tests/Schema/AdditionalPropertiesAccessorPostProcessorTest/AdditionalPropertiesMultiType.json @@ -0,0 +1,6 @@ +{ + "type": "object", + "additionalProperties": { + "type": ["string", "integer", "null"] + } +} \ No newline at end of file diff --git a/tests/Schema/AdditionalPropertiesAccessorPostProcessorTest/AdditionalPropertiesNotDefined.json b/tests/Schema/AdditionalPropertiesAccessorPostProcessorTest/AdditionalPropertiesNotDefined.json new file mode 100644 index 0000000..e498ac4 --- /dev/null +++ b/tests/Schema/AdditionalPropertiesAccessorPostProcessorTest/AdditionalPropertiesNotDefined.json @@ -0,0 +1,3 @@ +{ + "type": "object" +} \ No newline at end of file diff --git a/tests/Schema/AdditionalPropertiesAccessorPostProcessorTest/AdditionalPropertiesTransformingFilter.json b/tests/Schema/AdditionalPropertiesAccessorPostProcessorTest/AdditionalPropertiesTransformingFilter.json new file mode 100644 index 0000000..f7867aa --- /dev/null +++ b/tests/Schema/AdditionalPropertiesAccessorPostProcessorTest/AdditionalPropertiesTransformingFilter.json @@ -0,0 +1,15 @@ +{ + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "additionalProperties": { + "type": "string", + "filter": [{ + "filter": "dateTime", + "outputFormat": "Ymd" + }] + } +} \ No newline at end of file From d889e8dc00eb3caffbf991b3ac61e539464ac0a2 Mon Sep 17 00:00:00 2001 From: Enno Woortmann Date: Mon, 20 Jul 2020 18:06:20 +0200 Subject: [PATCH 4/4] bump wol-soft/php-json-schema-model-generator-production --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 819f95b..1a46cfd 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ ], "require": { "symplify/easy-coding-standard": "^7.2.3", - "wol-soft/php-json-schema-model-generator-production": "^0.12.2", + "wol-soft/php-json-schema-model-generator-production": "^0.13.0", "wol-soft/php-micro-template": "^1.3.2", "php": ">=7.2",