From e801a4003b39f31eb94d3545101c6b3ef095f962 Mon Sep 17 00:00:00 2001 From: Enno Woortmann Date: Mon, 13 Mar 2023 13:51:08 +0100 Subject: [PATCH 1/8] Issue #65. Add onResolved callback to properties to execute further post-processing of the property only after the property has been created completely. Otherwise, the property might redirect to null causing the generation process to crash. Compositions for example use generated properties to enrich the base model the composition belongs to. This process must be delayed until the properties are completely set up (an incomplete property might occur due to recursion). Consequently now the composition uses a callback mechanism to delay the process until all recursions are resolved. --- src/Model/Property/AbstractProperty.php | 19 +++++ .../Property/CompositionPropertyDecorator.php | 4 + src/Model/Property/Property.php | 58 ++++++++++--- src/Model/Property/PropertyInterface.php | 16 +++- src/Model/Property/PropertyProxy.php | 28 +++++- src/Model/Schema.php | 29 +++++++ .../SchemaDefinition/SchemaDefinition.php | 24 +++++- .../AbstractComposedPropertyValidator.php | 2 +- .../AdditionalPropertiesValidator.php | 1 + src/Model/Validator/ArrayItemValidator.php | 1 + src/Model/Validator/ArrayTupleValidator.php | 1 + .../Validator/ComposedPropertyValidator.php | 18 ++-- .../ConditionalPropertyValidator.php | 11 +-- .../Validator/ExtractedMethodValidator.php | 72 ++++++++++++++++ .../AbstractComposedValueProcessor.php | 85 ++++++++++++------- .../ComposedValue/IfProcessor.php | 9 +- .../TypeHint/ArrayTypeHintDecorator.php | 23 ++++- .../TypeHint/TypeHintTransferDecorator.php | 2 +- .../Property/BaseProcessor.php | 38 +++++---- src/Templates/Model.phptpl | 5 +- src/Templates/Validator/ArrayItem.phptpl | 5 +- src/Templates/Validator/ArrayTuple.phptpl | 5 +- src/Utils/RenderHelper.php | 20 +++++ 23 files changed, 369 insertions(+), 107 deletions(-) create mode 100644 src/Model/Validator/ExtractedMethodValidator.php diff --git a/src/Model/Property/AbstractProperty.php b/src/Model/Property/AbstractProperty.php index dc1153c..ace5715 100644 --- a/src/Model/Property/AbstractProperty.php +++ b/src/Model/Property/AbstractProperty.php @@ -22,6 +22,11 @@ abstract class AbstractProperty implements PropertyInterface /** @var string */ protected $attribute = ''; + /** @var callable[] */ + protected $onResolveCallbacks = []; + /** @var bool */ + protected $resolved = false; + /** * Property constructor. * @@ -58,6 +63,20 @@ public function getAttribute(bool $variableName = false): string return ($this->isInternal() ? '_' : '') . $attribute; } + public function onResolve(callable $callback): PropertyInterface + { + $this->resolved + ? $callback() + : $this->onResolveCallbacks[] = $callback; + + return $this; + } + + public function isResolved(): bool + { + return $this->resolved; + } + /** * Convert a name of a JSON-field into a valid PHP variable name to be used as class attribute * diff --git a/src/Model/Property/CompositionPropertyDecorator.php b/src/Model/Property/CompositionPropertyDecorator.php index 621ee88..a00ba7b 100644 --- a/src/Model/Property/CompositionPropertyDecorator.php +++ b/src/Model/Property/CompositionPropertyDecorator.php @@ -42,6 +42,10 @@ public function __construct(string $propertyName, JsonSchema $jsonSchema, Proper new ResolvedDefinitionsCollection([self::PROPERTY_KEY => $property]), self::PROPERTY_KEY ); + + $property->onResolve(function () { + $this->resolve(); + }); } /** diff --git a/src/Model/Property/Property.php b/src/Model/Property/Property.php index fb5e872..2187e91 100644 --- a/src/Model/Property/Property.php +++ b/src/Model/Property/Property.php @@ -37,12 +37,14 @@ class Property extends AbstractProperty /** @var Validator[] */ protected $validators = []; /** @var Schema */ - protected $schema; + protected $nestedSchema; /** @var PropertyDecoratorInterface[] */ public $decorators = []; /** @var TypeHintDecoratorInterface[] */ public $typeHintDecorators = []; + private $renderedTypeHints = []; + /** * Property constructor. * @@ -59,6 +61,9 @@ public function __construct(string $name, ?PropertyType $type, JsonSchema $jsonS $this->type = $type; $this->description = $description; + + // a concrete property doesn't need to be resolved + $this->resolved = true; } /** @@ -94,8 +99,17 @@ public function setType(PropertyType $type = null, PropertyType $outputType = nu /** * @inheritdoc */ - public function getTypeHint(bool $outputType = false): string + public function getTypeHint(bool $outputType = false, array $skipDecorators = []): string { + if (isset($this->renderedTypeHints[$outputType])) { + return $this->renderedTypeHints[$outputType]; + } + + static $skipDec = []; + + $additionalSkips = array_diff($skipDecorators, $skipDec); + $skipDec = array_merge($skipDec, $additionalSkips); + $input = [$outputType && $this->outputType !== null ? $this->outputType : $this->type]; // If the output type differs from an input type also accept the output type @@ -103,17 +117,29 @@ public function getTypeHint(bool $outputType = false): string $input = [$this->type, $this->outputType]; } - $input = join('|', array_filter(array_map(function (?PropertyType $input) use ($outputType): string { - $typeHint = $input ? $input->getName() : ''; + $input = join( + '|', + array_filter(array_map(function (?PropertyType $input) use ($outputType, $skipDec): string { + $typeHint = $input ? $input->getName() : ''; - foreach ($this->typeHintDecorators as $decorator) { - $typeHint = $decorator->decorate($typeHint, $outputType); - } + $filteredDecorators = array_filter( + $this->typeHintDecorators, + function (TypeHintDecoratorInterface $decorator) use ($skipDec) { + return !in_array(get_class($decorator), $skipDec); + } + ); - return $typeHint; - }, $input))); + foreach ($filteredDecorators as $decorator) { + $typeHint = $decorator->decorate($typeHint, $outputType); + } - return $input ?: 'mixed'; + return $typeHint; + }, $input)) + ); + + $skipDec = array_diff($skipDec, $additionalSkips); + + return $this->renderedTypeHints[$outputType] = $input ?: 'mixed'; } /** @@ -126,6 +152,14 @@ public function addTypeHintDecorator(TypeHintDecoratorInterface $typeHintDecorat return $this; } + /** + * @inheritdoc + */ + public function getTypeHintDecorators(): array + { + return $this->typeHintDecorators; + } + /** * @inheritdoc */ @@ -274,7 +308,7 @@ public function isReadOnly(): bool */ public function setNestedSchema(Schema $schema): PropertyInterface { - $this->schema = $schema; + $this->nestedSchema = $schema; return $this; } @@ -283,7 +317,7 @@ public function setNestedSchema(Schema $schema): PropertyInterface */ public function getNestedSchema(): ?Schema { - return $this->schema; + return $this->nestedSchema; } /** diff --git a/src/Model/Property/PropertyInterface.php b/src/Model/Property/PropertyInterface.php index b784d57..414a36f 100644 --- a/src/Model/Property/PropertyInterface.php +++ b/src/Model/Property/PropertyInterface.php @@ -49,10 +49,11 @@ public function setType(PropertyType $type = null, PropertyType $outputType = nu /** * @param bool $outputType If set to true the output type hint will be returned (may differ from the base type) - * + * @param string[] $skipDecorators Provide a set of decorators (FQCN) which shouldn't be applied + * (might be necessary to avoid infinite loops for recursive calls) * @return string */ - public function getTypeHint(bool $outputType = false): string; + public function getTypeHint(bool $outputType = false, array $skipDecorators = []): string; /** * @param TypeHintDecoratorInterface $typeHintDecorator @@ -61,6 +62,7 @@ public function getTypeHint(bool $outputType = false): string; */ public function addTypeHintDecorator(TypeHintDecoratorInterface $typeHintDecorator): PropertyInterface; + public function getTypeHintDecorators(): array; /** * Get a description for the property. If no description is available an empty string will be returned * @@ -203,4 +205,14 @@ public function getNestedSchema(): ?Schema; * @return JsonSchema */ public function getJsonSchema(): JsonSchema; + + /** + * Adds a callback which will be executed after the property is set up completely + */ + public function onResolve(callable $callback): PropertyInterface; + + /** + * Check if the property set up is finished + */ + public function isResolved(): bool; } diff --git a/src/Model/Property/PropertyProxy.php b/src/Model/Property/PropertyProxy.php index f152ae6..886e1d3 100644 --- a/src/Model/Property/PropertyProxy.php +++ b/src/Model/Property/PropertyProxy.php @@ -48,6 +48,23 @@ public function __construct( $this->definitionsCollection = $definitionsCollection; } + public function resolve(): PropertyInterface + { + if ($this->resolved) { + return $this; + } + + $this->resolved = true; + + foreach ($this->onResolveCallbacks as $callback) { + $callback(); + } + + $this->onResolveCallbacks = []; + + return $this; + } + /** * Get the property out of the resolved definitions collection to proxy function calls * @@ -77,9 +94,9 @@ public function setType(PropertyType $type = null, PropertyType $outputType = nu /** * @inheritdoc */ - public function getTypeHint(bool $outputType = false): string + public function getTypeHint(bool $outputType = false, array $skipDecorators = []): string { - return $this->getProperty()->getTypeHint($outputType); + return $this->getProperty()->getTypeHint($outputType, $skipDecorators); } /** @@ -89,6 +106,13 @@ public function addTypeHintDecorator(TypeHintDecoratorInterface $typeHintDecorat { return $this->getProperty()->addTypeHintDecorator($typeHintDecorator); } + /** + * @inheritdoc + */ + public function getTypeHintDecorators(): array + { + return $this->getProperty()->getTypeHintDecorators(); + } /** * @inheritdoc diff --git a/src/Model/Schema.php b/src/Model/Schema.php index 733de75..7b9c09c 100644 --- a/src/Model/Schema.php +++ b/src/Model/Schema.php @@ -56,6 +56,11 @@ class Schema /** @var SchemaDefinitionDictionary */ protected $schemaDefinitionDictionary; + /** @var int */ + private $resolvedProperties = 0; + /** @var callable[] */ + private $onAllPropertiesResolvedCallbacks = []; + /** * Schema constructor. * @@ -106,6 +111,15 @@ public function getDescription(): string return $this->description; } + public function onAllPropertiesResolved(callable $callback): self + { + $this->resolvedProperties === count($this->properties) + ? $callback() + : $this->onAllPropertiesResolvedCallbacks[] = $callback; + + return $this; + } + /** * @return PropertyInterface[] */ @@ -152,6 +166,16 @@ public function addProperty(PropertyInterface $property): self { if (!isset($this->properties[$property->getName()])) { $this->properties[$property->getName()] = $property; + + $property->onResolve(function () { + if (++$this->resolvedProperties === count($this->properties)) { + foreach ($this->onAllPropertiesResolvedCallbacks as $callback) { + $callback(); + + $this->onAllPropertiesResolvedCallbacks = []; + } + } + }); } else { // TODO tests: // testConditionalObjectProperty @@ -272,6 +296,11 @@ public function getMethods(): array return $this->methods; } + public function hasMethod(string $methodKey): bool + { + return isset($this->methods[$methodKey]); + } + /** * @return string[] */ diff --git a/src/Model/SchemaDefinition/SchemaDefinition.php b/src/Model/SchemaDefinition/SchemaDefinition.php index d24f5ba..d51cd64 100644 --- a/src/Model/SchemaDefinition/SchemaDefinition.php +++ b/src/Model/SchemaDefinition/SchemaDefinition.php @@ -31,6 +31,8 @@ class SchemaDefinition protected $schema; /** @var ResolvedDefinitionsCollection */ protected $resolvedPaths; + /** @var array */ + protected $unresolvedProxies = []; /** * SchemaDefinition constructor. @@ -92,21 +94,35 @@ public function resolveReference( $this->resolvedPaths->offsetSet($key, null); try { - $this->resolvedPaths->offsetSet($key, (new PropertyFactory(new PropertyProcessorFactory())) + $property = (new PropertyFactory(new PropertyProcessorFactory())) ->create( $propertyMetaDataCollection, $this->schemaProcessor, $this->schema, $propertyName, $this->source->withJson($jsonSchema) - ) - ); + ); + $this->resolvedPaths->offsetSet($key, $property); + + /** @var PropertyProxy $proxy */ + foreach ($this->unresolvedProxies[$key] ?? [] as $proxy) { + $proxy->resolve(); + } + + unset($this->unresolvedProxies[$key]); } catch (PHPModelGeneratorException $exception) { $this->resolvedPaths->offsetUnset($key); throw $exception; } } - return new PropertyProxy($propertyName, $this->source, $this->resolvedPaths, $key); + $proxy = new PropertyProxy($propertyName, $this->source, $this->resolvedPaths, $key); + $this->unresolvedProxies[$key][] = $proxy; + + if ($this->resolvedPaths->offsetGet($key)) { + $proxy->resolve(); + } + + return $proxy; } } diff --git a/src/Model/Validator/AbstractComposedPropertyValidator.php b/src/Model/Validator/AbstractComposedPropertyValidator.php index 2cc749e..f080814 100644 --- a/src/Model/Validator/AbstractComposedPropertyValidator.php +++ b/src/Model/Validator/AbstractComposedPropertyValidator.php @@ -11,7 +11,7 @@ * * @package PHPModelGenerator\Model\Validator */ -abstract class AbstractComposedPropertyValidator extends PropertyTemplateValidator +abstract class AbstractComposedPropertyValidator extends ExtractedMethodValidator { /** @var string */ protected $compositionProcessor; diff --git a/src/Model/Validator/AdditionalPropertiesValidator.php b/src/Model/Validator/AdditionalPropertiesValidator.php index 73913c6..e7655a9 100644 --- a/src/Model/Validator/AdditionalPropertiesValidator.php +++ b/src/Model/Validator/AdditionalPropertiesValidator.php @@ -67,6 +67,7 @@ public function __construct( new Property($propertyName ?? $schema->getClassName(), null, $propertiesStructure), DIRECTORY_SEPARATOR . 'Validator' . DIRECTORY_SEPARATOR . 'AdditionalProperties.phptpl', [ + 'schema' => $schema, 'validationProperty' => $this->validationProperty, 'additionalProperties' => RenderHelper::varExportArray( array_keys($propertiesStructure->getJson()[static::PROPERTIES_KEY] ?? []) diff --git a/src/Model/Validator/ArrayItemValidator.php b/src/Model/Validator/ArrayItemValidator.php index b39a9eb..380b68b 100644 --- a/src/Model/Validator/ArrayItemValidator.php +++ b/src/Model/Validator/ArrayItemValidator.php @@ -63,6 +63,7 @@ public function __construct( $property, DIRECTORY_SEPARATOR . 'Validator' . DIRECTORY_SEPARATOR . 'ArrayItem.phptpl', [ + 'schema' => $schema, 'nestedProperty' => $this->nestedProperty, 'viewHelper' => new RenderHelper($schemaProcessor->getGeneratorConfiguration()), 'generatorConfiguration' => $schemaProcessor->getGeneratorConfiguration(), diff --git a/src/Model/Validator/ArrayTupleValidator.php b/src/Model/Validator/ArrayTupleValidator.php index 5581751..ff4903b 100644 --- a/src/Model/Validator/ArrayTupleValidator.php +++ b/src/Model/Validator/ArrayTupleValidator.php @@ -62,6 +62,7 @@ public function __construct( new Property($propertyName, null, $propertiesStructure), DIRECTORY_SEPARATOR . 'Validator' . DIRECTORY_SEPARATOR . 'ArrayTuple.phptpl', [ + 'schema' => $schema, 'tupleProperties' => &$this->tupleProperties, 'viewHelper' => new RenderHelper($schemaProcessor->getGeneratorConfiguration()), 'generatorConfiguration' => $schemaProcessor->getGeneratorConfiguration(), diff --git a/src/Model/Validator/ComposedPropertyValidator.php b/src/Model/Validator/ComposedPropertyValidator.php index c8172fa..b1830b0 100644 --- a/src/Model/Validator/ComposedPropertyValidator.php +++ b/src/Model/Validator/ComposedPropertyValidator.php @@ -5,6 +5,7 @@ namespace PHPModelGenerator\Model\Validator; use PHPModelGenerator\Exception\ComposedValue\InvalidComposedValueException; +use PHPModelGenerator\Model\GeneratorConfiguration; use PHPModelGenerator\Model\Property\CompositionPropertyDecorator; use PHPModelGenerator\Model\Property\PropertyInterface; use PHPModelGenerator\Model\Validator; @@ -16,21 +17,15 @@ */ class ComposedPropertyValidator extends AbstractComposedPropertyValidator { - /** - * ComposedPropertyValidator constructor. - * - * @param PropertyInterface $property - * @param CompositionPropertyDecorator[] $composedProperties - * @param string $compositionProcessor - * @param array $validatorVariables - */ public function __construct( + GeneratorConfiguration $generatorConfiguration, PropertyInterface $property, array $composedProperties, string $compositionProcessor, array $validatorVariables ) { parent::__construct( + $generatorConfiguration, $property, DIRECTORY_SEPARATOR . 'Validator' . DIRECTORY_SEPARATOR . 'ComposedItem.phptpl', $validatorVariables, @@ -65,9 +60,12 @@ public function withoutNestedCompositionValidation(): self { $validator = clone $this; + /** @var CompositionPropertyDecorator $composedProperty */ foreach ($validator->composedProperties as $composedProperty) { - $composedProperty->filterValidators(function (Validator $validator): bool { - return !is_a($validator->getValidator(), AbstractComposedPropertyValidator::class); + $composedProperty->onResolve(function () use ($composedProperty) { + $composedProperty->filterValidators(function (Validator $validator): bool { + return !is_a($validator->getValidator(), AbstractComposedPropertyValidator::class); + }); }); } diff --git a/src/Model/Validator/ConditionalPropertyValidator.php b/src/Model/Validator/ConditionalPropertyValidator.php index d8e46b3..ab7a82b 100644 --- a/src/Model/Validator/ConditionalPropertyValidator.php +++ b/src/Model/Validator/ConditionalPropertyValidator.php @@ -5,7 +5,7 @@ namespace PHPModelGenerator\Model\Validator; use PHPModelGenerator\Exception\ComposedValue\ConditionalException; -use PHPModelGenerator\Model\Property\CompositionPropertyDecorator; +use PHPModelGenerator\Model\GeneratorConfiguration; use PHPModelGenerator\Model\Property\PropertyInterface; use PHPModelGenerator\PropertyProcessor\ComposedValue\IfProcessor; @@ -16,19 +16,14 @@ */ class ConditionalPropertyValidator extends AbstractComposedPropertyValidator { - /** - * ConditionalPropertyValidator constructor. - * - * @param PropertyInterface $property - * @param CompositionPropertyDecorator[] $composedProperties - * @param array $validatorVariables - */ public function __construct( + GeneratorConfiguration $generatorConfiguration, PropertyInterface $property, array $composedProperties, array $validatorVariables ) { parent::__construct( + $generatorConfiguration, $property, DIRECTORY_SEPARATOR . 'Validator' . DIRECTORY_SEPARATOR . 'ConditionalComposedItem.phptpl', $validatorVariables, diff --git a/src/Model/Validator/ExtractedMethodValidator.php b/src/Model/Validator/ExtractedMethodValidator.php new file mode 100644 index 0000000..66e615f --- /dev/null +++ b/src/Model/Validator/ExtractedMethodValidator.php @@ -0,0 +1,72 @@ +generatorConfiguration = $generatorConfiguration; + + $this->extractedMethodName = sprintf( + 'validate%s_%s_%s', + str_replace(' ', '', ucfirst($property->getAttribute())), + str_replace('Validator', '', basename(static::class)), + uniqid() + ); + + parent::__construct($property, $template, $templateValues, $exceptionClass, $exceptionParams); + } + + public function getMethod(): MethodInterface + { + return new class ($this, $this->generatorConfiguration) implements MethodInterface { + /** @var ExtractedMethodValidator */ + private $validator; + /** @var GeneratorConfiguration */ + private $generatorConfiguration; + + public function __construct( + ExtractedMethodValidator $validator, + GeneratorConfiguration $generatorConfiguration + ) { + $this->validator = $validator; + $this->generatorConfiguration = $generatorConfiguration; + } + + public function getCode(): string + { + $renderHelper = new RenderHelper($this->generatorConfiguration); + return "private function {$this->validator->getExtractedMethodName()}(&\$value): void { + {$this->validator->getValidatorSetUp()} + + if ({$this->validator->getCheck()}) { + {$renderHelper->validationError($this->validator)} + } + }"; + } + }; + } + + public function getExtractedMethodName(): string + { + return $this->extractedMethodName; + } +} diff --git a/src/PropertyProcessor/ComposedValue/AbstractComposedValueProcessor.php b/src/PropertyProcessor/ComposedValue/AbstractComposedValueProcessor.php index 8d6b184..f20c940 100644 --- a/src/PropertyProcessor/ComposedValue/AbstractComposedValueProcessor.php +++ b/src/PropertyProcessor/ComposedValue/AbstractComposedValueProcessor.php @@ -36,6 +36,8 @@ abstract class AbstractComposedValueProcessor extends AbstractValueProcessor private static $generatedMergedProperties = []; /** @var bool */ private $rootLevelComposition; + /** @var PropertyInterface|null */ + private $mergedProperty = null; /** * AbstractComposedValueProcessor constructor. @@ -73,12 +75,26 @@ protected function generateValidators(PropertyInterface $property, JsonSchema $p $compositionProperties = $this->getCompositionProperties($property, $propertySchema); - $this->transferPropertyType($property, $compositionProperties); + $resolvedCompositions = 0; + foreach ($compositionProperties as $compositionProperty) { + $compositionProperty->onResolve( + function () use (&$resolvedCompositions, $property, $compositionProperties, $propertySchema) { + if (++$resolvedCompositions === count($compositionProperties)) { + $this->transferPropertyType($property, $compositionProperties); + + $this->mergedProperty = !$this->rootLevelComposition && $this instanceof MergedComposedPropertiesInterface + ? $this->createMergedProperty($property, $compositionProperties, $propertySchema) + : null; + } + } + ); + } $availableAmount = count($compositionProperties); $property->addValidator( new ComposedPropertyValidator( + $this->schemaProcessor->getGeneratorConfiguration(), $property, $compositionProperties, static::class, @@ -89,13 +105,10 @@ protected function generateValidators(PropertyInterface $property, JsonSchema $p 'availableAmount' => $availableAmount, 'composedValueValidation' => $this->getComposedValueValidation($availableAmount), // if the property is a composed property the resulting value of a validation must be proposed - // to be the final value after the validations (eg. object instantiations may be performed). + // to be the final value after the validations (e.g. object instantiations may be performed). // Otherwise (eg. a NotProcessor) the value must be proposed before the validation 'postPropose' => $this instanceof ComposedPropertiesInterface, - 'mergedProperty' => - !$this->rootLevelComposition && $this instanceof MergedComposedPropertiesInterface - ? $this->createMergedProperty($property, $compositionProperties, $propertySchema) - : null, + 'mergedProperty' => &$this->mergedProperty, 'onlyForDefinedValues' => $propertySchema->getJson()['onlyForDefinedValues'] && $this instanceof ComposedPropertiesInterface, @@ -141,19 +154,21 @@ protected function getCompositionProperties(PropertyInterface $property, JsonSch ) ); - $compositionProperty->filterValidators(function (Validator $validator): bool { - return !is_a($validator->getValidator(), RequiredPropertyValidator::class) && - !is_a($validator->getValidator(), ComposedPropertyValidator::class); + $compositionProperty->onResolve(function () use ($compositionProperty, $property) { + $compositionProperty->filterValidators(function (Validator $validator): bool { + return !is_a($validator->getValidator(), RequiredPropertyValidator::class) && + !is_a($validator->getValidator(), ComposedPropertyValidator::class); + }); + + // only create a composed type hint if we aren't a AnyOf or an AllOf processor and the + // compositionProperty contains no object. This results in objects being composed each separately for a + // OneOf processor (e.g. string|ObjectA|ObjectB). For a merged composed property the objects are merged + // together, so it results in string|MergedObject + if (!($this instanceof MergedComposedPropertiesInterface && $compositionProperty->getNestedSchema())) { + $property->addTypeHintDecorator(new CompositionTypeHintDecorator($compositionProperty)); + } }); - // only create a composed type hint if we aren't a AnyOf or an AllOf processor and the compositionProperty - // contains no object. This results in objects being composed each separately for a OneOf processor - // (eg. string|ObjectA|ObjectB). For a merged composed property the objects are merged together so it - // results in string|MergedObject - if (!($this instanceof MergedComposedPropertiesInterface && $compositionProperty->getNestedSchema())) { - $property->addTypeHintDecorator(new CompositionTypeHintDecorator($compositionProperty)); - } - $compositionProperties[] = $compositionProperty; } @@ -296,24 +311,28 @@ private function transferPropertiesToMergedSchema(Schema $mergedPropertySchema, continue; } - foreach ($property->getNestedSchema()->getProperties() as $nestedProperty) { - $mergedPropertySchema->addProperty( - // don't validate fields in merged properties. All fields were validated before corresponding to - // the defined constraints of the composition property. - (clone $nestedProperty)->filterValidators(function (): bool { - return false; - }) - ); + $property->getNestedSchema()->onAllPropertiesResolved(function () use ($property, $mergedPropertySchema) { + foreach ($property->getNestedSchema()->getProperties() as $nestedProperty) { + $mergedPropertySchema->addProperty( + // don't validate fields in merged properties. All fields were validated before corresponding to + // the defined constraints of the composition property. + (clone $nestedProperty)->filterValidators(function (): bool { + return false; + }) + ); + + // the parent schema needs to know about all imports of the nested classes as all properties of the + // nested classes are available in the parent schema (combined schema merging) + $this->schema->addNamespaceTransferDecorator( + new SchemaNamespaceTransferDecorator($property->getNestedSchema()) + ); + } - // the parent schema needs to know about all imports of the nested classes as all properties of the - // nested classes are available in the parent schema (combined schema merging) - $this->schema->addNamespaceTransferDecorator( - new SchemaNamespaceTransferDecorator($property->getNestedSchema()) + // make sure the merged schema knows all imports of the parent schema + $mergedPropertySchema->addNamespaceTransferDecorator( + new SchemaNamespaceTransferDecorator($this->schema) ); - } - - // make sure the merged schema knows all imports of the parent schema - $mergedPropertySchema->addNamespaceTransferDecorator(new SchemaNamespaceTransferDecorator($this->schema)); + }); } } diff --git a/src/PropertyProcessor/ComposedValue/IfProcessor.php b/src/PropertyProcessor/ComposedValue/IfProcessor.php index 78ded75..456a692 100644 --- a/src/PropertyProcessor/ComposedValue/IfProcessor.php +++ b/src/PropertyProcessor/ComposedValue/IfProcessor.php @@ -67,9 +67,11 @@ protected function generateValidators(PropertyInterface $property, JsonSchema $p ) ); - $compositionProperty->filterValidators(function (Validator $validator): bool { - return !is_a($validator->getValidator(), RequiredPropertyValidator::class) && - !is_a($validator->getValidator(), ComposedPropertyValidator::class); + $compositionProperty->onResolve(function () use ($compositionProperty) { + $compositionProperty->filterValidators(function (Validator $validator): bool { + return !is_a($validator->getValidator(), RequiredPropertyValidator::class) && + !is_a($validator->getValidator(), ComposedPropertyValidator::class); + }); }); $properties[$compositionElement] = $compositionProperty; @@ -77,6 +79,7 @@ protected function generateValidators(PropertyInterface $property, JsonSchema $p $property->addValidator( new ConditionalPropertyValidator( + $this->schemaProcessor->getGeneratorConfiguration(), $property, $properties, [ diff --git a/src/PropertyProcessor/Decorator/TypeHint/ArrayTypeHintDecorator.php b/src/PropertyProcessor/Decorator/TypeHint/ArrayTypeHintDecorator.php index 94e5e6b..529b871 100644 --- a/src/PropertyProcessor/Decorator/TypeHint/ArrayTypeHintDecorator.php +++ b/src/PropertyProcessor/Decorator/TypeHint/ArrayTypeHintDecorator.php @@ -16,6 +16,8 @@ class ArrayTypeHintDecorator implements TypeHintDecoratorInterface /** @var PropertyInterface */ protected $nestedProperty; + private $recursiveArrayCheck = 0; + /** * ArrayTypeHintDecorator constructor. * @@ -31,8 +33,23 @@ public function __construct(PropertyInterface $nestedProperty) */ public function decorate(string $input, bool $outputType = false): string { - return implode('|', array_map(function (string $typeHint): string { - return "{$typeHint}[]"; - }, explode('|', $this->nestedProperty->getTypeHint($outputType)))); + // TODO: provide better type hints. Currently provides e.g. "string|array[]" instead of "string|string[]" for a recursive string array + if (++$this->recursiveArrayCheck > 1) { + return $this->nestedProperty->getTypeHint($outputType, [self::class]); + } + + $result = implode( + '|', + array_map( + function (string $typeHint): string { + return "{$typeHint}[]"; + }, + explode('|', $this->nestedProperty->getTypeHint($outputType)) + ) + ); + + $this->recursiveArrayCheck--; + + return $result; } } diff --git a/src/PropertyProcessor/Decorator/TypeHint/TypeHintTransferDecorator.php b/src/PropertyProcessor/Decorator/TypeHint/TypeHintTransferDecorator.php index 5485727..0c64c74 100644 --- a/src/PropertyProcessor/Decorator/TypeHint/TypeHintTransferDecorator.php +++ b/src/PropertyProcessor/Decorator/TypeHint/TypeHintTransferDecorator.php @@ -7,7 +7,7 @@ use PHPModelGenerator\Model\Property\PropertyInterface; /** - * Class ArrayTypeHintDecorator + * Class TypeHintTransferDecorator * * @package PHPModelGenerator\PropertyProcessor\Decorator\Property */ diff --git a/src/PropertyProcessor/Property/BaseProcessor.php b/src/PropertyProcessor/Property/BaseProcessor.php index 65eda43..9266860 100644 --- a/src/PropertyProcessor/Property/BaseProcessor.php +++ b/src/PropertyProcessor/Property/BaseProcessor.php @@ -316,23 +316,29 @@ protected function transferComposedPropertiesToSchema(PropertyInterface $propert } foreach ($validator->getComposedProperties() as $composedProperty) { - if (!$composedProperty->getNestedSchema()) { - throw new SchemaException( - sprintf( - "No nested schema for composed property %s in file %s found", - $property->getName(), - $property->getJsonSchema()->getFile() - ) + $composedProperty->onResolve(function () use ($composedProperty, $property, $validator) { + if (!$composedProperty->getNestedSchema()) { + throw new SchemaException( + sprintf( + "No nested schema for composed property %s in file %s found", + $property->getName(), + $property->getJsonSchema()->getFile() + ) + ); + } + + $composedProperty->getNestedSchema()->onAllPropertiesResolved( + function () use ($composedProperty, $validator) { + foreach ($composedProperty->getNestedSchema()->getProperties() as $property) { + $this->schema->addProperty( + $this->cloneTransferredProperty($property, $validator->getCompositionProcessor()) + ); + + $composedProperty->appendAffectedObjectProperty($property); + } + } ); - } - - foreach ($composedProperty->getNestedSchema()->getProperties() as $property) { - $this->schema->addProperty( - $this->cloneTransferredProperty($property, $validator->getCompositionProcessor()) - ); - - $composedProperty->appendAffectedObjectProperty($property); - } + }); } } } diff --git a/src/Templates/Model.phptpl b/src/Templates/Model.phptpl index ffb4d7b..c5d92a1 100644 --- a/src/Templates/Model.phptpl +++ b/src/Templates/Model.phptpl @@ -192,10 +192,7 @@ class {{ class }} {% if schema.getInterfaces() %}implements {{ viewHelper.joinCl protected function validate{{ viewHelper.ucfirst(property.getAttribute()) }}($value, array $modelData) { {% foreach property.getOrderedValidators() as validator %} - {{ validator.getValidatorSetUp() }} - if ({{ validator.getCheck() }}) { - {{ viewHelper.validationError(validator) }} - } + {{ viewHelper.renderValidator(validator, schema) }} {% endforeach %} return $value; diff --git a/src/Templates/Validator/ArrayItem.phptpl b/src/Templates/Validator/ArrayItem.phptpl index 57d527d..0c14f33 100644 --- a/src/Templates/Validator/ArrayItem.phptpl +++ b/src/Templates/Validator/ArrayItem.phptpl @@ -12,10 +12,7 @@ is_array($value) && (function (&$items) use (&$invalidItems{{ suffix }}) { {{ viewHelper.resolvePropertyDecorator(nestedProperty) }} {% foreach nestedProperty.getOrderedValidators() as validator %} - {{ validator.getValidatorSetUp() }} - if ({{ validator.getCheck() }}) { - {{ viewHelper.validationError(validator) }} - } + {{ viewHelper.renderValidator(validator, schema) }} {% endforeach %} {% if generatorConfiguration.collectErrors() %} diff --git a/src/Templates/Validator/ArrayTuple.phptpl b/src/Templates/Validator/ArrayTuple.phptpl index b4bd5ed..425e9d1 100644 --- a/src/Templates/Validator/ArrayTuple.phptpl +++ b/src/Templates/Validator/ArrayTuple.phptpl @@ -19,10 +19,7 @@ is_array($value) && (function (&$items) use (&$invalidTuples) { {{ viewHelper.resolvePropertyDecorator(tuple) }} {% foreach tuple.getOrderedValidators() as validator %} - {{ validator.getValidatorSetUp() }} - if ({{ validator.getCheck() }}) { - {{ viewHelper.validationError(validator) }} - } + {{ viewHelper.renderValidator(validator, schema) }} {% endforeach %} {% if generatorConfiguration.collectErrors() %} diff --git a/src/Utils/RenderHelper.php b/src/Utils/RenderHelper.php index fd32c94..9d98549 100644 --- a/src/Utils/RenderHelper.php +++ b/src/Utils/RenderHelper.php @@ -6,6 +6,8 @@ use PHPModelGenerator\Model\GeneratorConfiguration; use PHPModelGenerator\Model\Property\PropertyInterface; +use PHPModelGenerator\Model\Schema; +use PHPModelGenerator\Model\Validator\ExtractedMethodValidator; use PHPModelGenerator\Model\Validator\PropertyValidatorInterface; /** @@ -158,6 +160,24 @@ public function getTypeHintAnnotation(PropertyInterface $property, bool $outputT return implode('|', array_unique(explode('|', $typeHint))); } + public function renderValidator(PropertyValidatorInterface $validator, Schema $schema): string + { + if (!$validator instanceof ExtractedMethodValidator) { + return " +{$validator->getValidatorSetUp()} +if ({$validator->getCheck()}) { + {$this->validationError($validator)} +} +"; + } + + if (!$schema->hasMethod($validator->getExtractedMethodName())) { + $schema->addMethod($validator->getExtractedMethodName(), $validator->getMethod()); + } + + return "\$this->{$validator->getExtractedMethodName()}(\$value);"; + } + public static function varExportArray(array $values): string { return preg_replace('(\d+\s=>)', '', var_export($values, true)); From 1907602ca824261bda92b1c6491d74397b9d32b3 Mon Sep 17 00:00:00 2001 From: Enno Woortmann Date: Mon, 13 Mar 2023 14:25:11 +0100 Subject: [PATCH 2/8] Fix parse error --- src/Model/Validator/ExtractedMethodValidator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/Validator/ExtractedMethodValidator.php b/src/Model/Validator/ExtractedMethodValidator.php index 66e615f..bd4451b 100644 --- a/src/Model/Validator/ExtractedMethodValidator.php +++ b/src/Model/Validator/ExtractedMethodValidator.php @@ -28,7 +28,7 @@ public function __construct( $this->extractedMethodName = sprintf( 'validate%s_%s_%s', str_replace(' ', '', ucfirst($property->getAttribute())), - str_replace('Validator', '', basename(static::class)), + str_replace('Validator', '', substr(strrchr(static::class, '\\'), 1)), uniqid() ); From f80cb001718f639124b5091e59e2c20a9990b91f Mon Sep 17 00:00:00 2001 From: Enno Woortmann Date: Thu, 30 Mar 2023 10:46:11 +0200 Subject: [PATCH 3/8] Add recursive array test --- src/Model/Property/AbstractProperty.php | 5 -- src/Model/Property/PropertyInterface.php | 5 -- tests/Objects/ArrayPropertyTest.php | 48 +++++++++++++++++++ .../ArrayPropertyTest/RecursiveArray.json | 24 ++++++++++ 4 files changed, 72 insertions(+), 10 deletions(-) create mode 100644 tests/Schema/ArrayPropertyTest/RecursiveArray.json diff --git a/src/Model/Property/AbstractProperty.php b/src/Model/Property/AbstractProperty.php index ace5715..fa10656 100644 --- a/src/Model/Property/AbstractProperty.php +++ b/src/Model/Property/AbstractProperty.php @@ -72,11 +72,6 @@ public function onResolve(callable $callback): PropertyInterface return $this; } - public function isResolved(): bool - { - return $this->resolved; - } - /** * Convert a name of a JSON-field into a valid PHP variable name to be used as class attribute * diff --git a/src/Model/Property/PropertyInterface.php b/src/Model/Property/PropertyInterface.php index 414a36f..5aeccf8 100644 --- a/src/Model/Property/PropertyInterface.php +++ b/src/Model/Property/PropertyInterface.php @@ -210,9 +210,4 @@ public function getJsonSchema(): JsonSchema; * Adds a callback which will be executed after the property is set up completely */ public function onResolve(callable $callback): PropertyInterface; - - /** - * Check if the property set up is finished - */ - public function isResolved(): bool; } diff --git a/tests/Objects/ArrayPropertyTest.php b/tests/Objects/ArrayPropertyTest.php index 79e1f1e..6e1590e 100644 --- a/tests/Objects/ArrayPropertyTest.php +++ b/tests/Objects/ArrayPropertyTest.php @@ -2,6 +2,8 @@ namespace PHPModelGenerator\Tests\Objects; +use PHPModelGenerator\Exception\Arrays\InvalidItemException; +use PHPModelGenerator\Exception\Arrays\MinItemsException; use PHPModelGenerator\Exception\FileSystemException; use PHPModelGenerator\Exception\RenderException; use PHPModelGenerator\Exception\SchemaException; @@ -867,6 +869,7 @@ public function invalidObjectArrayDataProvider(): array ) ); } + public function invalidCombinedObjectArrayDataProvider(): array { return $this->combineDataProvider( @@ -951,4 +954,49 @@ public function invalidCombinedObjectArrayDataProvider(): array ) ); } + + /** + * @dataProvider validRecursiveArrayDataProvider + */ + public function testValidRecursiveArray(array $input): void + { + $className = $this->generateClassFromFile('RecursiveArray.json'); + + $object = new $className(['property' => $input]); + + $this->assertSame($input, $object->getProperty()); + } + + public function validRecursiveArrayDataProvider(): array + { + return [ + 'only string' => [['Hello']], + 'only nested array' => [[['Hello']]], + 'string and nested array' => [[['Hello'], 'World']], + 'two level nested array' => [[[['Hello'], 'World'], '!']], + ]; + } + + /** + * @dataProvider invalidRecursiveArrayDataProvider + */ + public function testInvalidRecursiveArrayThrowsAnException(string $expectedException, array $input): void + { + $this->expectException($expectedException); + + $className = $this->generateClassFromFile('RecursiveArray.json'); + + new $className(['property' => $input]); + } + + public function invalidRecursiveArrayDataProvider(): array + { + return [ + 'empty array' => [MinItemsException::class, []], + 'empty nested array' => [InvalidItemException::class, [[]]], + 'string with empty nested array' => [InvalidItemException::class, ['Hello', []]], + 'invalid type' => [InvalidItemException::class, [2]], + 'invalid nested type' => [InvalidItemException::class, ['Hello', [2]]], + ]; + } } diff --git a/tests/Schema/ArrayPropertyTest/RecursiveArray.json b/tests/Schema/ArrayPropertyTest/RecursiveArray.json new file mode 100644 index 0000000..766040f --- /dev/null +++ b/tests/Schema/ArrayPropertyTest/RecursiveArray.json @@ -0,0 +1,24 @@ +{ + "definitions": { + "list": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/list" + }, + { + "type": "string" + } + ] + }, + "minItems": 1 + } + }, + "type": "object", + "properties": { + "property": { + "$ref": "#/definitions/list" + } + } +} \ No newline at end of file From 6e9e9a497067301e509313f6bdbe7e8b8a825e36 Mon Sep 17 00:00:00 2001 From: Enno Woortmann Date: Thu, 30 Mar 2023 13:23:08 +0200 Subject: [PATCH 4/8] Issue #65. Add onResolved callback to validators to execute further post-processing of the validator and properties holding the validator only after the validator has been created completely. This is required as a validator might hold a nested property to execute the validation. This nested property might be a PropertyProxy due to recursion which might cause a crash of the generation process if the property hasn't been created completely before post-processing the property and the validator. --- src/Model/Property/AbstractProperty.php | 21 ++---- .../Property/CompositionPropertyDecorator.php | 2 +- src/Model/Property/Property.php | 23 +++++-- src/Model/Property/PropertyInterface.php | 8 +-- src/Model/Property/PropertyProxy.php | 28 ++------ src/Model/RenderJob.php | 4 +- src/Model/Schema.php | 6 +- .../SchemaDefinition/SchemaDefinition.php | 2 + .../Validator/AbstractPropertyValidator.php | 9 ++- .../AdditionalPropertiesValidator.php | 4 ++ src/Model/Validator/ArrayItemValidator.php | 9 ++- src/Model/Validator/ArrayTupleValidator.php | 19 +++++- .../Validator/ComposedPropertyValidator.php | 6 +- .../ConditionalPropertyValidator.php | 2 + .../Validator/ExtractedMethodValidator.php | 7 +- src/Model/Validator/FilterValidator.php | 6 +- src/Model/Validator/FormatValidator.php | 1 + .../Validator/MultiTypeCheckValidator.php | 2 +- .../NoAdditionalPropertiesValidator.php | 2 + .../Validator/PatternPropertiesValidator.php | 4 ++ .../Validator/PropertyDependencyValidator.php | 2 + .../Validator/PropertyNamesValidator.php | 4 +- src/Model/Validator/PropertyValidator.php | 1 + .../Validator/PropertyValidatorInterface.php | 3 +- .../Validator/SchemaDependencyValidator.php | 2 + .../AbstractComposedValueProcessor.php | 55 ++++++++-------- .../ComposedValue/IfProcessor.php | 4 +- .../TypeHint/ArrayTypeHintDecorator.php | 2 +- .../Filter/FilterProcessor.php | 4 +- .../Property/AbstractPropertyProcessor.php | 6 +- .../Property/BaseProcessor.php | 6 +- .../Property/MultiTypeProcessor.php | 66 +++++++++++-------- .../Hook/SchemaHookResolver.php | 4 +- ...itionalPropertiesAccessorPostProcessor.php | 4 +- .../CompositionValidationPostProcessor.php | 2 +- ...MatchingPatternPropertiesPostProcessor.php | 4 +- .../PatternPropertiesPostProcessor.php | 5 +- ...PatternPropertiesAccessorPostProcessor.php | 10 +-- src/SchemaProcessor/SchemaProcessor.php | 2 +- src/Templates/Model.phptpl | 4 +- src/Templates/Validator/ArrayUnique.phptpl | 2 +- .../Validator/NoAdditionalProperties.phptpl | 4 +- .../Validator/PropertyDependency.phptpl | 2 +- src/Utils/RenderHelper.php | 12 ++++ src/Utils/ResolvableInterface.php | 18 +++++ src/Utils/ResolvableTrait.php | 40 +++++++++++ tests/AbstractPHPModelGeneratorTest.php | 4 +- tests/Basic/BasicSchemaGenerationTest.php | 2 +- tests/Basic/SchemaHookTest.php | 2 +- tests/Objects/EnumPropertyTest.php | 2 +- tests/Objects/MultiTypePropertyTest.php | 21 ++++++ ...nalPropertiesAccessorPostProcessorTest.php | 12 ++-- ...ernPropertiesAccessorPostProcessorTest.php | 2 +- .../PopulatePostProcessorTest.php | 6 +- .../RecursiveMultiTypeProperty.json | 18 +++++ .../SchemaProvider/OpenAPIv3ProviderTest.php | 12 ++-- 56 files changed, 339 insertions(+), 175 deletions(-) create mode 100644 src/Utils/ResolvableInterface.php create mode 100644 src/Utils/ResolvableTrait.php create mode 100644 tests/Schema/MultiTypePropertyTest/RecursiveMultiTypeProperty.json diff --git a/src/Model/Property/AbstractProperty.php b/src/Model/Property/AbstractProperty.php index fa10656..ade0c08 100644 --- a/src/Model/Property/AbstractProperty.php +++ b/src/Model/Property/AbstractProperty.php @@ -7,6 +7,7 @@ use PHPModelGenerator\Exception\SchemaException; use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; use PHPModelGenerator\Model\SchemaDefinition\JsonSchemaTrait; +use PHPModelGenerator\Utils\ResolvableTrait; /** * Class AbstractProperty @@ -15,18 +16,13 @@ */ abstract class AbstractProperty implements PropertyInterface { - use JsonSchemaTrait; + use JsonSchemaTrait, ResolvableTrait; /** @var string */ protected $name = ''; /** @var string */ protected $attribute = ''; - /** @var callable[] */ - protected $onResolveCallbacks = []; - /** @var bool */ - protected $resolved = false; - /** * Property constructor. * @@ -63,15 +59,6 @@ public function getAttribute(bool $variableName = false): string return ($this->isInternal() ? '_' : '') . $attribute; } - public function onResolve(callable $callback): PropertyInterface - { - $this->resolved - ? $callback() - : $this->onResolveCallbacks[] = $callback; - - return $this; - } - /** * Convert a name of a JSON-field into a valid PHP variable name to be used as class attribute * @@ -85,14 +72,14 @@ protected function processAttributeName(string $name): string { $attributeName = preg_replace_callback( '/([a-z][a-z0-9]*)([A-Z])/', - function ($matches) { + static function (array $matches): string { return "{$matches[1]}-{$matches[2]}"; }, $name ); $elements = array_map( - function ($element) { + static function (string $element): string { return ucfirst(strtolower($element)); }, preg_split('/[^a-z0-9]/i', $attributeName) diff --git a/src/Model/Property/CompositionPropertyDecorator.php b/src/Model/Property/CompositionPropertyDecorator.php index a00ba7b..25ea933 100644 --- a/src/Model/Property/CompositionPropertyDecorator.php +++ b/src/Model/Property/CompositionPropertyDecorator.php @@ -43,7 +43,7 @@ public function __construct(string $propertyName, JsonSchema $jsonSchema, Proper self::PROPERTY_KEY ); - $property->onResolve(function () { + $property->onResolve(function (): void { $this->resolve(); }); } diff --git a/src/Model/Property/Property.php b/src/Model/Property/Property.php index 2187e91..d925158 100644 --- a/src/Model/Property/Property.php +++ b/src/Model/Property/Property.php @@ -44,6 +44,8 @@ class Property extends AbstractProperty public $typeHintDecorators = []; private $renderedTypeHints = []; + /** @var int Track the amount of unresolved validators */ + private $pendingValidators = 0; /** * Property constructor. @@ -62,8 +64,7 @@ public function __construct(string $name, ?PropertyType $type, JsonSchema $jsonS $this->type = $type; $this->description = $description; - // a concrete property doesn't need to be resolved - $this->resolved = true; + $this->resolve(); } /** @@ -124,7 +125,7 @@ public function getTypeHint(bool $outputType = false, array $skipDecorators = [] $filteredDecorators = array_filter( $this->typeHintDecorators, - function (TypeHintDecoratorInterface $decorator) use ($skipDec) { + static function (TypeHintDecoratorInterface $decorator) use ($skipDec): bool { return !in_array(get_class($decorator), $skipDec); } ); @@ -173,6 +174,18 @@ public function getDescription(): string */ public function addValidator(PropertyValidatorInterface $validator, int $priority = 99): PropertyInterface { + if (!$validator->isResolved()) { + $this->isResolved = false; + + $this->pendingValidators++; + + $validator->onResolve(function () { + if (--$this->pendingValidators === 0) { + $this->resolve(); + } + }); + } + $this->validators[] = new Validator($validator, $priority); return $this; @@ -203,7 +216,7 @@ public function getOrderedValidators(): array { usort( $this->validators, - function (Validator $validator, Validator $comparedValidator) { + static function (Validator $validator, Validator $comparedValidator): int { if ($validator->getPriority() == $comparedValidator->getPriority()) { return 0; } @@ -212,7 +225,7 @@ function (Validator $validator, Validator $comparedValidator) { ); return array_map( - function (Validator $validator) { + static function (Validator $validator): PropertyValidatorInterface { return $validator->getValidator(); }, $this->validators diff --git a/src/Model/Property/PropertyInterface.php b/src/Model/Property/PropertyInterface.php index 5aeccf8..f039e40 100644 --- a/src/Model/Property/PropertyInterface.php +++ b/src/Model/Property/PropertyInterface.php @@ -10,13 +10,14 @@ use PHPModelGenerator\Model\Validator\PropertyValidatorInterface; use PHPModelGenerator\PropertyProcessor\Decorator\Property\PropertyDecoratorInterface; use PHPModelGenerator\PropertyProcessor\Decorator\TypeHint\TypeHintDecoratorInterface; +use PHPModelGenerator\Utils\ResolvableInterface; /** * Interface PropertyInterface * * @package PHPModelGenerator\Model */ -interface PropertyInterface +interface PropertyInterface extends ResolvableInterface { /** * @return string @@ -205,9 +206,4 @@ public function getNestedSchema(): ?Schema; * @return JsonSchema */ public function getJsonSchema(): JsonSchema; - - /** - * Adds a callback which will be executed after the property is set up completely - */ - public function onResolve(callable $callback): PropertyInterface; } diff --git a/src/Model/Property/PropertyProxy.php b/src/Model/Property/PropertyProxy.php index 886e1d3..619131e 100644 --- a/src/Model/Property/PropertyProxy.php +++ b/src/Model/Property/PropertyProxy.php @@ -28,7 +28,7 @@ class PropertyProxy extends AbstractProperty * PropertyProxy constructor. * * @param string $name The name must be provided separately as the name is not bound to the structure of a - * referenced schema. Consequently two properties with different names can refer an identical schema utilizing the + * referenced schema. Consequently, two properties with different names can refer an identical schema utilizing the * PropertyProxy. By providing a name to each of the proxies the resulting properties will get the correct names. * @param JsonSchema $jsonSchema * @param ResolvedDefinitionsCollection $definitionsCollection @@ -48,23 +48,6 @@ public function __construct( $this->definitionsCollection = $definitionsCollection; } - public function resolve(): PropertyInterface - { - if ($this->resolved) { - return $this; - } - - $this->resolved = true; - - foreach ($this->onResolveCallbacks as $callback) { - $callback(); - } - - $this->onResolveCallbacks = []; - - return $this; - } - /** * Get the property out of the resolved definitions collection to proxy function calls * @@ -151,9 +134,12 @@ public function filterValidators(callable $filter): PropertyInterface */ public function getOrderedValidators(): array { - return array_map(function (PropertyValidatorInterface $propertyValidator): PropertyValidatorInterface { - return $propertyValidator->withProperty($this); - }, $this->getProperty()->getOrderedValidators()); + return array_map( + function (PropertyValidatorInterface $propertyValidator): PropertyValidatorInterface { + return $propertyValidator->withProperty($this); + }, + $this->getProperty()->getOrderedValidators() + ); } /** diff --git a/src/Model/RenderJob.php b/src/Model/RenderJob.php index ae9e79f..6c618d8 100644 --- a/src/Model/RenderJob.php +++ b/src/Model/RenderJob.php @@ -136,7 +136,7 @@ protected function renderClass(GeneratorConfiguration $generatorConfiguration): 'true' => true, 'baseValidatorsWithoutCompositions' => array_filter( $this->schema->getBaseValidators(), - function ($validator) { + static function ($validator): bool { return !is_a($validator, AbstractComposedPropertyValidator::class); } ), @@ -167,7 +167,7 @@ protected function getUseForSchema(GeneratorConfiguration $generatorConfiguratio ); // filter out non-compound uses and uses which link to the current namespace - $use = array_filter($use, function ($classPath) use ($namespace) { + $use = array_filter($use, static function ($classPath) use ($namespace): bool { return strstr(trim(str_replace("$namespace", '', $classPath), '\\'), '\\') || (!strstr($classPath, '\\') && !empty($namespace)); }); diff --git a/src/Model/Schema.php b/src/Model/Schema.php index 7b9c09c..ed7a17a 100644 --- a/src/Model/Schema.php +++ b/src/Model/Schema.php @@ -125,7 +125,7 @@ public function onAllPropertiesResolved(callable $callback): self */ public function getProperties(): array { - $hasSchemaDependencyValidator = function (PropertyInterface $property): bool { + $hasSchemaDependencyValidator = static function (PropertyInterface $property): bool { foreach ($property->getValidators() as $validator) { if ($validator->getValidator() instanceof SchemaDependencyValidator) { return true; @@ -139,7 +139,7 @@ public function getProperties(): array // of the validation process for correct exception order of the messages usort( $this->properties, - function ( + static function ( PropertyInterface $property, PropertyInterface $comparedProperty ) use ($hasSchemaDependencyValidator): int { @@ -167,7 +167,7 @@ public function addProperty(PropertyInterface $property): self if (!isset($this->properties[$property->getName()])) { $this->properties[$property->getName()] = $property; - $property->onResolve(function () { + $property->onResolve(function (): void { if (++$this->resolvedProperties === count($this->properties)) { foreach ($this->onAllPropertiesResolvedCallbacks as $callback) { $callback(); diff --git a/src/Model/SchemaDefinition/SchemaDefinition.php b/src/Model/SchemaDefinition/SchemaDefinition.php index d51cd64..17af985 100644 --- a/src/Model/SchemaDefinition/SchemaDefinition.php +++ b/src/Model/SchemaDefinition/SchemaDefinition.php @@ -110,6 +110,8 @@ public function resolveReference( } unset($this->unresolvedProxies[$key]); + + return $property; } catch (PHPModelGeneratorException $exception) { $this->resolvedPaths->offsetUnset($key); throw $exception; diff --git a/src/Model/Validator/AbstractPropertyValidator.php b/src/Model/Validator/AbstractPropertyValidator.php index 69f7561..2e9990c 100644 --- a/src/Model/Validator/AbstractPropertyValidator.php +++ b/src/Model/Validator/AbstractPropertyValidator.php @@ -6,6 +6,7 @@ use PHPModelGenerator\Model\Property\PropertyInterface; use PHPModelGenerator\Model\Validator; +use PHPModelGenerator\Utils\ResolvableTrait; /** * Class AbstractPropertyValidator @@ -14,6 +15,8 @@ */ abstract class AbstractPropertyValidator implements PropertyValidatorInterface { + use ResolvableTrait; + /** @var string */ protected $exceptionClass; /** @var array */ @@ -79,8 +82,10 @@ public function getValidatorSetUp(): string */ protected function removeRequiredPropertyValidator(PropertyInterface $property): void { - $property->filterValidators(function (Validator $validator): bool { - return !is_a($validator->getValidator(), RequiredPropertyValidator::class); + $property->onResolve(static function () use ($property): void { + $property->filterValidators(static function (Validator $validator): bool { + return !is_a($validator->getValidator(), RequiredPropertyValidator::class); + }); }); } } diff --git a/src/Model/Validator/AdditionalPropertiesValidator.php b/src/Model/Validator/AdditionalPropertiesValidator.php index e7655a9..494fd9f 100644 --- a/src/Model/Validator/AdditionalPropertiesValidator.php +++ b/src/Model/Validator/AdditionalPropertiesValidator.php @@ -61,6 +61,10 @@ public function __construct( $propertiesStructure->withJson($propertiesStructure->getJson()[static::ADDITIONAL_PROPERTIES_KEY]) ); + $this->validationProperty->onResolve(function (): void { + $this->resolve(); + }); + $patternProperties = array_keys($schema->getJsonSchema()->getJson()['patternProperties'] ?? []); parent::__construct( diff --git a/src/Model/Validator/ArrayItemValidator.php b/src/Model/Validator/ArrayItemValidator.php index 380b68b..65b91ca 100644 --- a/src/Model/Validator/ArrayItemValidator.php +++ b/src/Model/Validator/ArrayItemValidator.php @@ -21,7 +21,7 @@ * * @package PHPModelGenerator\Model\Validator */ -class ArrayItemValidator extends PropertyTemplateValidator +class ArrayItemValidator extends ExtractedMethodValidator { /** @var string */ private $variableSuffix = ''; @@ -57,9 +57,14 @@ public function __construct( $itemStructure ); - $property->addTypeHintDecorator(new ArrayTypeHintDecorator($this->nestedProperty)); + $this->nestedProperty->onResolve(function () use ($property): void { + $this->resolve(); + + $property->addTypeHintDecorator(new ArrayTypeHintDecorator($this->nestedProperty)); + }); parent::__construct( + $schemaProcessor->getGeneratorConfiguration(), $property, DIRECTORY_SEPARATOR . 'Validator' . DIRECTORY_SEPARATOR . 'ArrayItem.phptpl', [ diff --git a/src/Model/Validator/ArrayTupleValidator.php b/src/Model/Validator/ArrayTupleValidator.php index ff4903b..84a6903 100644 --- a/src/Model/Validator/ArrayTupleValidator.php +++ b/src/Model/Validator/ArrayTupleValidator.php @@ -45,17 +45,34 @@ public function __construct( $propertyFactory = new PropertyFactory(new PropertyProcessorFactory()); $this->tupleProperties = []; + + $pendingTuples = 0; foreach ($propertiesStructure->getJson() as $tupleIndex => $tupleItem) { $tupleItemName = "tuple item #$tupleIndex of array $propertyName"; // an item of the array behaves like a nested property to add item-level validation - $this->tupleProperties[] = $propertyFactory->create( + $tupleProperty = $propertyFactory->create( new PropertyMetaDataCollection([$tupleItemName]), $schemaProcessor, $schema, $tupleItemName, $propertiesStructure->withJson($tupleItem) ); + + if (!$tupleProperty->isResolved()) { + $pendingTuples++; + $tupleProperty->onResolve(function () use (&$pendingTuples): void { + if (--$pendingTuples === 0) { + $this->resolve(); + } + }); + } + + $this->tupleProperties[] = $tupleProperty; + } + + if ($pendingTuples === 0) { + $this->resolve(); } parent::__construct( diff --git a/src/Model/Validator/ComposedPropertyValidator.php b/src/Model/Validator/ComposedPropertyValidator.php index b1830b0..c1b4858 100644 --- a/src/Model/Validator/ComposedPropertyValidator.php +++ b/src/Model/Validator/ComposedPropertyValidator.php @@ -24,6 +24,8 @@ public function __construct( string $compositionProcessor, array $validatorVariables ) { + $this->isResolved = true; + parent::__construct( $generatorConfiguration, $property, @@ -62,8 +64,8 @@ public function withoutNestedCompositionValidation(): self /** @var CompositionPropertyDecorator $composedProperty */ foreach ($validator->composedProperties as $composedProperty) { - $composedProperty->onResolve(function () use ($composedProperty) { - $composedProperty->filterValidators(function (Validator $validator): bool { + $composedProperty->onResolve(static function () use ($composedProperty): void { + $composedProperty->filterValidators(static function (Validator $validator): bool { return !is_a($validator->getValidator(), AbstractComposedPropertyValidator::class); }); }); diff --git a/src/Model/Validator/ConditionalPropertyValidator.php b/src/Model/Validator/ConditionalPropertyValidator.php index ab7a82b..70d9a16 100644 --- a/src/Model/Validator/ConditionalPropertyValidator.php +++ b/src/Model/Validator/ConditionalPropertyValidator.php @@ -22,6 +22,8 @@ public function __construct( array $composedProperties, array $validatorVariables ) { + $this->isResolved = true; + parent::__construct( $generatorConfiguration, $property, diff --git a/src/Model/Validator/ExtractedMethodValidator.php b/src/Model/Validator/ExtractedMethodValidator.php index bd4451b..9574045 100644 --- a/src/Model/Validator/ExtractedMethodValidator.php +++ b/src/Model/Validator/ExtractedMethodValidator.php @@ -9,8 +9,13 @@ use PHPModelGenerator\Model\Property\PropertyInterface; use PHPModelGenerator\Utils\RenderHelper; -class ExtractedMethodValidator extends PropertyTemplateValidator +/** + * Renders the validator in a separate method. Might be required for recursive validations which would otherwise cause + * infinite loops during validator rendering + */ +abstract class ExtractedMethodValidator extends PropertyTemplateValidator { + /** @var string */ private $extractedMethodName; /** @var GeneratorConfiguration */ private $generatorConfiguration; diff --git a/src/Model/Validator/FilterValidator.php b/src/Model/Validator/FilterValidator.php index 22523c3..dd87c00 100644 --- a/src/Model/Validator/FilterValidator.php +++ b/src/Model/Validator/FilterValidator.php @@ -46,6 +46,8 @@ public function __construct( array $filterOptions = [], ?TransformingFilterInterface $transformingFilter = null ) { + $this->isResolved = true; + $this->filter = $filter; $this->filterOptions = $filterOptions; @@ -62,7 +64,7 @@ public function __construct( // check if the given value has a type matched by the filter 'typeCheck' => !empty($filter->getAcceptedTypes()) ? '(' . - implode(' && ', array_map(function (string $type) use ($property): string { + implode(' && ', array_map(static function (string $type) use ($property): string { return ReflectionTypeCheckValidator::fromType($type, $property)->getCheck(); }, $this->mapDataTypes($filter->getAcceptedTypes()))) . ')' @@ -209,7 +211,7 @@ private function validateFilterCompatibilityWithTransformedType( */ private function mapDataTypes(array $acceptedTypes): array { - return array_map(function (string $jsonSchemaType): string { + return array_map(static function (string $jsonSchemaType): string { switch ($jsonSchemaType) { case 'integer': return 'int'; case 'number': return 'float'; diff --git a/src/Model/Validator/FormatValidator.php b/src/Model/Validator/FormatValidator.php index 76be1a1..a4e692a 100644 --- a/src/Model/Validator/FormatValidator.php +++ b/src/Model/Validator/FormatValidator.php @@ -31,6 +31,7 @@ public function __construct( FormatValidatorInterface $validator, array $exceptionParams = [] ) { + $this->isResolved = true; $this->validator = $validator; parent::__construct($property, FormatException::class, $exceptionParams); diff --git a/src/Model/Validator/MultiTypeCheckValidator.php b/src/Model/Validator/MultiTypeCheckValidator.php index 4f2c0e0..f7efe6b 100644 --- a/src/Model/Validator/MultiTypeCheckValidator.php +++ b/src/Model/Validator/MultiTypeCheckValidator.php @@ -38,7 +38,7 @@ public function __construct(array $types, PropertyInterface $property, bool $all join( ' && ', array_map( - function (string $allowedType) use ($property) : string { + static function (string $allowedType) use ($property) : string { return ReflectionTypeCheckValidator::fromType($allowedType, $property)->getCheck(); }, $types diff --git a/src/Model/Validator/NoAdditionalPropertiesValidator.php b/src/Model/Validator/NoAdditionalPropertiesValidator.php index f5610c3..aa53fce 100644 --- a/src/Model/Validator/NoAdditionalPropertiesValidator.php +++ b/src/Model/Validator/NoAdditionalPropertiesValidator.php @@ -23,6 +23,8 @@ class NoAdditionalPropertiesValidator extends PropertyTemplateValidator */ public function __construct(PropertyInterface $property, array $json) { + $this->isResolved = true; + parent::__construct( $property, DIRECTORY_SEPARATOR . 'Validator' . DIRECTORY_SEPARATOR . 'NoAdditionalProperties.phptpl', diff --git a/src/Model/Validator/PatternPropertiesValidator.php b/src/Model/Validator/PatternPropertiesValidator.php index 470b6ef..c28ea3b 100644 --- a/src/Model/Validator/PatternPropertiesValidator.php +++ b/src/Model/Validator/PatternPropertiesValidator.php @@ -59,6 +59,10 @@ public function __construct( $propertyStructure ); + $this->validationProperty->onResolve(function (): void { + $this->resolve(); + }); + parent::__construct( new Property($schema->getClassName(), null, $propertyStructure), DIRECTORY_SEPARATOR . 'Validator' . DIRECTORY_SEPARATOR . 'PatternProperties.phptpl', diff --git a/src/Model/Validator/PropertyDependencyValidator.php b/src/Model/Validator/PropertyDependencyValidator.php index 250cdcb..2c1f17f 100644 --- a/src/Model/Validator/PropertyDependencyValidator.php +++ b/src/Model/Validator/PropertyDependencyValidator.php @@ -23,6 +23,8 @@ class PropertyDependencyValidator extends PropertyTemplateValidator */ public function __construct(PropertyInterface $property, array $dependencies) { + $this->isResolved = true; + parent::__construct( $property, DIRECTORY_SEPARATOR . 'Validator' . DIRECTORY_SEPARATOR . 'PropertyDependency.phptpl', diff --git a/src/Model/Validator/PropertyNamesValidator.php b/src/Model/Validator/PropertyNamesValidator.php index f8859be..6f753f1 100644 --- a/src/Model/Validator/PropertyNamesValidator.php +++ b/src/Model/Validator/PropertyNamesValidator.php @@ -36,10 +36,12 @@ public function __construct( Schema $schema, JsonSchema $propertiesNames ) { + $this->isResolved = true; + $nameValidationProperty = (new StringProcessor(new PropertyMetaDataCollection(), $schemaProcessor, $schema)) ->process('property name', $propertiesNames) // the property name validator doesn't need type checks or required checks so simply filter them out - ->filterValidators(function (Validator $validator): bool { + ->filterValidators(static function (Validator $validator): bool { return !is_a($validator->getValidator(), RequiredPropertyValidator::class) && !is_a($validator->getValidator(), TypeCheckValidator::class); }); diff --git a/src/Model/Validator/PropertyValidator.php b/src/Model/Validator/PropertyValidator.php index 96b286e..6026081 100644 --- a/src/Model/Validator/PropertyValidator.php +++ b/src/Model/Validator/PropertyValidator.php @@ -30,6 +30,7 @@ public function __construct( string $exceptionClass, array $exceptionParams = [] ) { + $this->isResolved = true; $this->check = $check; parent::__construct($property, $exceptionClass, $exceptionParams); diff --git a/src/Model/Validator/PropertyValidatorInterface.php b/src/Model/Validator/PropertyValidatorInterface.php index 850829d..9893186 100644 --- a/src/Model/Validator/PropertyValidatorInterface.php +++ b/src/Model/Validator/PropertyValidatorInterface.php @@ -5,13 +5,14 @@ namespace PHPModelGenerator\Model\Validator; use PHPModelGenerator\Model\Property\PropertyInterface; +use PHPModelGenerator\Utils\ResolvableInterface; /** * Interface PropertyValidatorInterface * * @package PHPModelGenerator\Model\Validator */ -interface PropertyValidatorInterface +interface PropertyValidatorInterface extends ResolvableInterface { /** * Get the source code for the check to perform diff --git a/src/Model/Validator/SchemaDependencyValidator.php b/src/Model/Validator/SchemaDependencyValidator.php index b5468af..01134b9 100644 --- a/src/Model/Validator/SchemaDependencyValidator.php +++ b/src/Model/Validator/SchemaDependencyValidator.php @@ -31,6 +31,8 @@ class SchemaDependencyValidator extends PropertyTemplateValidator */ public function __construct(SchemaProcessor $schemaProcessor, PropertyInterface $property, Schema $schema) { + $this->isResolved = true; + parent::__construct( $property, DIRECTORY_SEPARATOR . 'Validator' . DIRECTORY_SEPARATOR . 'SchemaDependency.phptpl', diff --git a/src/PropertyProcessor/ComposedValue/AbstractComposedValueProcessor.php b/src/PropertyProcessor/ComposedValue/AbstractComposedValueProcessor.php index f20c940..13bade1 100644 --- a/src/PropertyProcessor/ComposedValue/AbstractComposedValueProcessor.php +++ b/src/PropertyProcessor/ComposedValue/AbstractComposedValueProcessor.php @@ -78,13 +78,14 @@ protected function generateValidators(PropertyInterface $property, JsonSchema $p $resolvedCompositions = 0; foreach ($compositionProperties as $compositionProperty) { $compositionProperty->onResolve( - function () use (&$resolvedCompositions, $property, $compositionProperties, $propertySchema) { + function () use (&$resolvedCompositions, $property, $compositionProperties, $propertySchema): void { if (++$resolvedCompositions === count($compositionProperties)) { $this->transferPropertyType($property, $compositionProperties); - $this->mergedProperty = !$this->rootLevelComposition && $this instanceof MergedComposedPropertiesInterface - ? $this->createMergedProperty($property, $compositionProperties, $propertySchema) - : null; + $this->mergedProperty = !$this->rootLevelComposition + && $this instanceof MergedComposedPropertiesInterface + ? $this->createMergedProperty($property, $compositionProperties, $propertySchema) + : null; } } ); @@ -154,8 +155,8 @@ protected function getCompositionProperties(PropertyInterface $property, JsonSch ) ); - $compositionProperty->onResolve(function () use ($compositionProperty, $property) { - $compositionProperty->filterValidators(function (Validator $validator): bool { + $compositionProperty->onResolve(function () use ($compositionProperty, $property): void { + $compositionProperty->filterValidators(static function (Validator $validator): bool { return !is_a($validator->getValidator(), RequiredPropertyValidator::class) && !is_a($validator->getValidator(), ComposedPropertyValidator::class); }); @@ -186,7 +187,7 @@ private function transferPropertyType(PropertyInterface $property, array $compos $compositionPropertyTypes = array_values( array_unique( array_map( - function (CompositionPropertyDecorator $property): string { + static function (CompositionPropertyDecorator $property): string { return $property->getType() ? $property->getType()->getName() : ''; }, $compositionProperties @@ -311,28 +312,30 @@ private function transferPropertiesToMergedSchema(Schema $mergedPropertySchema, continue; } - $property->getNestedSchema()->onAllPropertiesResolved(function () use ($property, $mergedPropertySchema) { - foreach ($property->getNestedSchema()->getProperties() as $nestedProperty) { - $mergedPropertySchema->addProperty( - // don't validate fields in merged properties. All fields were validated before corresponding to - // the defined constraints of the composition property. - (clone $nestedProperty)->filterValidators(function (): bool { - return false; - }) - ); + $property->getNestedSchema()->onAllPropertiesResolved( + function () use ($property, $mergedPropertySchema): void { + foreach ($property->getNestedSchema()->getProperties() as $nestedProperty) { + $mergedPropertySchema->addProperty( + // don't validate fields in merged properties. All fields were validated before + // corresponding to the defined constraints of the composition property. + (clone $nestedProperty)->filterValidators(static function (): bool { + return false; + }) + ); + + // the parent schema needs to know about all imports of the nested classes as all properties + // of the nested classes are available in the parent schema (combined schema merging) + $this->schema->addNamespaceTransferDecorator( + new SchemaNamespaceTransferDecorator($property->getNestedSchema()) + ); + } - // the parent schema needs to know about all imports of the nested classes as all properties of the - // nested classes are available in the parent schema (combined schema merging) - $this->schema->addNamespaceTransferDecorator( - new SchemaNamespaceTransferDecorator($property->getNestedSchema()) + // make sure the merged schema knows all imports of the parent schema + $mergedPropertySchema->addNamespaceTransferDecorator( + new SchemaNamespaceTransferDecorator($this->schema) ); } - - // make sure the merged schema knows all imports of the parent schema - $mergedPropertySchema->addNamespaceTransferDecorator( - new SchemaNamespaceTransferDecorator($this->schema) - ); - }); + ); } } diff --git a/src/PropertyProcessor/ComposedValue/IfProcessor.php b/src/PropertyProcessor/ComposedValue/IfProcessor.php index 456a692..6ef67ae 100644 --- a/src/PropertyProcessor/ComposedValue/IfProcessor.php +++ b/src/PropertyProcessor/ComposedValue/IfProcessor.php @@ -67,8 +67,8 @@ protected function generateValidators(PropertyInterface $property, JsonSchema $p ) ); - $compositionProperty->onResolve(function () use ($compositionProperty) { - $compositionProperty->filterValidators(function (Validator $validator): bool { + $compositionProperty->onResolve(static function () use ($compositionProperty): void { + $compositionProperty->filterValidators(static function (Validator $validator): bool { return !is_a($validator->getValidator(), RequiredPropertyValidator::class) && !is_a($validator->getValidator(), ComposedPropertyValidator::class); }); diff --git a/src/PropertyProcessor/Decorator/TypeHint/ArrayTypeHintDecorator.php b/src/PropertyProcessor/Decorator/TypeHint/ArrayTypeHintDecorator.php index 529b871..114de5e 100644 --- a/src/PropertyProcessor/Decorator/TypeHint/ArrayTypeHintDecorator.php +++ b/src/PropertyProcessor/Decorator/TypeHint/ArrayTypeHintDecorator.php @@ -41,7 +41,7 @@ public function decorate(string $input, bool $outputType = false): string $result = implode( '|', array_map( - function (string $typeHint): string { + static function (string $typeHint): string { return "{$typeHint}[]"; }, explode('|', $this->nestedProperty->getTypeHint($outputType)) diff --git a/src/PropertyProcessor/Filter/FilterProcessor.php b/src/PropertyProcessor/Filter/FilterProcessor.php index a12d716..9562c62 100644 --- a/src/PropertyProcessor/Filter/FilterProcessor.php +++ b/src/PropertyProcessor/Filter/FilterProcessor.php @@ -168,7 +168,7 @@ private function addTransformedValuePassThrough( } if ($validator instanceof EnumValidator) { - $property->filterValidators(function (Validator $validator): bool { + $property->filterValidators(static function (Validator $validator): bool { return !is_a($validator->getValidator(), EnumValidator::class); }); @@ -206,7 +206,7 @@ private function extendTypeCheckValidatorToAllowTransformedValue( ): void { $typeCheckValidator = null; - $property->filterValidators(function (Validator $validator) use (&$typeCheckValidator): bool { + $property->filterValidators(static function (Validator $validator) use (&$typeCheckValidator): bool { if (is_a($validator->getValidator(), TypeCheckValidator::class)) { $typeCheckValidator = $validator->getValidator(); return false; diff --git a/src/PropertyProcessor/Property/AbstractPropertyProcessor.php b/src/PropertyProcessor/Property/AbstractPropertyProcessor.php index 6829df4..f665913 100644 --- a/src/PropertyProcessor/Property/AbstractPropertyProcessor.php +++ b/src/PropertyProcessor/Property/AbstractPropertyProcessor.php @@ -105,7 +105,7 @@ protected function addEnumValidator(PropertyInterface $property, array $allowedV // no type information provided - inherit the types from the enum values if (!$property->getType()) { $typesOfEnum = array_unique(array_map( - function ($value): string { + static function ($value): string { return TypeConverter::gettypeToInternal(gettype($value)); }, $allowedValues @@ -137,7 +137,7 @@ protected function addDependencyValidator(PropertyInterface $property, array $de array_walk( $dependencies, - function ($dependency, $index) use (&$propertyDependency): void { + static function ($dependency, $index) use (&$propertyDependency): void { $propertyDependency = $propertyDependency && is_int($index) && is_string($dependency); } ); @@ -179,7 +179,7 @@ private function transferDependentPropertiesToBaseSchema(Schema $dependencySchem (clone $property) ->setRequired(false) ->setType(null) - ->filterValidators(function (): bool { + ->filterValidators(static function (): bool { return false; }) ); diff --git a/src/PropertyProcessor/Property/BaseProcessor.php b/src/PropertyProcessor/Property/BaseProcessor.php index 9266860..8bf2ec9 100644 --- a/src/PropertyProcessor/Property/BaseProcessor.php +++ b/src/PropertyProcessor/Property/BaseProcessor.php @@ -316,7 +316,7 @@ protected function transferComposedPropertiesToSchema(PropertyInterface $propert } foreach ($validator->getComposedProperties() as $composedProperty) { - $composedProperty->onResolve(function () use ($composedProperty, $property, $validator) { + $composedProperty->onResolve(function () use ($composedProperty, $property, $validator): void { if (!$composedProperty->getNestedSchema()) { throw new SchemaException( sprintf( @@ -328,7 +328,7 @@ protected function transferComposedPropertiesToSchema(PropertyInterface $propert } $composedProperty->getNestedSchema()->onAllPropertiesResolved( - function () use ($composedProperty, $validator) { + function () use ($composedProperty, $validator): void { foreach ($composedProperty->getNestedSchema()->getProperties() as $property) { $this->schema->addProperty( $this->cloneTransferredProperty($property, $validator->getCompositionProcessor()) @@ -357,7 +357,7 @@ private function cloneTransferredProperty( string $compositionProcessor ): PropertyInterface { $transferredProperty = (clone $property) - ->filterValidators(function (Validator $validator): bool { + ->filterValidators(static function (Validator $validator): bool { return is_a($validator->getValidator(), PropertyTemplateValidator::class); }); diff --git a/src/PropertyProcessor/Property/MultiTypeProcessor.php b/src/PropertyProcessor/Property/MultiTypeProcessor.php index 386e872..1b1a3fd 100644 --- a/src/PropertyProcessor/Property/MultiTypeProcessor.php +++ b/src/PropertyProcessor/Property/MultiTypeProcessor.php @@ -12,6 +12,7 @@ use PHPModelGenerator\Model\Validator\TypeCheckInterface; use PHPModelGenerator\PropertyProcessor\Decorator\Property\PropertyTransferDecorator; use PHPModelGenerator\PropertyProcessor\Decorator\TypeHint\TypeHintDecorator; +use PHPModelGenerator\PropertyProcessor\Decorator\TypeHint\TypeHintTransferDecorator; use PHPModelGenerator\PropertyProcessor\PropertyMetaDataCollection; use PHPModelGenerator\PropertyProcessor\PropertyProcessorFactory; use PHPModelGenerator\PropertyProcessor\PropertyProcessorInterface; @@ -77,35 +78,39 @@ public function process(string $propertyName, JsonSchema $propertySchema): Prope { $property = parent::process($propertyName, $propertySchema); - foreach ($property->getValidators() as $validator) { - $this->checks[] = $validator->getValidator()->getCheck(); - } + $property->onResolve(function () use ($property, $propertyName, $propertySchema): void { + foreach ($property->getValidators() as $validator) { + $this->checks[] = $validator->getValidator()->getCheck(); + } - $subProperties = $this->processSubProperties($propertyName, $propertySchema, $property); + $subProperties = $this->processSubProperties($propertyName, $propertySchema, $property); - if (empty($this->allowedPropertyTypes)) { - return $property; - } + if (empty($this->allowedPropertyTypes)) { + return; + } - $property->addTypeHintDecorator( - new TypeHintDecorator( - array_map( - function (PropertyInterface $subProperty): string { - return $subProperty->getTypeHint(); - }, - $subProperties + $property->addTypeHintDecorator( + new TypeHintDecorator( + array_map( + static function (PropertyInterface $subProperty): string { + return $subProperty->getTypeHint(); + }, + $subProperties + ) ) - ) - ); - - return $property->addValidator( - new MultiTypeCheckValidator( - array_unique($this->allowedPropertyTypes), - $property, - $this->isImplicitNullAllowed($property) - ), - 2 - ); + ); + + $property->addValidator( + new MultiTypeCheckValidator( + array_unique($this->allowedPropertyTypes), + $property, + $this->isImplicitNullAllowed($property) + ), + 2 + ); + }); + + return $property; } /** @@ -165,11 +170,14 @@ protected function processSubProperties( $json['type'] = $type; $subProperty = $propertyProcessor->process($propertyName, $propertySchema->withJson($json)); - $this->transferValidators($subProperty, $property); - if ($subProperty->getDecorators()) { - $property->addDecorator(new PropertyTransferDecorator($subProperty)); - } + $subProperty->onResolve(function () use ($property, $subProperty): void { + $this->transferValidators($subProperty, $property); + + if ($subProperty->getDecorators()) { + $property->addDecorator(new PropertyTransferDecorator($subProperty)); + } + }); if ($defaultValue !== null && $propertyProcessor instanceof AbstractTypedValueProcessor) { try { diff --git a/src/SchemaProcessor/Hook/SchemaHookResolver.php b/src/SchemaProcessor/Hook/SchemaHookResolver.php index a1e997c..e2ad9e6 100644 --- a/src/SchemaProcessor/Hook/SchemaHookResolver.php +++ b/src/SchemaProcessor/Hook/SchemaHookResolver.php @@ -51,7 +51,7 @@ private function getHooks(string $filterHook): array { return array_filter( $this->schema->getSchemaHooks(), - function (SchemaHookInterface $hook) use ($filterHook): bool { + static function (SchemaHookInterface $hook) use ($filterHook): bool { return is_a($hook, $filterHook); } ); @@ -61,7 +61,7 @@ private function resolveHook(string $filterHook, ...$parameters): string { return join( "\n\n", - array_map(function ($hook) use ($parameters): string { + array_map(static function ($hook) use ($parameters): string { return $hook->getCode(...$parameters); }, $this->getHooks($filterHook)) ); diff --git a/src/SchemaProcessor/PostProcessor/AdditionalPropertiesAccessorPostProcessor.php b/src/SchemaProcessor/PostProcessor/AdditionalPropertiesAccessorPostProcessor.php index 05aecb9..4bb5394 100644 --- a/src/SchemaProcessor/PostProcessor/AdditionalPropertiesAccessorPostProcessor.php +++ b/src/SchemaProcessor/PostProcessor/AdditionalPropertiesAccessorPostProcessor.php @@ -104,12 +104,12 @@ private function addSetAdditionalPropertyMethod( ): void { $objectProperties = RenderHelper::varExportArray( array_map( - function (PropertyInterface $property): string { + static function (PropertyInterface $property): string { return $property->getName(); }, array_filter( $schema->getProperties(), - function (PropertyInterface $property): bool { + static function (PropertyInterface $property): bool { return !$property->isInternal(); } ) diff --git a/src/SchemaProcessor/PostProcessor/Internal/CompositionValidationPostProcessor.php b/src/SchemaProcessor/PostProcessor/Internal/CompositionValidationPostProcessor.php index bdc19e9..326b722 100644 --- a/src/SchemaProcessor/PostProcessor/Internal/CompositionValidationPostProcessor.php +++ b/src/SchemaProcessor/PostProcessor/Internal/CompositionValidationPostProcessor.php @@ -152,7 +152,7 @@ public function getCode(PropertyInterface $property, bool $batchUpdate = false): return join( "\n", array_map( - function ($validatorIndex) { + static function (int $validatorIndex): string { return sprintf('$this->validateComposition_%s($modelData);', $validatorIndex); }, array_unique($this->validatorPropertyMap[$property->getName()] ?? []) diff --git a/src/SchemaProcessor/PostProcessor/Internal/ExtendObjectPropertiesMatchingPatternPropertiesPostProcessor.php b/src/SchemaProcessor/PostProcessor/Internal/ExtendObjectPropertiesMatchingPatternPropertiesPostProcessor.php index 2ddc964..1c63d53 100644 --- a/src/SchemaProcessor/PostProcessor/Internal/ExtendObjectPropertiesMatchingPatternPropertiesPostProcessor.php +++ b/src/SchemaProcessor/PostProcessor/Internal/ExtendObjectPropertiesMatchingPatternPropertiesPostProcessor.php @@ -86,7 +86,7 @@ protected function transferPatternPropertiesFilterToProperty( ): void { $patternPropertiesValidators = array_filter( $schema->getBaseValidators(), - function (PropertyValidatorInterface $validator): bool { + static function (PropertyValidatorInterface $validator): bool { return $validator instanceof PatternPropertiesValidator; }); @@ -98,7 +98,7 @@ function (PropertyValidatorInterface $validator): bool { $propertyHasTransformingFilter = !empty( array_filter( $property->getValidators(), - function (Validator $validator): bool { + static function (Validator $validator): bool { return $validator->getValidator() instanceof FilterValidator && $validator->getValidator()->getFilter() instanceof TransformingFilterInterface; } diff --git a/src/SchemaProcessor/PostProcessor/Internal/PatternPropertiesPostProcessor.php b/src/SchemaProcessor/PostProcessor/Internal/PatternPropertiesPostProcessor.php index a2f631d..cd41fd3 100644 --- a/src/SchemaProcessor/PostProcessor/Internal/PatternPropertiesPostProcessor.php +++ b/src/SchemaProcessor/PostProcessor/Internal/PatternPropertiesPostProcessor.php @@ -51,7 +51,10 @@ public function process(Schema $schema, GeneratorConfiguration $generatorConfigu $patternHashes[$validator->getKey()] = array_reduce( $schema->getProperties(), - function (array $carry, PropertyInterface $property) use ($schemaProperties, $validator): array { + static function ( + array $carry, + PropertyInterface $property + ) use ($schemaProperties, $validator): array { if (in_array($property->getName(), $schemaProperties) && preg_match('/' . addcslashes($validator->getPattern(), '/') . '/', $property->getName()) ) { diff --git a/src/SchemaProcessor/PostProcessor/PatternPropertiesAccessorPostProcessor.php b/src/SchemaProcessor/PostProcessor/PatternPropertiesAccessorPostProcessor.php index 22e4fca..942331f 100644 --- a/src/SchemaProcessor/PostProcessor/PatternPropertiesAccessorPostProcessor.php +++ b/src/SchemaProcessor/PostProcessor/PatternPropertiesAccessorPostProcessor.php @@ -79,16 +79,18 @@ private function addGetPatternPropertiesMethod( */ private function getReturnTypeAnnotationForGetPatternProperties(array $patternTypes): string { - $baseTypes = array_unique(array_map( - function (PropertyType $type): string { + $baseTypes = array_unique( + array_map( + static function (PropertyType $type): string { return $type->getName(); }, - $patternTypes) + $patternTypes + ) ); $nullable = array_reduce( $patternTypes, - function (bool $carry, PropertyType $type): bool { + static function (bool $carry, PropertyType $type): bool { return $carry || $type->isNullable(); }, false diff --git a/src/SchemaProcessor/SchemaProcessor.php b/src/SchemaProcessor/SchemaProcessor.php index daa186c..1b08f40 100644 --- a/src/SchemaProcessor/SchemaProcessor.php +++ b/src/SchemaProcessor/SchemaProcessor.php @@ -234,7 +234,7 @@ protected function setCurrentClassPath(string $jsonSchemaFile): void { $path = str_replace($this->baseSource, '', dirname($jsonSchemaFile)); $pieces = array_map( - function ($directory) { + static function (string $directory): string { return ucfirst($directory); }, explode(DIRECTORY_SEPARATOR, $path) diff --git a/src/Templates/Model.phptpl b/src/Templates/Model.phptpl index c5d92a1..8b77391 100644 --- a/src/Templates/Model.phptpl +++ b/src/Templates/Model.phptpl @@ -200,9 +200,7 @@ class {{ class }} {% if schema.getInterfaces() %}implements {{ viewHelper.joinCl {% endif %} {% endforeach %} - {% foreach schema.getMethods() as method %} - {{ method.getCode(generatorConfiguration) }} - {% endforeach %} + {{ viewHelper.renderMethods(schema) }} } // @codeCoverageIgnoreEnd diff --git a/src/Templates/Validator/ArrayUnique.phptpl b/src/Templates/Validator/ArrayUnique.phptpl index d010620..23189f9 100644 --- a/src/Templates/Validator/ArrayUnique.phptpl +++ b/src/Templates/Validator/ArrayUnique.phptpl @@ -1,4 +1,4 @@ -is_array($value) && (function (array $items): bool { +is_array($value) && (static function (array $items): bool { if (empty($items)) { return false; } diff --git a/src/Templates/Validator/NoAdditionalProperties.phptpl b/src/Templates/Validator/NoAdditionalProperties.phptpl index 445d8aa..5c8138c 100644 --- a/src/Templates/Validator/NoAdditionalProperties.phptpl +++ b/src/Templates/Validator/NoAdditionalProperties.phptpl @@ -1,11 +1,11 @@ -$additionalProperties = (function () use ($modelData): array { +$additionalProperties = (static function () use ($modelData): array { $additionalProperties = array_diff(array_keys($modelData), {{ properties }}); {% if pattern %} // filter out all pattern properties $additionalProperties = array_filter( $additionalProperties, - function (string $property): bool { + static function (string $property): bool { return preg_match('/{{ pattern }}/', $property) !== 1; } ); diff --git a/src/Templates/Validator/PropertyDependency.phptpl b/src/Templates/Validator/PropertyDependency.phptpl index 03809fa..d147dff 100644 --- a/src/Templates/Validator/PropertyDependency.phptpl +++ b/src/Templates/Validator/PropertyDependency.phptpl @@ -1,4 +1,4 @@ -array_key_exists('{{ property.getName() }}', $modelData) && (function () use ($modelData, &$missingAttributes) { +array_key_exists('{{ property.getName() }}', $modelData) && (static function () use ($modelData, &$missingAttributes) { foreach ({{ dependencies }} as $dependency) { if (!array_key_exists($dependency, $modelData)) { $missingAttributes[] = $dependency; diff --git a/src/Utils/RenderHelper.php b/src/Utils/RenderHelper.php index 9d98549..e8ff411 100644 --- a/src/Utils/RenderHelper.php +++ b/src/Utils/RenderHelper.php @@ -178,6 +178,18 @@ public function renderValidator(PropertyValidatorInterface $validator, Schema $s return "\$this->{$validator->getExtractedMethodName()}(\$value);"; } + public function renderMethods(Schema $schema): string + { + $renderedMethods = ''; + + // don't change to a foreach loop as the render process of a method might add additional methods + for ($i = 0; $i < count($schema->getMethods()); $i++) { + $renderedMethods .= $schema->getMethods()[array_keys($schema->getMethods())[$i]]->getCode(); + } + + return $renderedMethods; + } + public static function varExportArray(array $values): string { return preg_replace('(\d+\s=>)', '', var_export($values, true)); diff --git a/src/Utils/ResolvableInterface.php b/src/Utils/ResolvableInterface.php new file mode 100644 index 0000000..6614b79 --- /dev/null +++ b/src/Utils/ResolvableInterface.php @@ -0,0 +1,18 @@ +isResolved + ? $callback() + : $this->onResolveCallbacks[] = $callback; + } + + public function isResolved(): bool + { + return $this->isResolved; + } + + public function resolve(): void + { + if ($this->isResolved) { + return; + } + + $this->isResolved = true; + + foreach ($this->onResolveCallbacks as $callback) { + $callback(); + } + + $this->onResolveCallbacks = []; + } +} diff --git a/tests/AbstractPHPModelGeneratorTest.php b/tests/AbstractPHPModelGeneratorTest.php index bbf2824..544c0f7 100644 --- a/tests/AbstractPHPModelGeneratorTest.php +++ b/tests/AbstractPHPModelGeneratorTest.php @@ -150,7 +150,7 @@ protected function generateClassFromFile( * Generate a class from a file template and apply all $values via sprintf to the template * * @param string $file - * @param array $values + * @param string[] $values * @param GeneratorConfiguration|null $generatorConfiguration * @param bool $escape * @param bool $implicitNull @@ -176,7 +176,7 @@ protected function generateClassFromFileTemplate( array_merge( [file_get_contents(__DIR__ . '/Schema/' . $this->getStaticClassName() . '/' . $file)], array_map( - function ($item) use ($escape) { + static function (string $item) use ($escape): string { return $escape ? str_replace("'", '"', addcslashes($item, '"\\')) : $item; }, $values diff --git a/tests/Basic/BasicSchemaGenerationTest.php b/tests/Basic/BasicSchemaGenerationTest.php index 2df67b7..91aab96 100644 --- a/tests/Basic/BasicSchemaGenerationTest.php +++ b/tests/Basic/BasicSchemaGenerationTest.php @@ -65,7 +65,7 @@ public function testGetterAndSetterAreGeneratedForMutableObjects(bool $implicitN public function testSetterLogicIsNotExecutedWhenValueIsIdentical(): void { - $this->modifyModelGenerator = function (ModelGenerator $modelGenerator): void { + $this->modifyModelGenerator = static function (ModelGenerator $modelGenerator): void { $modelGenerator->addPostProcessor(new class () extends PostProcessor { public function process(Schema $schema, GeneratorConfiguration $generatorConfiguration): void { diff --git a/tests/Basic/SchemaHookTest.php b/tests/Basic/SchemaHookTest.php index beb3014..00cf2c1 100644 --- a/tests/Basic/SchemaHookTest.php +++ b/tests/Basic/SchemaHookTest.php @@ -206,7 +206,7 @@ public function getCode(): string protected function addSchemaHook(SchemaHookInterface $schemaHook): void { - $this->modifyModelGenerator = function (ModelGenerator $modelGenerator) use ($schemaHook): void { + $this->modifyModelGenerator = static function (ModelGenerator $modelGenerator) use ($schemaHook): void { $modelGenerator->addPostProcessor(new class ($schemaHook) extends PostProcessor { private $schemaHook; diff --git a/tests/Objects/EnumPropertyTest.php b/tests/Objects/EnumPropertyTest.php index 7be832e..acc0173 100644 --- a/tests/Objects/EnumPropertyTest.php +++ b/tests/Objects/EnumPropertyTest.php @@ -384,7 +384,7 @@ public function testEmptyEnumThrowsSchemaException(): void protected function generateEnumClass(string $type, array $enumValues, $required = false): string { $enumValues = array_map( - function ($item) { + static function ($item): string { return var_export($item, true); }, $enumValues diff --git a/tests/Objects/MultiTypePropertyTest.php b/tests/Objects/MultiTypePropertyTest.php index 93dca82..6f78981 100644 --- a/tests/Objects/MultiTypePropertyTest.php +++ b/tests/Objects/MultiTypePropertyTest.php @@ -212,4 +212,25 @@ public function invalidNestedObjectDataProvider(): array ], ]; } + + /** + * @dataProvider validRecursiveMultiTypeDataProvider + */ + public function testValidRecursiveMultiType($input): void + { + + $className = $this->generateClassFromFile('RecursiveMultiTypeProperty.json'); + + $object = new $className(['property' => $input]); + $this->assertSame($input, $object->getProperty()); + } + + public function validRecursiveMultiTypeDataProvider(): array + { + return [ + 'string' => ['Test'], + # 'array' => [['Test1', 'Test2']], + # 'nested array' => [[['Test1', 'Test2'], 'Test3']], + ]; + } } diff --git a/tests/PostProcessor/AdditionalPropertiesAccessorPostProcessorTest.php b/tests/PostProcessor/AdditionalPropertiesAccessorPostProcessorTest.php index 76a942f..9351a82 100644 --- a/tests/PostProcessor/AdditionalPropertiesAccessorPostProcessorTest.php +++ b/tests/PostProcessor/AdditionalPropertiesAccessorPostProcessorTest.php @@ -29,9 +29,9 @@ class AdditionalPropertiesAccessorPostProcessorTest extends AbstractPHPModelGene { protected function addPostProcessor(bool $addForModelsWithoutAdditionalPropertiesDefinition) { - $this->modifyModelGenerator = function (ModelGenerator $generator) use ( + $this->modifyModelGenerator = static function (ModelGenerator $generator) use ( $addForModelsWithoutAdditionalPropertiesDefinition - ) { + ): void { $generator->addPostProcessor( new AdditionalPropertiesAccessorPostProcessor($addForModelsWithoutAdditionalPropertiesDefinition) ); @@ -324,7 +324,7 @@ public function invalidAdditionalPropertyDataProvider(): array public function testSetterSchemaHooksAreResolvedInSetAdditionalProperties(): void { - $this->modifyModelGenerator = function (ModelGenerator $modelGenerator): void { + $this->modifyModelGenerator = static function (ModelGenerator $modelGenerator): void { $modelGenerator ->addPostProcessor(new AdditionalPropertiesAccessorPostProcessor()) ->addPostProcessor(new class () extends PostProcessor { @@ -409,7 +409,7 @@ public function testAdditionalPropertiesAreSerialized(bool $implicitNull): void public function testAdditionalPropertiesAreSerializedWithoutAdditionalPropertiesAccessorPostProcessor(): void { - $this->modifyModelGenerator = function (ModelGenerator $generator): void { + $this->modifyModelGenerator = static function (ModelGenerator $generator): void { $generator->addPostProcessor(new PopulatePostProcessor()); }; @@ -430,7 +430,7 @@ public function testAdditionalPropertiesAreSerializedWithoutAdditionalProperties public function testAdditionalPropertiesAreNotSerializedWhenNotDefinedWithoutExplicitAccessorMethods(): void { - $this->modifyModelGenerator = function (ModelGenerator $generator): void { + $this->modifyModelGenerator = static function (ModelGenerator $generator): void { $generator->addPostProcessor(new PopulatePostProcessor()); }; @@ -448,7 +448,7 @@ public function testAdditionalPropertiesAreNotSerializedWhenNotDefinedWithoutExp public function testAdditionalPropertiesAreSerializedWhenNotDefinedWithExplicitAccessorMethods(): void { - $this->modifyModelGenerator = function (ModelGenerator $generator): void { + $this->modifyModelGenerator = static function (ModelGenerator $generator): void { $generator ->addPostProcessor(new PopulatePostProcessor()) ->addPostProcessor(new AdditionalPropertiesAccessorPostProcessor(true)); diff --git a/tests/PostProcessor/PatternPropertiesAccessorPostProcessorTest.php b/tests/PostProcessor/PatternPropertiesAccessorPostProcessorTest.php index a4f12dd..f0f148a 100644 --- a/tests/PostProcessor/PatternPropertiesAccessorPostProcessorTest.php +++ b/tests/PostProcessor/PatternPropertiesAccessorPostProcessorTest.php @@ -28,7 +28,7 @@ class PatternPropertiesAccessorPostProcessorTest extends AbstractPHPModelGenerat { protected function addPostProcessors(PostProcessor ...$postProcessors): void { - $this->modifyModelGenerator = function (ModelGenerator $generator) use ($postProcessors): void { + $this->modifyModelGenerator = static function (ModelGenerator $generator) use ($postProcessors): void { foreach ($postProcessors as $postProcessor) { $generator->addPostProcessor($postProcessor); } diff --git a/tests/PostProcessor/PopulatePostProcessorTest.php b/tests/PostProcessor/PopulatePostProcessorTest.php index caff403..2379d25 100644 --- a/tests/PostProcessor/PopulatePostProcessorTest.php +++ b/tests/PostProcessor/PopulatePostProcessorTest.php @@ -26,7 +26,7 @@ public function setUp(): void { parent::setUp(); - $this->modifyModelGenerator = function (ModelGenerator $generator): void { + $this->modifyModelGenerator = static function (ModelGenerator $generator): void { $generator->addPostProcessor(new PopulatePostProcessor()); }; } @@ -215,7 +215,7 @@ public function invalidPopulateDataProvider(): array public function testSetterBeforeValidationHookInsidePopulateIsResolved(): void { - $this->modifyModelGenerator = function (ModelGenerator $modelGenerator): void { + $this->modifyModelGenerator = static function (ModelGenerator $modelGenerator): void { $modelGenerator->addPostProcessor(new PopulatePostProcessor()); $modelGenerator->addPostProcessor(new class () extends PostProcessor { public function process(Schema $schema, GeneratorConfiguration $generatorConfiguration): void @@ -259,7 +259,7 @@ public function testSetterAfterValidationHookInsidePopulateIsResolved( array $populateValues ): void { - $this->modifyModelGenerator = function (ModelGenerator $modelGenerator): void { + $this->modifyModelGenerator = static function (ModelGenerator $modelGenerator): void { $modelGenerator->addPostProcessor(new PopulatePostProcessor()); $modelGenerator->addPostProcessor(new class () extends PostProcessor { public function process(Schema $schema, GeneratorConfiguration $generatorConfiguration): void diff --git a/tests/Schema/MultiTypePropertyTest/RecursiveMultiTypeProperty.json b/tests/Schema/MultiTypePropertyTest/RecursiveMultiTypeProperty.json new file mode 100644 index 0000000..1f2d952 --- /dev/null +++ b/tests/Schema/MultiTypePropertyTest/RecursiveMultiTypeProperty.json @@ -0,0 +1,18 @@ +{ + "type": "object", + "definitions": { + "property": { + "type": ["string", "array"], + "minLength": 4, + "items": { + "$ref": "#/definitions/property" + }, + "minItems": 2 + } + }, + "properties": { + "property": { + "$ref": "#/definitions/property" + } + } +} \ No newline at end of file diff --git a/tests/SchemaProvider/OpenAPIv3ProviderTest.php b/tests/SchemaProvider/OpenAPIv3ProviderTest.php index b2a3cd2..323c9e8 100644 --- a/tests/SchemaProvider/OpenAPIv3ProviderTest.php +++ b/tests/SchemaProvider/OpenAPIv3ProviderTest.php @@ -88,13 +88,13 @@ public function referencedSchemaDataProvider(): array 'Empty data path reference' => [ '#/components/modules/person', [], - function ($person) { + function ($person): void { $this->assertNull($person->getName()); $this->assertIsArray($person->getChildren()); $this->assertEmpty($person->getChildren()); }, [], - function ($car) { + function ($car): void { $this->assertNull($car->getPs()); $this->assertNull($car->getOwner()); }, @@ -109,7 +109,7 @@ function ($car) { ], ], ], - function ($person) { + function ($person): void { $this->assertSame('Hannes', $person->getName()); $this->assertCount(1, $person->getChildren()); $this->assertSame('Erwin', $person->getChildren()[0]->getName()); @@ -121,7 +121,7 @@ function ($person) { 'name' => 'Susi', ], ], - function ($car) { + function ($car): void { $this->assertSame(150, $car->getPs()); $this->assertSame('Susi', $car->getOwner()->getName()); $this->assertEmpty($car->getOwner()->getChildren()); @@ -142,7 +142,7 @@ function ($car) { ], ], ], - function ($person) { + function ($person): void { $this->assertSame('Hannes', $person->getName()); $this->assertCount(1, $person->getChildren()); $this->assertSame('Erwin', $person->getChildren()[0]->getName()); @@ -161,7 +161,7 @@ function ($person) { ], ], ], - function ($car) { + function ($car): void { $this->assertSame(150, $car->getPs()); $this->assertSame('Susi', $car->getOwner()->getName()); $this->assertCount(1, $car->getOwner()->getChildren()); From 470d2b77439d403dbb8d0309720bb5e6a1de65da Mon Sep 17 00:00:00 2001 From: Enno Woortmann Date: Thu, 30 Mar 2023 14:00:04 +0200 Subject: [PATCH 5/8] Remove unused code, add recursive tuple test --- src/Model/Property/Property.php | 8 ---- src/Model/Property/PropertyInterface.php | 1 - src/Model/Property/PropertyProxy.php | 14 ------ src/Model/Validator/ArrayTupleValidator.php | 14 +----- tests/AbstractPHPModelGeneratorTest.php | 3 +- tests/Basic/IdenticalNestedSchemaTest.php | 2 +- tests/Objects/MultiTypePropertyTest.php | 2 +- tests/Objects/TupleArrayPropertyTest.php | 45 +++++++++++++++++++ .../RecursiveTupleArray.json | 29 ++++++++++++ 9 files changed, 79 insertions(+), 39 deletions(-) create mode 100644 tests/Schema/TupleArrayPropertyTest/RecursiveTupleArray.json diff --git a/src/Model/Property/Property.php b/src/Model/Property/Property.php index d925158..ca8346a 100644 --- a/src/Model/Property/Property.php +++ b/src/Model/Property/Property.php @@ -153,14 +153,6 @@ public function addTypeHintDecorator(TypeHintDecoratorInterface $typeHintDecorat return $this; } - /** - * @inheritdoc - */ - public function getTypeHintDecorators(): array - { - return $this->typeHintDecorators; - } - /** * @inheritdoc */ diff --git a/src/Model/Property/PropertyInterface.php b/src/Model/Property/PropertyInterface.php index f039e40..cb13895 100644 --- a/src/Model/Property/PropertyInterface.php +++ b/src/Model/Property/PropertyInterface.php @@ -63,7 +63,6 @@ public function getTypeHint(bool $outputType = false, array $skipDecorators = [] */ public function addTypeHintDecorator(TypeHintDecoratorInterface $typeHintDecorator): PropertyInterface; - public function getTypeHintDecorators(): array; /** * Get a description for the property. If no description is available an empty string will be returned * diff --git a/src/Model/Property/PropertyProxy.php b/src/Model/Property/PropertyProxy.php index 619131e..e7d0541 100644 --- a/src/Model/Property/PropertyProxy.php +++ b/src/Model/Property/PropertyProxy.php @@ -89,13 +89,6 @@ public function addTypeHintDecorator(TypeHintDecoratorInterface $typeHintDecorat { return $this->getProperty()->addTypeHintDecorator($typeHintDecorator); } - /** - * @inheritdoc - */ - public function getTypeHintDecorators(): array - { - return $this->getProperty()->getTypeHintDecorators(); - } /** * @inheritdoc @@ -257,11 +250,4 @@ public function isInternal(): bool { return $this->getProperty()->isInternal(); } - - public function __clone() - { - $cloneKey = $this->key . uniqid(); - $this->definitionsCollection->offsetSet($cloneKey, clone $this->definitionsCollection->offsetGet($this->key)); - $this->key = $cloneKey; - } } diff --git a/src/Model/Validator/ArrayTupleValidator.php b/src/Model/Validator/ArrayTupleValidator.php index 84a6903..cabcb8b 100644 --- a/src/Model/Validator/ArrayTupleValidator.php +++ b/src/Model/Validator/ArrayTupleValidator.php @@ -46,7 +46,6 @@ public function __construct( $this->tupleProperties = []; - $pendingTuples = 0; foreach ($propertiesStructure->getJson() as $tupleIndex => $tupleItem) { $tupleItemName = "tuple item #$tupleIndex of array $propertyName"; @@ -59,21 +58,10 @@ public function __construct( $propertiesStructure->withJson($tupleItem) ); - if (!$tupleProperty->isResolved()) { - $pendingTuples++; - $tupleProperty->onResolve(function () use (&$pendingTuples): void { - if (--$pendingTuples === 0) { - $this->resolve(); - } - }); - } - $this->tupleProperties[] = $tupleProperty; } - if ($pendingTuples === 0) { - $this->resolve(); - } + $this->resolve(); parent::__construct( new Property($propertyName, null, $propertiesStructure), diff --git a/tests/AbstractPHPModelGeneratorTest.php b/tests/AbstractPHPModelGeneratorTest.php index 544c0f7..be0e670 100644 --- a/tests/AbstractPHPModelGeneratorTest.php +++ b/tests/AbstractPHPModelGeneratorTest.php @@ -417,7 +417,8 @@ protected function assertErrorRegistryContainsException( $this->fail("Error exception $expectedException not found in error registry exception"); } - public function validationMethodDataProvider(): array { + public function validationMethodDataProvider(): array + { return [ 'Error Collection' => [new GeneratorConfiguration()], 'Direct Exception' => [(new GeneratorConfiguration())->setCollectErrors(false)], diff --git a/tests/Basic/IdenticalNestedSchemaTest.php b/tests/Basic/IdenticalNestedSchemaTest.php index b72b2a6..3898b17 100644 --- a/tests/Basic/IdenticalNestedSchemaTest.php +++ b/tests/Basic/IdenticalNestedSchemaTest.php @@ -97,7 +97,7 @@ public function testIdenticalReferencedSchemaInMultipleFilesAreMappedToOneClass( $this->assertSame(get_class($object1->getMember()), get_class($object2->getMember())); } - public function identicalReferencedSchemaDataProvider() + public function identicalReferencedSchemaDataProvider(): array { return [ 'In same namespace' => [ diff --git a/tests/Objects/MultiTypePropertyTest.php b/tests/Objects/MultiTypePropertyTest.php index 6f78981..bfdd30d 100644 --- a/tests/Objects/MultiTypePropertyTest.php +++ b/tests/Objects/MultiTypePropertyTest.php @@ -120,7 +120,7 @@ public function testInvalidProvidedValueThrowsAnException($propertyValue, string new $className(['property' => $propertyValue]); } - public function invalidValueDataProvider() + public function invalidValueDataProvider(): array { return [ 'Bool' => [true, 'Invalid type for property. Requires [float, string, array], got boolean'], diff --git a/tests/Objects/TupleArrayPropertyTest.php b/tests/Objects/TupleArrayPropertyTest.php index 1d3be7e..6f3e932 100644 --- a/tests/Objects/TupleArrayPropertyTest.php +++ b/tests/Objects/TupleArrayPropertyTest.php @@ -2,6 +2,7 @@ namespace PHPModelGenerator\Tests\Objects; +use PHPModelGenerator\Exception\Arrays\InvalidTupleException; use PHPModelGenerator\Exception\FileSystemException; use PHPModelGenerator\Exception\RenderException; use PHPModelGenerator\Exception\SchemaException; @@ -418,4 +419,48 @@ public function invalidObjectAdditionalItemsDataProvider(): array ] ); } + + /** + * @dataProvider validRecursiveTupleDataProvider + */ + public function testValidRecursiveTuple(array $input): void + { + $className = $this->generateClassFromFile('RecursiveTupleArray.json'); + + $object = new $className(['property' => $input]); + + $this->assertSame($input, $object->getProperty()); + } + + public function validRecursiveTupleDataProvider(): array + { + return [ + 'string' => [['abc', 'def']], + 'one level nested' => [['abc', ['abc', 'def']]], + 'two level nested' => [['abc', ['abc', ['abc', 'def']]]], + ]; + } + + /** + * @dataProvider invalidRecursiveTupleDataProvider + */ + public function testInvalidRecursiveTuple(array $input): void + { + $this->expectException(InvalidTupleException::class); + + $className = $this->generateClassFromFile('RecursiveTupleArray.json'); + + new $className(['property' => $input]); + } + + public function invalidRecursiveTupleDataProvider(): array + { + return [ + 'invalid first tuple' => [[1, 'def']], + 'invalid second tuple' => [['abc', 1]], + 'one level nested - invalid first tuple' => [[1, ['abc', 'def']]], + 'one level nested - invalid nested first tuple' => [['abc', [1, 'def']]], + 'one level nested - invalid nested second tuple' => [['abc', ['abc', 1]]], + ]; + } } diff --git a/tests/Schema/TupleArrayPropertyTest/RecursiveTupleArray.json b/tests/Schema/TupleArrayPropertyTest/RecursiveTupleArray.json new file mode 100644 index 0000000..f59aef6 --- /dev/null +++ b/tests/Schema/TupleArrayPropertyTest/RecursiveTupleArray.json @@ -0,0 +1,29 @@ +{ + "definitions": { + "list": { + "type": "array", + "items": [ + { + "type": "string" + }, + { + "oneOf": [ + { + "$ref": "#/definitions/list" + }, + { + "type": "string" + } + ] + } + ], + "minItems": 1 + } + }, + "type": "object", + "properties": { + "property": { + "$ref": "#/definitions/list" + } + } +} \ No newline at end of file From 4c9986fcbe209611e451b39ca83a36baea9fcde3 Mon Sep 17 00:00:00 2001 From: Enno Woortmann Date: Thu, 30 Mar 2023 14:04:40 +0200 Subject: [PATCH 6/8] remove unused code --- src/Utils/ResolvableTrait.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Utils/ResolvableTrait.php b/src/Utils/ResolvableTrait.php index 4a72580..e191646 100644 --- a/src/Utils/ResolvableTrait.php +++ b/src/Utils/ResolvableTrait.php @@ -25,10 +25,6 @@ public function isResolved(): bool public function resolve(): void { - if ($this->isResolved) { - return; - } - $this->isResolved = true; foreach ($this->onResolveCallbacks as $callback) { From 1ae4cf138d816369f731bd56a45227d496486924 Mon Sep 17 00:00:00 2001 From: Enno Woortmann Date: Thu, 30 Mar 2023 14:34:17 +0200 Subject: [PATCH 7/8] Add recursive multi type tests --- .../Property/MultiTypeProcessor.php | 51 ++++++++------- tests/Objects/MultiTypePropertyTest.php | 65 ++++++++++++++++++- 2 files changed, 92 insertions(+), 24 deletions(-) diff --git a/src/PropertyProcessor/Property/MultiTypeProcessor.php b/src/PropertyProcessor/Property/MultiTypeProcessor.php index 1b1a3fd..0275303 100644 --- a/src/PropertyProcessor/Property/MultiTypeProcessor.php +++ b/src/PropertyProcessor/Property/MultiTypeProcessor.php @@ -85,29 +85,36 @@ public function process(string $propertyName, JsonSchema $propertySchema): Prope $subProperties = $this->processSubProperties($propertyName, $propertySchema, $property); - if (empty($this->allowedPropertyTypes)) { - return; + $processedSubProperties = 0; + foreach ($subProperties as $subProperty) { + $subProperty->onResolve(function () use ($property, $subProperties, &$processedSubProperties) { + if (++$processedSubProperties === count($subProperties)) { + if (empty($this->allowedPropertyTypes)) { + return; + } + + $property->addTypeHintDecorator( + new TypeHintDecorator( + array_map( + static function (PropertyInterface $subProperty): string { + return $subProperty->getTypeHint(); + }, + $subProperties + ) + ) + ); + + $property->addValidator( + new MultiTypeCheckValidator( + array_unique($this->allowedPropertyTypes), + $property, + $this->isImplicitNullAllowed($property) + ), + 2 + ); + } + }); } - - $property->addTypeHintDecorator( - new TypeHintDecorator( - array_map( - static function (PropertyInterface $subProperty): string { - return $subProperty->getTypeHint(); - }, - $subProperties - ) - ) - ); - - $property->addValidator( - new MultiTypeCheckValidator( - array_unique($this->allowedPropertyTypes), - $property, - $this->isImplicitNullAllowed($property) - ), - 2 - ); }); return $property; diff --git a/tests/Objects/MultiTypePropertyTest.php b/tests/Objects/MultiTypePropertyTest.php index bfdd30d..73af16d 100644 --- a/tests/Objects/MultiTypePropertyTest.php +++ b/tests/Objects/MultiTypePropertyTest.php @@ -2,7 +2,10 @@ namespace PHPModelGenerator\Tests\Objects; +use PHPModelGenerator\Exception\Arrays\InvalidItemException; +use PHPModelGenerator\Exception\Arrays\MinItemsException; use PHPModelGenerator\Exception\FileSystemException; +use PHPModelGenerator\Exception\Generic\InvalidTypeException; use PHPModelGenerator\Exception\RenderException; use PHPModelGenerator\Exception\SchemaException; use PHPModelGenerator\Model\GeneratorConfiguration; @@ -229,8 +232,66 @@ public function validRecursiveMultiTypeDataProvider(): array { return [ 'string' => ['Test'], - # 'array' => [['Test1', 'Test2']], - # 'nested array' => [[['Test1', 'Test2'], 'Test3']], + 'array' => [['Test1', 'Test2']], + 'nested array' => [[['Test1', 'Test2'], 'Test3']], + ]; + } + + /** + * @dataProvider invalidRecursiveMultiTypeDataProvider + */ + public function testInvalidRecursiveMultiType($input, string $expectedException, string $exceptionMessage): void + { + $this->expectException($expectedException); + $this->expectExceptionMessage($exceptionMessage); + + $className = $this->generateClassFromFile('RecursiveMultiTypeProperty.json'); + + new $className(['property' => $input]); + } + + public function invalidRecursiveMultiTypeDataProvider(): array + { + return [ + 'int' => [ + 1, + InvalidTypeException::class, + 'Invalid type for property. Requires [string, array], got integer', + ], + 'invalid item in array' => [ + ['Test1', 1], + InvalidItemException::class, + << [ + [], + MinItemsException::class, + 'Array property must not contain less than 2 items', + ], + 'invalid item in nested array' => [ + ['Test1', [3, 'Test3']], + InvalidItemException::class, + << [ + ['Test1', []], + InvalidItemException::class, + << Date: Thu, 30 Mar 2023 14:34:57 +0200 Subject: [PATCH 8/8] remove unused import --- src/PropertyProcessor/Property/MultiTypeProcessor.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PropertyProcessor/Property/MultiTypeProcessor.php b/src/PropertyProcessor/Property/MultiTypeProcessor.php index 0275303..d4f9c70 100644 --- a/src/PropertyProcessor/Property/MultiTypeProcessor.php +++ b/src/PropertyProcessor/Property/MultiTypeProcessor.php @@ -12,7 +12,6 @@ use PHPModelGenerator\Model\Validator\TypeCheckInterface; use PHPModelGenerator\PropertyProcessor\Decorator\Property\PropertyTransferDecorator; use PHPModelGenerator\PropertyProcessor\Decorator\TypeHint\TypeHintDecorator; -use PHPModelGenerator\PropertyProcessor\Decorator\TypeHint\TypeHintTransferDecorator; use PHPModelGenerator\PropertyProcessor\PropertyMetaDataCollection; use PHPModelGenerator\PropertyProcessor\PropertyProcessorFactory; use PHPModelGenerator\PropertyProcessor\PropertyProcessorInterface;