diff --git a/docs/source/generator/postProcessor.rst b/docs/source/generator/postProcessor.rst new file mode 100644 index 0000000..58b7b03 --- /dev/null +++ b/docs/source/generator/postProcessor.rst @@ -0,0 +1,101 @@ +Post Processor +============== + +Post processors provide an easy way to extend your generated code. A post processor can be added to your `ModelGenerator` object: + +.. code-block:: php + + $generator = new ModelGenerator(); + $generator->addPostProcessor(new PopulatePostProcessor()); + + $files = $generator->generateModelDirectory(__DIR__ . '/result') + ->generateModels(new RecursiveDirectoryProvider(__DIR__ . '/schema'), __DIR__ . '/result'); + +All added post processors will be executed after a schema was processed and before a model is rendered. Consequently a post processor can be used to change the generated class or to extend the class. Also additional tasks which don't change the rendered code may be executed (eg. create a documentation file for the class, create SQL create statements for tables representing the class, ...). + +Builtin Post Processors +----------------------- + +PopulatePostProcessor +^^^^^^^^^^^^^^^^^^^^^ + +The **PopulatePostProcessor** adds a populate method to your generated model. The populate method accepts an array which might contain any subset of the model's properties. All properties present in the provided array will be validated according to the validation rules from the JSON-Schema. If all values are valid the properties will be updated otherwise an exception will be thrown (if error collection is enabled an exception containing all violations, otherwise on the first occurring error, compare `collecting errors <../gettingStarted.html#collect-errors-vs-early-return>`__). Also basic model constraints like `minProperties`, `maxProperties` or `propertyNames` will be validated as the provided array may add additional properties to the model. If the model is updated also the values which can be fetched via `getRawModelDataInput` will be updated. + +.. code-block:: json + + { + "$id": "example", + "type": "object", + "properties": { + "value": { + "type": "string" + } + } + } + +Generated interface with the **PopulatePostProcessor**: + +.. code-block:: php + + public function getRawModelDataInput(): array; + public function populate(array $modelData): self; + + public function setExample(float $example): self; + public function getExample(): float; + +Now let's have a look at the behaviour of the generated model: + +.. code-block:: php + + // initialize the model with a valid value + $example = new Example(['value' => 'Hello World']); + $example->getRawModelDataInput(); // returns ['value' => 'Hello World'] + + // add an additional property to the model. + // if additional property constraints are defined in your JSON-Schema + // each additional property will be validated against the defined constraints. + $example->populate(['additionalValue' => 12]); + $example->getRawModelDataInput(); // returns ['value' => 'Hello World', 'additionalValue' => 12] + + // update an existing property with a valid value + $example->populate(['value' => 'Good night!']); + $example->getRawModelDataInput(); // returns ['value' => 'Good night!', 'additionalValue' => 12] + + // update an existing property with an invalid value which will throw an exception + try { + $example->populate(['value' => false]); + } catch (Exception $e) { + // perform error handling + } + // if the update of the model fails no values will be updated + $example->getRawModelDataInput(); // returns ['value' => 'Good night!', 'additionalValue' => 12] + +.. warning:: + + If the **PopulatePostProcessor** is added to your model generator the populate method will be added to the model independently of the `immutable setting <../gettingStarted.html#immutable-classes>`__. + +Custom Post Processors +---------------------- + +You can implement custom post processors to accomplish your tasks. Each post processor must implement the **PHPModelGenerator\\SchemaProcessor\\PostProcessor\\PostProcessorInterface**. If you have implemented a post processor add the post processor to your `ModelGenerator` and the post processor will be executed for each class. + +A custom post processor which adds a custom trait to the generated model (eg. a trait adding methods for an active record pattern implementation) may look like: + +.. code-block:: php + + namespace MyApp\Model\Generator\PostProcessor; + + use MyApp\Model\ActiveRecordTrait; + use PHPModelGenerator\SchemaProcessor\PostProcessor\PostProcessorInterface; + + class ActiveRecordPostProcessor implements PostProcessorInterface + { + public function process(Schema $schema, GeneratorConfiguration $generatorConfiguration): void + { + $schema->addTrait(ActiveRecordTrait::class); + } + } + +.. hint:: + + For examples how to implement a custom post processor have a look at the built in post processors located at **src/SchemaProcessor/PostProcessor/** diff --git a/docs/source/gettingStarted.rst b/docs/source/gettingStarted.rst index 5b08901..c25c25e 100644 --- a/docs/source/gettingStarted.rst +++ b/docs/source/gettingStarted.rst @@ -280,3 +280,8 @@ Custom filter addFilter(FilterInterface $customFilter); Add a custom filter to the generator. For more details see `Filter `__. + +Post Processors +--------------- + +Additionally to the described generator configuration options you can add post processors to your model generator object to change or extend the generated code. For more details see `post processors `__. diff --git a/docs/source/index.rst b/docs/source/index.rst index b1b28ef..e43db62 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -12,4 +12,5 @@ Generates PHP model classes from JSON-Schema files including validation and prov .. include:: toc-types.rst .. include:: toc-complexTypes.rst .. include:: toc-combinedSchemas.rst +.. include:: toc-generator.rst .. include:: toc-nonStandardExtensions.rst diff --git a/docs/source/toc-generator.rst b/docs/source/toc-generator.rst new file mode 100644 index 0000000..beb5f7b --- /dev/null +++ b/docs/source/toc-generator.rst @@ -0,0 +1,5 @@ +.. toctree:: + :caption: Extending the generator + :maxdepth: 1 + + generator/postProcessor diff --git a/src/Model/GeneratorConfiguration.php b/src/Model/GeneratorConfiguration.php index 56193a9..3e45360 100644 --- a/src/Model/GeneratorConfiguration.php +++ b/src/Model/GeneratorConfiguration.php @@ -13,7 +13,6 @@ use PHPModelGenerator\Utils\ClassNameGenerator; use PHPModelGenerator\Utils\ClassNameGeneratorInterface; use PHPModelGenerator\Exception\ErrorRegistryException; -use PHPModelGenerator\Exception\ValidationException; /** * Class GeneratorConfiguration @@ -67,22 +66,13 @@ public function __construct() */ public function addFilter(FilterInterface $filter): self { - if (!(count($filter->getFilter()) === 2) || - !is_string($filter->getFilter()[0]) || - !is_string($filter->getFilter()[1]) || - !is_callable($filter->getFilter()) - ) { - throw new InvalidFilterException("Invalid filter callback for filter {$filter->getToken()}"); - } + $this->validateFilterCallback($filter->getFilter(), "Invalid filter callback for filter {$filter->getToken()}"); if ($filter instanceof TransformingFilterInterface) { - if (!(count($filter->getSerializer()) === 2) || - !is_string($filter->getSerializer()[0]) || - !is_string($filter->getSerializer()[1]) || - !is_callable($filter->getSerializer()) - ) { - throw new InvalidFilterException("Invalid serializer callback for filter {$filter->getToken()}"); - } + $this->validateFilterCallback( + $filter->getSerializer(), + "Invalid serializer callback for filter {$filter->getToken()}" + ); } foreach ($filter->getAcceptedTypes() as $acceptedType) { @@ -98,6 +88,23 @@ public function addFilter(FilterInterface $filter): self return $this; } + /** + * @param array $callback + * @param string $message + * + * @throws InvalidFilterException + */ + private function validateFilterCallback(array $callback, string $message): void + { + if (!(count($callback) === 2) || + !is_string($callback[0]) || + !is_string($callback[1]) || + !is_callable($callback) + ) { + throw new InvalidFilterException($message); + } + } + /** * Get a filter by the given token * diff --git a/src/Model/MethodInterface.php b/src/Model/MethodInterface.php new file mode 100644 index 0000000..bcf16a6 --- /dev/null +++ b/src/Model/MethodInterface.php @@ -0,0 +1,15 @@ +attribute = $this->processAttributeName($name); $this->name = $name; $this->type = $type; + $this->jsonSchema = $jsonSchema; $this->description = $description; + + $this->attribute = $this->processAttributeName($name); } /** @@ -254,7 +261,13 @@ function ($element) { $attributeName = lcfirst(join('', $elements)); if (empty($attributeName)) { - throw new SchemaException("Property name '$name' results in an empty attribute name"); + throw new SchemaException( + sprintf( + "Property name '%s' results in an empty attribute name in file %s", + $name, + $this->jsonSchema->getFile() + ) + ); } return $attributeName; diff --git a/src/Model/Property/PropertyInterface.php b/src/Model/Property/PropertyInterface.php index c1bf433..3f0351b 100644 --- a/src/Model/Property/PropertyInterface.php +++ b/src/Model/Property/PropertyInterface.php @@ -5,6 +5,7 @@ namespace PHPModelGenerator\Model\Property; use PHPModelGenerator\Model\Schema; +use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; use PHPModelGenerator\Model\Validator; use PHPModelGenerator\Model\Validator\PropertyValidatorInterface; use PHPModelGenerator\PropertyProcessor\Decorator\Property\PropertyDecoratorInterface; @@ -179,4 +180,11 @@ public function setNestedSchema(Schema $schema); * @return null|Schema */ public function getNestedSchema(): ?Schema; + + /** + * Get the JSON schema used to set up the property + * + * @return JsonSchema + */ + public function getJsonSchema(): JsonSchema; } diff --git a/src/Model/Property/PropertyProxy.php b/src/Model/Property/PropertyProxy.php index 57552eb..1fd405c 100644 --- a/src/Model/Property/PropertyProxy.php +++ b/src/Model/Property/PropertyProxy.php @@ -4,6 +4,7 @@ namespace PHPModelGenerator\Model\Property; +use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; use PHPModelGenerator\Model\SchemaDefinition\ResolvedDefinitionsCollection; use PHPModelGenerator\Model\Schema; use PHPModelGenerator\Model\Validator\PropertyValidatorInterface; @@ -219,4 +220,12 @@ public function getNestedSchema(): ?Schema { return $this->getProperty()->getNestedSchema(); } + + /** + * @inheritdoc + */ + public function getJsonSchema(): JsonSchema + { + return $this->getProperty()->getJsonSchema(); + } } diff --git a/src/Model/Property/Serializer/TransformingFilterSerializer.php b/src/Model/Property/Serializer/TransformingFilterSerializer.php deleted file mode 100644 index 1e28a2e..0000000 --- a/src/Model/Property/Serializer/TransformingFilterSerializer.php +++ /dev/null @@ -1,69 +0,0 @@ -propertyName = $propertyName; - $this->filter = $filter; - $this->filterOptions = $filterOptions; - } - - /** - * @param GeneratorConfiguration $generatorConfiguration - * - * @return string - * - * @throws FileSystemException - * @throws SyntaxErrorException - * @throws UndefinedSymbolException - */ - public function getSerializer(GeneratorConfiguration $generatorConfiguration): string - { - return (new Render(join(DIRECTORY_SEPARATOR, [__DIR__, '..', '..', '..', 'Templates']) . DIRECTORY_SEPARATOR)) - ->renderTemplate( - DIRECTORY_SEPARATOR . 'Serializer' . DIRECTORY_SEPARATOR . 'TransformingFilterSerializer.phptpl', - [ - 'viewHelper' => new RenderHelper($generatorConfiguration), - 'property' => $this->propertyName, - 'serializerClass' => $this->filter->getSerializer()[0], - 'serializerMethod' => $this->filter->getSerializer()[1], - 'serializerOptions' => var_export($this->filterOptions, true), - ] - ); - } -} diff --git a/src/Model/RenderJob.php b/src/Model/RenderJob.php index 13c6a65..864cd79 100644 --- a/src/Model/RenderJob.php +++ b/src/Model/RenderJob.php @@ -9,6 +9,7 @@ use PHPModelGenerator\Exception\FileSystemException; use PHPModelGenerator\Exception\RenderException; use PHPModelGenerator\Exception\ValidationException; +use PHPModelGenerator\SchemaProcessor\PostProcessor\PostProcessorInterface; use PHPModelGenerator\Utils\RenderHelper; /** @@ -52,6 +53,17 @@ public function __construct( $this->initialClass = $initialClass; } + /** + * @param PostProcessorInterface[] $postProcessors + * @param GeneratorConfiguration $generatorConfiguration + */ + public function postProcess(array $postProcessors, GeneratorConfiguration $generatorConfiguration) + { + foreach ($postProcessors as $postProcessor) { + $postProcessor->process($this->schema, $generatorConfiguration); + } + } + /** * Execute the render job and render the class * @@ -136,9 +148,7 @@ protected function renderClass(GeneratorConfiguration $generatorConfiguration): 'namespace' => $namespace, 'use' => 'use ' . join(";\nuse ", array_unique($use)) . ';', 'class' => $this->className, - 'baseValidators' => $this->schema->getBaseValidators(), - 'properties' => $this->schema->getProperties(), - 'customSerializer' => $this->schema->getCustomSerializer(), + 'schema' => $this->schema, 'generatorConfiguration' => $generatorConfiguration, 'viewHelper' => new RenderHelper($generatorConfiguration), 'initialClass' => $this->initialClass, diff --git a/src/Model/Schema.php b/src/Model/Schema.php index 1ab119b..a9a10ee 100644 --- a/src/Model/Schema.php +++ b/src/Model/Schema.php @@ -4,8 +4,10 @@ namespace PHPModelGenerator\Model; +use PHPModelGenerator\Interfaces\JSONModelInterface; use PHPModelGenerator\Model\Property\PropertyInterface; -use PHPModelGenerator\Model\Property\Serializer\TransformingFilterSerializer; +use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; +use PHPModelGenerator\Model\SchemaDefinition\JsonSchemaTrait; use PHPModelGenerator\Model\SchemaDefinition\SchemaDefinitionDictionary; use PHPModelGenerator\Model\Validator\PropertyValidatorInterface; use PHPModelGenerator\Model\Validator\SchemaDependencyValidator; @@ -18,12 +20,22 @@ */ class Schema { + use JsonSchemaTrait; + /** @var string */ protected $className; /** @var string */ protected $classPath; + + /** @var string[] */ + protected $traits = []; + /** @var string[] */ + protected $interfaces = []; /** @var PropertyInterface[] The properties which are part of the class */ protected $properties = []; + /** @var MethodInterface[] */ + protected $methods = []; + /** @var PropertyValidatorInterface[] A Collection of validators which must be applied * before adding properties to the model */ @@ -32,8 +44,6 @@ class Schema protected $usedClasses = []; /** @var SchemaNamespaceTransferDecorator[] */ protected $namespaceTransferDecorators = []; - /** @var TransformingFilterSerializer[] */ - protected $customSerializer = []; /** @var SchemaDefinitionDictionary */ protected $schemaDefinitionDictionary; @@ -41,15 +51,23 @@ class Schema /** * Schema constructor. * - * @param string $classPath - * @param string $className + * @param string $classPath + * @param string $className + * @param JsonSchema $schema * @param SchemaDefinitionDictionary|null $dictionary */ - public function __construct(string $classPath, string $className, SchemaDefinitionDictionary $dictionary = null) - { + public function __construct( + string $classPath, + string $className, + JsonSchema $schema, + SchemaDefinitionDictionary $dictionary = null + ) { $this->className = $className; $this->classPath = $classPath; + $this->jsonSchema = $schema; $this->schemaDefinitionDictionary = $dictionary ?? new SchemaDefinitionDictionary(''); + + $this->addInterface(JSONModelInterface::class); } /** @@ -196,23 +214,63 @@ public function getUsedClasses(array $visitedSchema = []): array } /** - * @param string $property - * @param TransformingFilterSerializer $serializer + * @param string $methodKey An unique key in the scope of the schema to identify the method + * @param MethodInterface $method * * @return $this */ - public function addCustomSerializer(string $property, TransformingFilterSerializer $serializer): self + public function addMethod(string $methodKey, MethodInterface $method): self { - $this->customSerializer[$property] = $serializer; + $this->methods[$methodKey] = $method; return $this; } /** - * @return TransformingFilterSerializer[] + * @return MethodInterface[] + */ + public function getMethods(): array + { + return $this->methods; + } + + /** + * @return string[] + */ + public function getTraits(): array + { + return $this->traits; + } + + /** + * @param string $trait + * @return Schema */ - public function getCustomSerializer(): array + public function addTrait(string $trait): self { - return $this->customSerializer; + $this->traits[] = $trait; + $this->addUsedClass($trait); + + return $this; + } + + /** + * @return string[] + */ + public function getInterfaces(): array + { + return $this->interfaces; + } + + /** + * @param string $interface + * @return Schema + */ + public function addInterface(string $interface): self + { + $this->interfaces[] = $interface; + $this->addUsedClass($interface); + + return $this; } } diff --git a/src/Model/SchemaDefinition/JsonSchema.php b/src/Model/SchemaDefinition/JsonSchema.php new file mode 100644 index 0000000..e57995d --- /dev/null +++ b/src/Model/SchemaDefinition/JsonSchema.php @@ -0,0 +1,59 @@ +json = $json; + $this->file = $file; + } + + /** + * @return array + */ + public function getJson(): array + { + return $this->json; + } + + /** + * @param array $json + * + * @return JsonSchema + */ + public function withJson(array $json): JsonSchema + { + $jsonSchema = clone $this; + $jsonSchema->json = $json; + + return $jsonSchema; + } + + /** + * @return string + */ + public function getFile(): string + { + return $this->file; + } +} diff --git a/src/Model/SchemaDefinition/JsonSchemaTrait.php b/src/Model/SchemaDefinition/JsonSchemaTrait.php new file mode 100644 index 0000000..822913f --- /dev/null +++ b/src/Model/SchemaDefinition/JsonSchemaTrait.php @@ -0,0 +1,26 @@ +jsonSchema; + } +} diff --git a/src/Model/SchemaDefinition/SchemaDefinition.php b/src/Model/SchemaDefinition/SchemaDefinition.php index ece4d2c..a300e8e 100644 --- a/src/Model/SchemaDefinition/SchemaDefinition.php +++ b/src/Model/SchemaDefinition/SchemaDefinition.php @@ -23,8 +23,8 @@ */ class SchemaDefinition { - /** @var array */ - protected $structure; + /** @var JsonSchema */ + protected $source; /** @var SchemaProcessor */ protected $schemaProcessor; /** @var Schema */ @@ -35,13 +35,13 @@ class SchemaDefinition /** * SchemaDefinition constructor. * - * @param array $structure + * @param JsonSchema $jsonSchema * @param SchemaProcessor $schemaProcessor - * @param Schema $schema + * @param Schema $schema */ - public function __construct(array $structure, SchemaProcessor $schemaProcessor, Schema $schema) + public function __construct(JsonSchema $jsonSchema, SchemaProcessor $schemaProcessor, Schema $schema) { - $this->structure = $structure; + $this->source = $jsonSchema; $this->schemaProcessor = $schemaProcessor; $this->schema = $schema; @@ -73,15 +73,15 @@ public function resolveReference( array $path, PropertyMetaDataCollection $propertyMetaDataCollection ): PropertyInterface { - $structure = $this->structure; + $jsonSchema = $this->source->getJson(); $originalPath = $path; while ($segment = array_shift($path)) { - if (!isset($structure[$segment])) { - throw new SchemaException("Unresolved path segment: $segment"); + if (!isset($jsonSchema[$segment])) { + throw new SchemaException("Unresolved path segment $segment in file {$this->source->getFile()}"); } - $structure = $structure[$segment]; + $jsonSchema = $jsonSchema[$segment]; } $key = implode('-', $originalPath); @@ -98,7 +98,7 @@ public function resolveReference( $this->schemaProcessor, $this->schema, $propertyName, - $structure + $this->source->withJson($jsonSchema) ) ); } catch (PHPModelGeneratorException $exception) { diff --git a/src/Model/SchemaDefinition/SchemaDefinitionDictionary.php b/src/Model/SchemaDefinition/SchemaDefinitionDictionary.php index 3b75648..1249566 100644 --- a/src/Model/SchemaDefinition/SchemaDefinitionDictionary.php +++ b/src/Model/SchemaDefinition/SchemaDefinitionDictionary.php @@ -36,52 +36,55 @@ public function __construct(string $sourceDirectory) /** * Set up the definition directory for the schema * - * @param array $propertyData * @param SchemaProcessor $schemaProcessor - * @param Schema $schema + * @param Schema $schema */ - public function setUpDefinitionDictionary( - array $propertyData, - SchemaProcessor $schemaProcessor, - Schema $schema - ): void { + public function setUpDefinitionDictionary(SchemaProcessor $schemaProcessor, Schema $schema): void { // attach the root node to the definition dictionary - $this->addDefinition('#', new SchemaDefinition($propertyData, $schemaProcessor, $schema)); + $this->addDefinition('#', new SchemaDefinition($schema->getJsonSchema(), $schemaProcessor, $schema)); - foreach ($propertyData as $key => $propertyEntry) { + foreach ($schema->getJsonSchema()->getJson() as $key => $propertyEntry) { if (!is_array($propertyEntry)) { continue; } // add the root nodes of the schema to resolve path references - $this->addDefinition($key, new SchemaDefinition($propertyEntry, $schemaProcessor, $schema)); + $this->addDefinition( + $key, + new SchemaDefinition($schema->getJsonSchema()->withJson($propertyEntry), $schemaProcessor, $schema) + ); } - $this->fetchDefinitionsById($propertyData, $schemaProcessor, $schema); + $this->fetchDefinitionsById($schema->getJsonSchema(), $schemaProcessor, $schema); } /** * Fetch all schema definitions with an ID for direct references * - * @param array $propertyData + * @param JsonSchema $jsonSchema * @param SchemaProcessor $schemaProcessor * @param Schema $schema */ - protected function fetchDefinitionsById(array $propertyData, SchemaProcessor $schemaProcessor, Schema $schema): void - { - if (isset($propertyData['$id'])) { + protected function fetchDefinitionsById( + JsonSchema $jsonSchema, + SchemaProcessor $schemaProcessor, + Schema $schema + ): void { + $json = $jsonSchema->getJson(); + + if (isset($json['$id'])) { $this->addDefinition( - strpos($propertyData['$id'], '#') === 0 ? $propertyData['$id'] : "#{$propertyData['$id']}", - new SchemaDefinition($propertyData, $schemaProcessor, $schema) + strpos($json['$id'], '#') === 0 ? $json['$id'] : "#{$json['$id']}", + new SchemaDefinition($jsonSchema, $schemaProcessor, $schema) ); } - foreach ($propertyData as $item) { + foreach ($json as $item) { if (!is_array($item)) { continue; } - $this->fetchDefinitionsById($item, $schemaProcessor, $schema); + $this->fetchDefinitionsById($jsonSchema->withJson($item), $schemaProcessor, $schema); } } @@ -176,8 +179,14 @@ protected function parseExternalFile( } // set up a dummy schema to fetch the definitions from the external file - $schema = new Schema('ExternalSchema_' . uniqid(), '', new self(dirname($jsonSchemaFilePath))); - $schema->getSchemaDictionary()->setUpDefinitionDictionary($decodedJsonSchema, $schemaProcessor, $schema); + $schema = new Schema( + 'ExternalSchema_' . uniqid(), + '', + new JsonSchema($jsonSchemaFilePath, $decodedJsonSchema), + new self(dirname($jsonSchemaFilePath)) + ); + + $schema->getSchemaDictionary()->setUpDefinitionDictionary($schemaProcessor, $schema); $this->parsedExternalFileSchemas[$jsonSchemaFile] = $schema; return $schema->getSchemaDictionary()->getDefinition($externalKey, $schemaProcessor, $path); diff --git a/src/Model/Validator/AdditionalItemsValidator.php b/src/Model/Validator/AdditionalItemsValidator.php index f3451a2..e1f5313 100644 --- a/src/Model/Validator/AdditionalItemsValidator.php +++ b/src/Model/Validator/AdditionalItemsValidator.php @@ -7,6 +7,7 @@ use PHPModelGenerator\Exception\Arrays\InvalidAdditionalTupleItemsException; use PHPModelGenerator\Exception\SchemaException; use PHPModelGenerator\Model\Schema; +use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; use PHPModelGenerator\SchemaProcessor\SchemaProcessor; /** @@ -28,7 +29,7 @@ class AdditionalItemsValidator extends AdditionalPropertiesValidator * * @param SchemaProcessor $schemaProcessor * @param Schema $schema - * @param array $propertiesStructure + * @param JsonSchema $propertiesStructure * @param string $propertyName * * @throws SchemaException @@ -36,7 +37,7 @@ class AdditionalItemsValidator extends AdditionalPropertiesValidator public function __construct( SchemaProcessor $schemaProcessor, Schema $schema, - array $propertiesStructure, + JsonSchema $propertiesStructure, string $propertyName ) { parent::__construct($schemaProcessor, $schema, $propertiesStructure, $propertyName); diff --git a/src/Model/Validator/AdditionalPropertiesValidator.php b/src/Model/Validator/AdditionalPropertiesValidator.php index ab12c05..41f5fa0 100644 --- a/src/Model/Validator/AdditionalPropertiesValidator.php +++ b/src/Model/Validator/AdditionalPropertiesValidator.php @@ -8,6 +8,7 @@ use PHPModelGenerator\Exception\SchemaException; use PHPModelGenerator\Model\Property\PropertyInterface; use PHPModelGenerator\Model\Schema; +use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; use PHPModelGenerator\PropertyProcessor\PropertyMetaDataCollection; use PHPModelGenerator\PropertyProcessor\PropertyFactory; use PHPModelGenerator\PropertyProcessor\PropertyProcessorFactory; @@ -36,7 +37,7 @@ class AdditionalPropertiesValidator extends PropertyTemplateValidator * * @param SchemaProcessor $schemaProcessor * @param Schema $schema - * @param array $propertiesStructure + * @param JsonSchema $propertiesStructure * @param string|null $propertyName * * @throws SchemaException @@ -44,7 +45,7 @@ class AdditionalPropertiesValidator extends PropertyTemplateValidator public function __construct( SchemaProcessor $schemaProcessor, Schema $schema, - array $propertiesStructure, + JsonSchema $propertiesStructure, ?string $propertyName = null ) { $propertyFactory = new PropertyFactory(new PropertyProcessorFactory()); @@ -54,7 +55,7 @@ public function __construct( $schemaProcessor, $schema, static::PROPERTY_NAME, - $propertiesStructure[static::ADDITIONAL_PROPERTIES_KEY] + $propertiesStructure->withJson($propertiesStructure->getJson()[static::ADDITIONAL_PROPERTIES_KEY]) ); parent::__construct( @@ -64,7 +65,7 @@ public function __construct( 'additionalProperties' => preg_replace( '(\d+\s=>)', '', - var_export(array_keys($propertiesStructure[static::PROPERTIES_KEY] ?? []), true) + var_export(array_keys($propertiesStructure->getJson()[static::PROPERTIES_KEY] ?? []), true) ), 'generatorConfiguration' => $schemaProcessor->getGeneratorConfiguration(), 'viewHelper' => new RenderHelper($schemaProcessor->getGeneratorConfiguration()), diff --git a/src/Model/Validator/ArrayItemValidator.php b/src/Model/Validator/ArrayItemValidator.php index e652d59..3a1a210 100644 --- a/src/Model/Validator/ArrayItemValidator.php +++ b/src/Model/Validator/ArrayItemValidator.php @@ -11,6 +11,7 @@ use PHPModelGenerator\Exception\SchemaException; use PHPModelGenerator\Model\Property\PropertyInterface; use PHPModelGenerator\Model\Schema; +use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; use PHPModelGenerator\PropertyProcessor\Decorator\TypeHint\ArrayTypeHintDecorator; use PHPModelGenerator\PropertyProcessor\PropertyMetaDataCollection; use PHPModelGenerator\PropertyProcessor\PropertyFactory; @@ -35,7 +36,7 @@ class ArrayItemValidator extends PropertyTemplateValidator * * @param SchemaProcessor $schemaProcessor * @param Schema $schema - * @param array $itemStructure + * @param JsonSchema $itemStructure * @param PropertyInterface $property * * @throws SchemaException @@ -46,7 +47,7 @@ class ArrayItemValidator extends PropertyTemplateValidator public function __construct( SchemaProcessor $schemaProcessor, Schema $schema, - array $itemStructure, + JsonSchema $itemStructure, PropertyInterface $property ) { $this->variableSuffix = '_' . uniqid(); diff --git a/src/Model/Validator/ArrayTupleValidator.php b/src/Model/Validator/ArrayTupleValidator.php index 75b6068..bd4a822 100644 --- a/src/Model/Validator/ArrayTupleValidator.php +++ b/src/Model/Validator/ArrayTupleValidator.php @@ -8,6 +8,7 @@ use PHPModelGenerator\Exception\SchemaException; use PHPModelGenerator\Model\Property\PropertyInterface; use PHPModelGenerator\Model\Schema; +use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; use PHPModelGenerator\PropertyProcessor\PropertyMetaDataCollection; use PHPModelGenerator\PropertyProcessor\PropertyFactory; use PHPModelGenerator\PropertyProcessor\PropertyProcessorFactory; @@ -29,7 +30,7 @@ class ArrayTupleValidator extends PropertyTemplateValidator * * @param SchemaProcessor $schemaProcessor * @param Schema $schema - * @param array $propertiesStructure + * @param JsonSchema $propertiesStructure * @param string $propertyName * * @throws SchemaException @@ -37,13 +38,13 @@ class ArrayTupleValidator extends PropertyTemplateValidator public function __construct( SchemaProcessor $schemaProcessor, Schema $schema, - array $propertiesStructure, + JsonSchema $propertiesStructure, string $propertyName ) { $propertyFactory = new PropertyFactory(new PropertyProcessorFactory()); $this->tupleProperties = []; - foreach ($propertiesStructure as $tupleIndex => $tupleItem) { + 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 @@ -52,7 +53,7 @@ public function __construct( $schemaProcessor, $schema, $tupleItemName, - $tupleItem + $propertiesStructure->withJson($tupleItem) ); } diff --git a/src/Model/Validator/FilterValidator.php b/src/Model/Validator/FilterValidator.php index b2e2280..2528fb8 100644 --- a/src/Model/Validator/FilterValidator.php +++ b/src/Model/Validator/FilterValidator.php @@ -24,6 +24,8 @@ class FilterValidator extends PropertyTemplateValidator { /** @var FilterInterface $filter */ protected $filter; + /** @var array */ + protected $filterOptions; /** * FilterValidator constructor. @@ -45,6 +47,7 @@ public function __construct( ?TransformingFilterInterface $transformingFilter = null ) { $this->filter = $filter; + $this->filterOptions = $filterOptions; $transformingFilter === null ? $this->validateFilterCompatibilityWithBaseType($filter, $property) @@ -137,10 +140,11 @@ private function validateFilterCompatibilityWithBaseType(FilterInterface $filter ) { throw new SchemaException( sprintf( - 'Filter %s is not compatible with property type %s for property %s', + 'Filter %s is not compatible with property type %s for property %s in file %s', $filter->getToken(), $property->getType(), - $property->getName() + $property->getName(), + $property->getJsonSchema()->getFile() ) ); } @@ -171,10 +175,11 @@ private function validateFilterCompatibilityWithTransformedType( ) { throw new SchemaException( sprintf( - 'Filter %s is not compatible with transformed property type %s for property %s', + 'Filter %s is not compatible with transformed property type %s for property %s in file %s', $filter->getToken(), $transformedType->getName(), - $property->getName() + $property->getName(), + $property->getJsonSchema()->getFile() ) ); } @@ -199,4 +204,20 @@ private function mapDataTypes(array $acceptedTypes): array } }, $acceptedTypes); } + + /** + * @return FilterInterface + */ + public function getFilter(): FilterInterface + { + return $this->filter; + } + + /** + * @return array + */ + public function getFilterOptions(): array + { + return $this->filterOptions; + } } diff --git a/src/Model/Validator/PropertyNamesValidator.php b/src/Model/Validator/PropertyNamesValidator.php index 2cb8397..d3461c4 100644 --- a/src/Model/Validator/PropertyNamesValidator.php +++ b/src/Model/Validator/PropertyNamesValidator.php @@ -10,6 +10,7 @@ use PHPModelGenerator\Exception\Object\InvalidPropertyNamesException; use PHPModelGenerator\Exception\SchemaException; use PHPModelGenerator\Model\Schema; +use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; use PHPModelGenerator\Model\Validator; use PHPModelGenerator\PropertyProcessor\Property\StringProcessor; use PHPModelGenerator\PropertyProcessor\PropertyMetaDataCollection; @@ -27,8 +28,8 @@ class PropertyNamesValidator extends PropertyTemplateValidator * PropertyNamesValidator constructor. * * @param SchemaProcessor $schemaProcessor - * @param Schema $schema - * @param array $propertiesNames + * @param Schema $schema + * @param JsonSchema $propertiesNames * * @throws FileSystemException * @throws SyntaxErrorException @@ -38,7 +39,7 @@ class PropertyNamesValidator extends PropertyTemplateValidator public function __construct( SchemaProcessor $schemaProcessor, Schema $schema, - array $propertiesNames + JsonSchema $propertiesNames ) { $nameValidationProperty = (new StringProcessor(new PropertyMetaDataCollection(), $schemaProcessor, $schema)) ->process('property name', $propertiesNames) diff --git a/src/Model/Validator/SchemaDependencyValidator.php b/src/Model/Validator/SchemaDependencyValidator.php index 2694097..ed2b9a1 100644 --- a/src/Model/Validator/SchemaDependencyValidator.php +++ b/src/Model/Validator/SchemaDependencyValidator.php @@ -39,7 +39,7 @@ public function __construct(SchemaProcessor $schemaProcessor, PropertyInterface 'propertyName' => $property->getName(), 'transferProperties' => $schema->getProperties(), // set up a helper property for handling of the nested object - 'nestedProperty' => (new Property("{$property->getName()}Dependency", '')) + 'nestedProperty' => (new Property("{$property->getName()}Dependency", '', $schema->getJsonSchema())) ->addDecorator(new ObjectInstantiationDecorator( $schema->getClassName(), $schemaProcessor->getGeneratorConfiguration() diff --git a/src/ModelGenerator.php b/src/ModelGenerator.php index 4a3085a..ba1cc6b 100644 --- a/src/ModelGenerator.php +++ b/src/ModelGenerator.php @@ -9,6 +9,8 @@ use PHPModelGenerator\Exception\RenderException; use PHPModelGenerator\Exception\SchemaException; use PHPModelGenerator\Model\GeneratorConfiguration; +use PHPModelGenerator\SchemaProcessor\PostProcessor\PostProcessorInterface; +use PHPModelGenerator\SchemaProcessor\PostProcessor\SerializationPostProcessor; use PHPModelGenerator\SchemaProcessor\RenderQueue; use PHPModelGenerator\SchemaProcessor\SchemaProcessor; use PHPModelGenerator\SchemaProvider\SchemaProviderInterface; @@ -24,6 +26,8 @@ class ModelGenerator { /** @var GeneratorConfiguration */ protected $generatorConfiguration; + /** @var PostProcessorInterface[] */ + protected $postProcessors = []; /** * Generator constructor. @@ -33,6 +37,22 @@ class ModelGenerator public function __construct(GeneratorConfiguration $generatorConfiguration = null) { $this->generatorConfiguration = $generatorConfiguration ?? new GeneratorConfiguration(); + + if ($this->generatorConfiguration->hasSerializationEnabled()) { + $this->addPostProcessor(new SerializationPostProcessor()); + } + } + + /** + * @param PostProcessorInterface $postProcessor + * + * @return $this + */ + public function addPostProcessor(PostProcessorInterface $postProcessor): self + { + $this->postProcessors[] = $postProcessor; + + return $this; } /** @@ -88,12 +108,12 @@ public function generateModels(SchemaProviderInterface $schemaProvider, string $ $renderQueue ); - foreach ($schemaProvider->getSchemas() as [$filePath, $jsonSchema]) { - $schemaProcessor->process($jsonSchema, $filePath); + foreach ($schemaProvider->getSchemas() as $jsonSchema) { + $schemaProcessor->process($jsonSchema); } // render all collected classes - $renderQueue->execute($destination, $this->generatorConfiguration); + $renderQueue->execute($destination, $this->generatorConfiguration, $this->postProcessors); if ($this->generatorConfiguration->hasPrettyPrintEnabled()) { // @codeCoverageIgnoreStart diff --git a/src/PropertyProcessor/ComposedValue/AbstractComposedValueProcessor.php b/src/PropertyProcessor/ComposedValue/AbstractComposedValueProcessor.php index c055d69..dd2b19f 100644 --- a/src/PropertyProcessor/ComposedValue/AbstractComposedValueProcessor.php +++ b/src/PropertyProcessor/ComposedValue/AbstractComposedValueProcessor.php @@ -9,6 +9,8 @@ use PHPModelGenerator\Model\Property\Property; use PHPModelGenerator\Model\Property\PropertyInterface; use PHPModelGenerator\Model\Schema; +use PHPModelGenerator\Model\SchemaDefinition\ComposedJsonSchema; +use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; use PHPModelGenerator\Model\Validator; use PHPModelGenerator\Model\Validator\ComposedPropertyValidator; use PHPModelGenerator\Model\Validator\RequiredPropertyValidator; @@ -56,9 +58,11 @@ public function __construct( /** * @inheritdoc */ - protected function generateValidators(PropertyInterface $property, array $propertyData): void + protected function generateValidators(PropertyInterface $property, JsonSchema $propertySchema): void { - if (empty($propertyData['propertyData'][$propertyData['type']]) && + $json = $propertySchema->getJson()['propertySchema']->getJson(); + + if (empty($json[$propertySchema->getJson()['type']]) && $this->schemaProcessor->getGeneratorConfiguration()->isOutputEnabled() ) { // @codeCoverageIgnoreStart @@ -66,7 +70,7 @@ protected function generateValidators(PropertyInterface $property, array $proper // @codeCoverageIgnoreEnd } - $compositionProperties = $this->getCompositionProperties($property, $propertyData); + $compositionProperties = $this->getCompositionProperties($property, $propertySchema); $this->transferPropertyType($property, $compositionProperties); @@ -89,10 +93,11 @@ protected function generateValidators(PropertyInterface $property, array $proper 'postPropose' => $this instanceof ComposedPropertiesInterface, 'mergedProperty' => !$this->rootLevelComposition && $this instanceof MergedComposedPropertiesInterface - ? $this->createMergedProperty($property, $compositionProperties, $propertyData) + ? $this->createMergedProperty($property, $compositionProperties, $propertySchema) : null, 'onlyForDefinedValues' => - $propertyData['onlyForDefinedValues'] && $this instanceof ComposedPropertiesInterface, + $propertySchema->getJson()['onlyForDefinedValues'] + && $this instanceof ComposedPropertiesInterface, ] ), 100 @@ -100,21 +105,22 @@ protected function generateValidators(PropertyInterface $property, array $proper } /** - * Set up composition properties for the given propertyData schema + * Set up composition properties for the given property schema * * @param PropertyInterface $property - * @param array $propertyData + * @param JsonSchema $propertySchema * * @return CompositionPropertyDecorator[] * * @throws SchemaException */ - protected function getCompositionProperties(PropertyInterface $property, array $propertyData): array + protected function getCompositionProperties(PropertyInterface $property, JsonSchema $propertySchema): array { $propertyFactory = new PropertyFactory(new PropertyProcessorFactory()); $compositionProperties = []; + $json = $propertySchema->getJson()['propertySchema']->getJson(); - foreach ($propertyData['propertyData'][$propertyData['type']] as $compositionElement) { + foreach ($json[$propertySchema->getJson()['type']] as $compositionElement) { $compositionProperty = new CompositionPropertyDecorator( $propertyFactory ->create( @@ -122,7 +128,7 @@ protected function getCompositionProperties(PropertyInterface $property, array $ $this->schemaProcessor, $this->schema, $property->getName(), - $compositionElement + $propertySchema->getJson()['propertySchema']->withJson($compositionElement) ) ); @@ -172,7 +178,7 @@ function (CompositionPropertyDecorator $property): string { * * @param PropertyInterface $property * @param CompositionPropertyDecorator[] $compositionProperties - * @param array $propertyData + * @param JsonSchema $propertySchema * * @return PropertyInterface|null * @@ -181,7 +187,7 @@ function (CompositionPropertyDecorator $property): string { private function createMergedProperty( PropertyInterface $property, array $compositionProperties, - array $propertyData + JsonSchema $propertySchema ): ?PropertyInterface { $redirectToProperty = $this->redirectMergedProperty($compositionProperties); if ($redirectToProperty === null || $redirectToProperty instanceof PropertyInterface) { @@ -197,7 +203,7 @@ private function createMergedProperty( ->getClassNameGenerator() ->getClassName( $property->getName(), - $propertyData['propertyData'], + $propertySchema, true, $this->schemaProcessor->getCurrentClassName() ); @@ -207,8 +213,9 @@ private function createMergedProperty( return self::$generatedMergedProperties[$mergedClassName]; } - $mergedPropertySchema = new Schema($this->schema->getClassPath(), $mergedClassName); - $mergedProperty = new Property('MergedProperty', $mergedClassName); + $mergedPropertySchema = new Schema($this->schema->getClassPath(), $mergedClassName, $propertySchema); + + $mergedProperty = new Property('MergedProperty', $mergedClassName, $mergedPropertySchema->getJsonSchema()); self::$generatedMergedProperties[$mergedClassName] = $mergedProperty; $this->transferPropertiesToMergedSchema($mergedPropertySchema, $compositionProperties); diff --git a/src/PropertyProcessor/ComposedValue/IfProcessor.php b/src/PropertyProcessor/ComposedValue/IfProcessor.php index c300e4e..46866e3 100644 --- a/src/PropertyProcessor/ComposedValue/IfProcessor.php +++ b/src/PropertyProcessor/ComposedValue/IfProcessor.php @@ -7,6 +7,7 @@ use PHPModelGenerator\Exception\SchemaException; use PHPModelGenerator\Model\Property\CompositionPropertyDecorator; use PHPModelGenerator\Model\Property\PropertyInterface; +use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; use PHPModelGenerator\Model\Validator; use PHPModelGenerator\Model\Validator\ComposedPropertyValidator; use PHPModelGenerator\Model\Validator\ConditionalPropertyValidator; @@ -27,10 +28,18 @@ class IfProcessor extends AbstractValueProcessor implements ComposedPropertiesIn /** * @inheritdoc */ - protected function generateValidators(PropertyInterface $property, array $propertyData): void + protected function generateValidators(PropertyInterface $property, JsonSchema $propertySchema): void { - if (!isset($propertyData['propertyData']['then']) && !isset($propertyData['propertyData']['else'])) { - throw new SchemaException('Incomplete conditional composition'); + $json = $propertySchema->getJson()['propertySchema']->getJson(); + + if (!isset($json['then']) && !isset($json['else'])) { + throw new SchemaException( + sprintf( + 'Incomplete conditional composition for property %s in file %s', + $property->getName(), + $property->getJsonSchema()->getFile() + ) + ); } $propertyFactory = new PropertyFactory(new PropertyProcessorFactory()); @@ -38,7 +47,7 @@ protected function generateValidators(PropertyInterface $property, array $proper $properties = []; foreach (['if', 'then', 'else'] as $compositionElement) { - if (!isset($propertyData['propertyData'][$compositionElement])) { + if (!isset($json[$compositionElement])) { $properties[$compositionElement] = null; continue; } @@ -50,7 +59,7 @@ protected function generateValidators(PropertyInterface $property, array $proper $this->schemaProcessor, $this->schema, $property->getName(), - $propertyData['propertyData'][$compositionElement] + $propertySchema->getJson()['propertySchema']->withJson($json[$compositionElement]) ) ); @@ -72,12 +81,12 @@ protected function generateValidators(PropertyInterface $property, array $proper 'elseProperty' => $properties['else'], 'generatorConfiguration' => $this->schemaProcessor->getGeneratorConfiguration(), 'viewHelper' => new RenderHelper($this->schemaProcessor->getGeneratorConfiguration()), - 'onlyForDefinedValues' => $propertyData['onlyForDefinedValues'], + 'onlyForDefinedValues' => $propertySchema->getJson()['onlyForDefinedValues'], ] ), 100 ); - parent::generateValidators($property, $propertyData); + //parent::generateValidators($property, $propertySchema); } } diff --git a/src/PropertyProcessor/ComposedValue/NotProcessor.php b/src/PropertyProcessor/ComposedValue/NotProcessor.php index 102d1cb..3fba3c0 100644 --- a/src/PropertyProcessor/ComposedValue/NotProcessor.php +++ b/src/PropertyProcessor/ComposedValue/NotProcessor.php @@ -3,6 +3,7 @@ namespace PHPModelGenerator\PropertyProcessor\ComposedValue; use PHPModelGenerator\Model\Property\PropertyInterface; +use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; /** * Class NotProcessor @@ -14,13 +15,24 @@ class NotProcessor extends AbstractComposedValueProcessor /** * @inheritdoc */ - protected function generateValidators(PropertyInterface $property, array $propertyData): void + protected function generateValidators(PropertyInterface $property, JsonSchema $propertySchema): void { + $json = $propertySchema->getJson()['propertySchema']->getJson(); + // as the not composition only takes one schema nest it one level deeper to use the ComposedValueProcessor - $propertyData['propertyData']['not'] = [$propertyData['propertyData']['not']]; + $json['not'] = [$json['not']]; + // strict type checks for not constraint to avoid issues with null $property->setRequired(true); - parent::generateValidators($property, $propertyData); + parent::generateValidators( + $property, + $propertySchema->withJson( + array_merge( + $propertySchema->getJson(), + ['propertySchema' => $propertySchema->getJson()['propertySchema']->withJson($json)] + ) + ) + ); } /** diff --git a/src/PropertyProcessor/ComposedValueProcessorFactory.php b/src/PropertyProcessor/ComposedValueProcessorFactory.php index 7f367c1..428665e 100644 --- a/src/PropertyProcessor/ComposedValueProcessorFactory.php +++ b/src/PropertyProcessor/ComposedValueProcessorFactory.php @@ -38,9 +38,6 @@ public function getProcessor( Schema $schema ): PropertyProcessorInterface { $processor = '\\PHPModelGenerator\\PropertyProcessor\\ComposedValue\\' . ucfirst($type) . 'Processor'; - if (!class_exists($processor)) { - throw new SchemaException("Unsupported composed value type $type"); - } return new $processor($propertyMetaDataCollection, $schemaProcessor, $schema, $this->rootLevelComposition); } diff --git a/src/PropertyProcessor/Filter/FilterProcessor.php b/src/PropertyProcessor/Filter/FilterProcessor.php index c36a5ea..7551912 100644 --- a/src/PropertyProcessor/Filter/FilterProcessor.php +++ b/src/PropertyProcessor/Filter/FilterProcessor.php @@ -59,7 +59,14 @@ public function process( } if (!($filter = $generatorConfiguration->getFilter($filterToken))) { - throw new SchemaException("Unsupported filter $filterToken"); + throw new SchemaException( + sprintf( + 'Unsupported filter %s on property %s in file %s', + $filterToken, + $property->getName(), + $property->getJsonSchema()->getFile() + ) + ); } $property->addValidator( @@ -70,12 +77,20 @@ public function process( if ($filter instanceof TransformingFilterInterface) { if ($property->getType() === 'array') { throw new SchemaException( - "Applying a transforming filter to the array property {$property->getName()} is not supported" + sprintf( + 'Applying a transforming filter to the array property %s is not supported in file %s', + $property->getName(), + $property->getJsonSchema()->getFile() + ) ); } if ($transformingFilter) { throw new SchemaException( - "Applying multiple transforming filters for property {$property->getName()} is not supported" + sprintf( + 'Applying multiple transforming filters for property %s is not supported in file %s', + $property->getName(), + $property->getJsonSchema()->getFile() + ) ); } @@ -100,11 +115,6 @@ public function process( if (!$typeAfterFilter->isBuiltin()) { $schema->addUsedClass($typeAfterFilter->getName()); } - - $schema->addCustomSerializer( - $property->getAttribute(), - new TransformingFilterSerializer($property->getAttribute(), $filter, $filterOptions) - ); } } } diff --git a/src/PropertyProcessor/Property/AbstractNumericProcessor.php b/src/PropertyProcessor/Property/AbstractNumericProcessor.php index 976331b..8b9f499 100644 --- a/src/PropertyProcessor/Property/AbstractNumericProcessor.php +++ b/src/PropertyProcessor/Property/AbstractNumericProcessor.php @@ -10,6 +10,7 @@ use PHPModelGenerator\Exception\Number\MinimumException; use PHPModelGenerator\Exception\Number\MultipleOfException; use PHPModelGenerator\Model\Property\PropertyInterface; +use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; use PHPModelGenerator\Model\Validator\PropertyValidator; /** @@ -19,8 +20,6 @@ */ abstract class AbstractNumericProcessor extends AbstractTypedValueProcessor { - protected const LIMIT_MESSAGE = 'Value for %s must %s than %s'; - protected const JSON_FIELD_MINIMUM = 'minimum'; protected const JSON_FIELD_MAXIMUM = 'maximum'; @@ -32,16 +31,16 @@ abstract class AbstractNumericProcessor extends AbstractTypedValueProcessor /** * @inheritdoc */ - protected function generateValidators(PropertyInterface $property, array $propertyData): void + protected function generateValidators(PropertyInterface $property, JsonSchema $propertySchema): void { - parent::generateValidators($property, $propertyData); + parent::generateValidators($property, $propertySchema); - $this->addRangeValidator($property, $propertyData, self::JSON_FIELD_MINIMUM, '<', MinimumException::class); - $this->addRangeValidator($property, $propertyData, self::JSON_FIELD_MAXIMUM, '>', MaximumException::class); + $this->addRangeValidator($property, $propertySchema, self::JSON_FIELD_MINIMUM, '<', MinimumException::class); + $this->addRangeValidator($property, $propertySchema, self::JSON_FIELD_MAXIMUM, '>', MaximumException::class); $this->addRangeValidator( $property, - $propertyData, + $propertySchema, self::JSON_FIELD_MINIMUM_EXCLUSIVE, '<=', ExclusiveMinimumException::class @@ -49,40 +48,42 @@ protected function generateValidators(PropertyInterface $property, array $proper $this->addRangeValidator( $property, - $propertyData, + $propertySchema, self::JSON_FIELD_MAXIMUM_EXCLUSIVE, '>=', ExclusiveMaximumException::class ); - $this->addMultipleOfValidator($property, $propertyData); + $this->addMultipleOfValidator($property, $propertySchema); } /** * Adds a range validator to the property * * @param PropertyInterface $property The property which shall be validated - * @param array $propertyData The data for the property + * @param JsonSchema $propertySchema The schema for the property * @param string $field Which field of the property data provides the validation value * @param string $check The check to execute (eg. '<', '>') * @param string $exceptionClass The exception class for the validation */ protected function addRangeValidator( PropertyInterface $property, - array $propertyData, + JsonSchema $propertySchema, string $field, string $check, string $exceptionClass ): void { - if (!isset($propertyData[$field])) { + $json = $propertySchema->getJson(); + + if (!isset($json[$field])) { return; } $property->addValidator( new PropertyValidator( - $this->getTypeCheck() . "\$value $check {$propertyData[$field]}", + $this->getTypeCheck() . "\$value $check {$json[$field]}", $exceptionClass, - [$property->getName(), $propertyData[$field]] + [$property->getName(), $json[$field]] ) ); } @@ -91,26 +92,28 @@ protected function addRangeValidator( * Adds a multiple of validator to the property * * @param PropertyInterface $property - * @param array $propertyData + * @param JsonSchema $propertySchema */ - protected function addMultipleOfValidator(PropertyInterface $property, array $propertyData) + protected function addMultipleOfValidator(PropertyInterface $property, JsonSchema $propertySchema) { - if (!isset($propertyData[self::JSON_FIELD_MULTIPLE])) { + $json = $propertySchema->getJson(); + + if (!isset($json[self::JSON_FIELD_MULTIPLE])) { return; } $property->addValidator( new PropertyValidator( // type unsafe comparison to be compatible with int and float - $propertyData[self::JSON_FIELD_MULTIPLE] == 0 + $json[self::JSON_FIELD_MULTIPLE] == 0 ? $this->getTypeCheck() . '$value != 0' : ( static::TYPE === 'int' - ? $this->getTypeCheck() . "\$value % {$propertyData[self::JSON_FIELD_MULTIPLE]} != 0" - : $this->getTypeCheck() . "fmod(\$value, {$propertyData[self::JSON_FIELD_MULTIPLE]}) != 0" + ? $this->getTypeCheck() . "\$value % {$json[self::JSON_FIELD_MULTIPLE]} != 0" + : $this->getTypeCheck() . "fmod(\$value, {$json[self::JSON_FIELD_MULTIPLE]}) != 0" ), MultipleOfException::class, - [$property->getName(), $propertyData[self::JSON_FIELD_MULTIPLE]] + [$property->getName(), $json[self::JSON_FIELD_MULTIPLE]] ) ); } diff --git a/src/PropertyProcessor/Property/AbstractPropertyProcessor.php b/src/PropertyProcessor/Property/AbstractPropertyProcessor.php index 89e4417..8b285b9 100644 --- a/src/PropertyProcessor/Property/AbstractPropertyProcessor.php +++ b/src/PropertyProcessor/Property/AbstractPropertyProcessor.php @@ -8,6 +8,7 @@ use PHPModelGenerator\Model\Property\BaseProperty; use PHPModelGenerator\Model\Property\PropertyInterface; use PHPModelGenerator\Model\Schema; +use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; use PHPModelGenerator\Model\Validator\EnumValidator; use PHPModelGenerator\Model\Validator\PropertyDependencyValidator; use PHPModelGenerator\Model\Validator\RequiredPropertyValidator; @@ -55,11 +56,11 @@ public function __construct( * Generates the validators for the property * * @param PropertyInterface $property - * @param array $propertyData + * @param JsonSchema $propertySchema * * @throws SchemaException */ - protected function generateValidators(PropertyInterface $property, array $propertyData): void + protected function generateValidators(PropertyInterface $property, JsonSchema $propertySchema): void { if ($dependencies = $this->propertyMetaDataCollection->getAttributeDependencies($property->getName())) { $this->addDependencyValidator($property, $dependencies); @@ -69,11 +70,11 @@ protected function generateValidators(PropertyInterface $property, array $proper $property->addValidator(new RequiredPropertyValidator($property), 1); } - if (isset($propertyData['enum'])) { - $this->addEnumValidator($property, $propertyData['enum']); + if (isset($propertySchema->getJson()['enum'])) { + $this->addEnumValidator($property, $propertySchema->getJson()['enum']); } - $this->addComposedValueValidator($property, $propertyData); + $this->addComposedValueValidator($property, $propertySchema); } /** @@ -120,7 +121,7 @@ function ($dependency, $index) use (&$propertyDependency): void { } $dependencySchema = $this->schemaProcessor->processSchema( - $dependencies, + new JsonSchema($this->schema->getJsonSchema()->getFile(), $dependencies), $this->schema->getClassPath(), ucfirst("{$property->getName()}_Dependency_" . uniqid()), $this->schema->getSchemaDictionary() @@ -155,21 +156,21 @@ private function transferDependendPropertiesToBaseSchema(Schema $dependencySchem /** * @param PropertyInterface $property - * @param array $propertyData + * @param JsonSchema $propertySchema * * @throws SchemaException */ - protected function addComposedValueValidator(PropertyInterface $property, array $propertyData): void + protected function addComposedValueValidator(PropertyInterface $property, JsonSchema $propertySchema): void { $composedValueKeywords = ['allOf', 'anyOf', 'oneOf', 'not', 'if']; $propertyFactory = new PropertyFactory(new ComposedValueProcessorFactory($property instanceof BaseProperty)); foreach ($composedValueKeywords as $composedValueKeyword) { - if (!isset($propertyData[$composedValueKeyword])) { + if (!isset($propertySchema->getJson()[$composedValueKeyword])) { continue; } - $propertyData = $this->inheritPropertyType($propertyData, $composedValueKeyword); + $propertySchema = $this->inheritPropertyType($propertySchema, $composedValueKeyword); $composedProperty = $propertyFactory ->create( @@ -177,11 +178,11 @@ protected function addComposedValueValidator(PropertyInterface $property, array $this->schemaProcessor, $this->schema, $property->getName(), - [ + $propertySchema->withJson([ 'type' => $composedValueKeyword, - 'propertyData' => $propertyData, + 'propertySchema' => $propertySchema, 'onlyForDefinedValues' => !($this instanceof BaseProcessor) && !$property->isRequired(), - ] + ]) ); foreach ($composedProperty->getValidators() as $validator) { @@ -200,60 +201,64 @@ protected function addComposedValueValidator(PropertyInterface $property, array * If the type of a property containing a composition is defined outside of the composition make sure each * composition which doesn't define a type inherits the type * - * @param array $propertyData + * @param JsonSchema $propertySchema * @param string $composedValueKeyword * - * @return array + * @return JsonSchema */ - protected function inheritPropertyType(array $propertyData, string $composedValueKeyword): array + protected function inheritPropertyType(JsonSchema $propertySchema, string $composedValueKeyword): JsonSchema { - if (!isset($propertyData['type'])) { - return $propertyData; + $json = $propertySchema->getJson(); + + if (!isset($json['type'])) { + return $propertySchema; } - if ($propertyData['type'] === 'base') { - $propertyData['type'] = 'object'; + if ($json['type'] === 'base') { + $json['type'] = 'object'; } switch ($composedValueKeyword) { case 'not': - if (!isset($propertyData[$composedValueKeyword]['type'])) { - $propertyData[$composedValueKeyword]['type'] = $propertyData['type']; + if (!isset($json[$composedValueKeyword]['type'])) { + $json[$composedValueKeyword]['type'] = $json['type']; } break; case 'if': - return $this->inheritIfPropertyType($propertyData); + return $this->inheritIfPropertyType($propertySchema->withJson($json)); default: - foreach ($propertyData[$composedValueKeyword] as &$composedElement) { + foreach ($json[$composedValueKeyword] as &$composedElement) { if (!isset($composedElement['type'])) { - $composedElement['type'] = $propertyData['type']; + $composedElement['type'] = $json['type']; } } } - return $propertyData; + return $propertySchema->withJson($json); } /** * Inherit the type of a property into all composed components of a conditional composition * - * @param array $propertyData + * @param JsonSchema $propertySchema * - * @return array + * @return JsonSchema */ - protected function inheritIfPropertyType(array $propertyData): array + protected function inheritIfPropertyType(JsonSchema $propertySchema): JsonSchema { + $json = $propertySchema->getJson(); + foreach (['if', 'then', 'else'] as $composedValueKeyword) { - if (!isset($propertyData[$composedValueKeyword])) { + if (!isset($json[$composedValueKeyword])) { continue; } - if (!isset($propertyData[$composedValueKeyword]['type'])) { - $propertyData[$composedValueKeyword]['type'] = $propertyData['type']; + if (!isset($json[$composedValueKeyword]['type'])) { + $json[$composedValueKeyword]['type'] = $json['type']; } } - return $propertyData; + return $propertySchema->withJson($json); } /** diff --git a/src/PropertyProcessor/Property/AbstractTypedValueProcessor.php b/src/PropertyProcessor/Property/AbstractTypedValueProcessor.php index 8bef59f..62a8029 100644 --- a/src/PropertyProcessor/Property/AbstractTypedValueProcessor.php +++ b/src/PropertyProcessor/Property/AbstractTypedValueProcessor.php @@ -7,6 +7,7 @@ use PHPModelGenerator\Exception\SchemaException; use PHPModelGenerator\Model\Property\PropertyInterface; use PHPModelGenerator\Model\Schema; +use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; use PHPModelGenerator\Model\Validator\TypeCheckValidator; use PHPModelGenerator\PropertyProcessor\PropertyMetaDataCollection; use PHPModelGenerator\SchemaProcessor\SchemaProcessor; @@ -37,18 +38,18 @@ public function __construct( /** * @param string $propertyName - * @param array $propertyData + * @param JsonSchema $propertySchema * * @return PropertyInterface * * @throws SchemaException */ - public function process(string $propertyName, array $propertyData): PropertyInterface + public function process(string $propertyName, JsonSchema $propertySchema): PropertyInterface { - $property = parent::process($propertyName, $propertyData); + $property = parent::process($propertyName, $propertySchema); - if (isset($propertyData['default'])) { - $this->setDefaultValue($property, $propertyData['default']); + if (isset($propertySchema->getJson()['default'])) { + $this->setDefaultValue($property, $propertySchema->getJson()['default'], $propertySchema); } return $property; @@ -56,11 +57,12 @@ public function process(string $propertyName, array $propertyData): PropertyInte /** * @param PropertyInterface $property - * @param $default + * @param mixed $default + * @param JsonSchema $propertySchema * * @throws SchemaException */ - public function setDefaultValue(PropertyInterface $property, $default): void + public function setDefaultValue(PropertyInterface $property, $default, JsonSchema $propertySchema): void { // allow integer default values for Number properties if ($this instanceof NumberProcessor && is_int($default)) { @@ -68,7 +70,13 @@ public function setDefaultValue(PropertyInterface $property, $default): void } if (!$this->getTypeCheckFunction()($default)) { - throw new SchemaException("Invalid type for default value of property {$property->getName()}"); + throw new SchemaException( + sprintf( + "Invalid type for default value of property %s in file %s", + $property->getName(), + $propertySchema->getFile() + ) + ); } $property->setDefaultValue($default); @@ -77,9 +85,9 @@ public function setDefaultValue(PropertyInterface $property, $default): void /** * @inheritdoc */ - protected function generateValidators(PropertyInterface $property, array $propertyData): void + protected function generateValidators(PropertyInterface $property, JsonSchema $propertySchema): void { - parent::generateValidators($property, $propertyData); + parent::generateValidators($property, $propertySchema); $property->addValidator( new TypeCheckValidator(static::TYPE, $property, $this->isImplicitNullAllowed($property)), diff --git a/src/PropertyProcessor/Property/AbstractValueProcessor.php b/src/PropertyProcessor/Property/AbstractValueProcessor.php index 994f871..e9536f7 100644 --- a/src/PropertyProcessor/Property/AbstractValueProcessor.php +++ b/src/PropertyProcessor/Property/AbstractValueProcessor.php @@ -8,7 +8,7 @@ use PHPModelGenerator\Model\Property\Property; use PHPModelGenerator\Model\Property\PropertyInterface; use PHPModelGenerator\Model\Schema; -use PHPModelGenerator\PropertyProcessor\Decorator\TypeHint\TypeHintDecorator; +use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; use PHPModelGenerator\PropertyProcessor\Filter\FilterProcessor; use PHPModelGenerator\PropertyProcessor\PropertyMetaDataCollection; use PHPModelGenerator\SchemaProcessor\SchemaProcessor; @@ -47,21 +47,28 @@ public function __construct( * @throws ReflectionException * @throws SchemaException */ - public function process(string $propertyName, array $propertyData): PropertyInterface + public function process(string $propertyName, JsonSchema $propertySchema): PropertyInterface { - $property = (new Property($propertyName, $this->type, $propertyData['description'] ?? '')) + $json = $propertySchema->getJson(); + + $property = (new Property( + $propertyName, + $this->type, + $propertySchema, + $json['description'] ?? '' + )) ->setRequired($this->propertyMetaDataCollection->isAttributeRequired($propertyName)) ->setReadOnly( - (isset($propertyData['readOnly']) && $propertyData['readOnly'] === true) || + (isset($json['readOnly']) && $json['readOnly'] === true) || $this->schemaProcessor->getGeneratorConfiguration()->isImmutable() ); - $this->generateValidators($property, $propertyData); + $this->generateValidators($property, $propertySchema); - if (isset($propertyData['filter'])) { + if (isset($json['filter'])) { (new FilterProcessor())->process( $property, - $propertyData['filter'], + $json['filter'], $this->schemaProcessor->getGeneratorConfiguration(), $this->schema ); diff --git a/src/PropertyProcessor/Property/AnyProcessor.php b/src/PropertyProcessor/Property/AnyProcessor.php index 8257fe2..69524cc 100644 --- a/src/PropertyProcessor/Property/AnyProcessor.php +++ b/src/PropertyProcessor/Property/AnyProcessor.php @@ -5,6 +5,7 @@ namespace PHPModelGenerator\PropertyProcessor\Property; use PHPModelGenerator\Model\Property\PropertyInterface; +use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; /** * Class AnyProcessor @@ -15,16 +16,16 @@ class AnyProcessor extends AbstractValueProcessor { /** * @param string $propertyName - * @param array $propertyData + * @param JsonSchema $propertySchema * * @return PropertyInterface */ - public function process(string $propertyName, array $propertyData): PropertyInterface + public function process(string $propertyName, JsonSchema $propertySchema): PropertyInterface { - $property = parent::process($propertyName, $propertyData); + $property = parent::process($propertyName, $propertySchema); - if (isset($propertyData['default'])) { - $property->setDefaultValue($propertyData['default']); + if (isset($propertySchema->getJson()['default'])) { + $property->setDefaultValue($propertySchema->getJson()['default']); } return $property; diff --git a/src/PropertyProcessor/Property/ArrayProcessor.php b/src/PropertyProcessor/Property/ArrayProcessor.php index 438cbd7..0c34ecb 100644 --- a/src/PropertyProcessor/Property/ArrayProcessor.php +++ b/src/PropertyProcessor/Property/ArrayProcessor.php @@ -14,6 +14,7 @@ use PHPModelGenerator\Exception\Arrays\UniqueItemsException; use PHPModelGenerator\Exception\SchemaException; use PHPModelGenerator\Model\Property\PropertyInterface; +use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; use PHPModelGenerator\Model\Validator\AdditionalItemsValidator; use PHPModelGenerator\Model\Validator\ArrayItemValidator; use PHPModelGenerator\Model\Validator\ArrayTupleValidator; @@ -40,47 +41,49 @@ class ArrayProcessor extends AbstractTypedValueProcessor /** * @param PropertyInterface $property - * @param array $propertyData + * @param JsonSchema $propertySchema * * @throws FileSystemException * @throws SchemaException * @throws SyntaxErrorException * @throws UndefinedSymbolException */ - protected function generateValidators(PropertyInterface $property, array $propertyData): void + protected function generateValidators(PropertyInterface $property, JsonSchema $propertySchema): void { - parent::generateValidators($property, $propertyData); + parent::generateValidators($property, $propertySchema); - $this->addLengthValidation($property, $propertyData); - $this->addUniqueItemsValidation($property, $propertyData); - $this->addItemsValidation($property, $propertyData); - $this->addContainsValidation($property, $propertyData); + $this->addLengthValidation($property, $propertySchema); + $this->addUniqueItemsValidation($property, $propertySchema); + $this->addItemsValidation($property, $propertySchema); + $this->addContainsValidation($property, $propertySchema); } /** * Add the vaidation for the allowed amount of items in the array * * @param PropertyInterface $property - * @param array $propertyData + * @param JsonSchema $propertySchema */ - private function addLengthValidation(PropertyInterface $property, array $propertyData): void + private function addLengthValidation(PropertyInterface $property, JsonSchema $propertySchema): void { - if (isset($propertyData[self::JSON_FIELD_MIN_ITEMS])) { + $json = $propertySchema->getJson(); + + if (isset($json[self::JSON_FIELD_MIN_ITEMS])) { $property->addValidator( new PropertyValidator( - $this->getTypeCheck() . "count(\$value) < {$propertyData[self::JSON_FIELD_MIN_ITEMS]}", + $this->getTypeCheck() . "count(\$value) < {$json[self::JSON_FIELD_MIN_ITEMS]}", MinItemsException::class, - [$property->getName(), $propertyData[self::JSON_FIELD_MIN_ITEMS]] + [$property->getName(), $json[self::JSON_FIELD_MIN_ITEMS]] ) ); } - if (isset($propertyData[self::JSON_FIELD_MAX_ITEMS])) { + if (isset($json[self::JSON_FIELD_MAX_ITEMS])) { $property->addValidator( new PropertyValidator( - $this->getTypeCheck() . "count(\$value) > {$propertyData[self::JSON_FIELD_MAX_ITEMS]}", + $this->getTypeCheck() . "count(\$value) > {$json[self::JSON_FIELD_MAX_ITEMS]}", MaxItemsException::class, - [$property->getName(), $propertyData[self::JSON_FIELD_MAX_ITEMS]] + [$property->getName(), $json[self::JSON_FIELD_MAX_ITEMS]] ) ); } @@ -90,11 +93,13 @@ private function addLengthValidation(PropertyInterface $property, array $propert * Add the validator to check if the items inside an array are unique * * @param PropertyInterface $property - * @param array $propertyData + * @param JsonSchema $propertySchema */ - private function addUniqueItemsValidation(PropertyInterface $property, array $propertyData): void + private function addUniqueItemsValidation(PropertyInterface $property, JsonSchema $propertySchema): void { - if (!isset($propertyData['uniqueItems']) || $propertyData['uniqueItems'] !== true) { + $json = $propertySchema->getJson(); + + if (!isset($json['uniqueItems']) || $json['uniqueItems'] !== true) { return; } @@ -111,25 +116,27 @@ private function addUniqueItemsValidation(PropertyInterface $property, array $pr * Add the validator to check for constraints required for each item * * @param PropertyInterface $property - * @param array $propertyData + * @param JsonSchema $propertySchema * * @throws FileSystemException * @throws SchemaException * @throws SyntaxErrorException * @throws UndefinedSymbolException */ - private function addItemsValidation(PropertyInterface $property, array $propertyData): void + private function addItemsValidation(PropertyInterface $property, JsonSchema $propertySchema): void { - if (!isset($propertyData[self::JSON_FIELD_ITEMS])) { + $json = $propertySchema->getJson(); + + if (!isset($json[self::JSON_FIELD_ITEMS])) { return; } // check if the items require a tuple validation - if (is_array($propertyData[self::JSON_FIELD_ITEMS]) && - array_keys($propertyData[self::JSON_FIELD_ITEMS]) === - range(0, count($propertyData[self::JSON_FIELD_ITEMS]) - 1) + if (is_array($json[self::JSON_FIELD_ITEMS]) && + array_keys($json[self::JSON_FIELD_ITEMS]) === + range(0, count($json[self::JSON_FIELD_ITEMS]) - 1) ) { - $this->addTupleValidator($property, $propertyData); + $this->addTupleValidator($property, $propertySchema); return; } @@ -138,7 +145,7 @@ private function addItemsValidation(PropertyInterface $property, array $property new ArrayItemValidator( $this->schemaProcessor, $this->schema, - $propertyData[self::JSON_FIELD_ITEMS], + $propertySchema->withJson($json[self::JSON_FIELD_ITEMS]), $property ) ); @@ -148,24 +155,26 @@ private function addItemsValidation(PropertyInterface $property, array $property * Add the validator to check a tuple validation for each item of the array * * @param PropertyInterface $property - * @param array $propertyData + * @param JsonSchema $propertySchema * * @throws SchemaException * @throws FileSystemException * @throws SyntaxErrorException * @throws UndefinedSymbolException */ - private function addTupleValidator(PropertyInterface $property, array $propertyData): void + private function addTupleValidator(PropertyInterface $property, JsonSchema $propertySchema): void { - if (isset($propertyData['additionalItems']) && $propertyData['additionalItems'] !== true) { - $this->addAdditionalItemsValidator($property, $propertyData); + $json = $propertySchema->getJson(); + + if (isset($json['additionalItems']) && $json['additionalItems'] !== true) { + $this->addAdditionalItemsValidator($property, $propertySchema); } $property->addValidator( new ArrayTupleValidator( $this->schemaProcessor, $this->schema, - $propertyData[self::JSON_FIELD_ITEMS], + $propertySchema->withJson($json[self::JSON_FIELD_ITEMS]), $property->getName() ) ); @@ -173,21 +182,23 @@ private function addTupleValidator(PropertyInterface $property, array $propertyD /** * @param PropertyInterface $property - * @param array $propertyData + * @param JsonSchema $propertySchema * * @throws FileSystemException * @throws SchemaException * @throws SyntaxErrorException * @throws UndefinedSymbolException */ - private function addAdditionalItemsValidator(PropertyInterface $property, array $propertyData): void + private function addAdditionalItemsValidator(PropertyInterface $property, JsonSchema $propertySchema): void { - if (!is_bool($propertyData['additionalItems'])) { + $json = $propertySchema->getJson(); + + if (!is_bool($json['additionalItems'])) { $property->addValidator( new AdditionalItemsValidator( $this->schemaProcessor, $this->schema, - $propertyData, + $propertySchema, $property->getName() ) ); @@ -195,7 +206,7 @@ private function addAdditionalItemsValidator(PropertyInterface $property, array return; } - $expectedAmount = count($propertyData[self::JSON_FIELD_ITEMS]); + $expectedAmount = count($json[self::JSON_FIELD_ITEMS]); $property->addValidator( new PropertyValidator( @@ -210,13 +221,13 @@ private function addAdditionalItemsValidator(PropertyInterface $property, array * Add the validator to check for constraints required for at least one item * * @param PropertyInterface $property - * @param array $propertyData + * @param JsonSchema $propertySchema * * @throws SchemaException */ - private function addContainsValidation(PropertyInterface $property, array $propertyData): void + private function addContainsValidation(PropertyInterface $property, JsonSchema $propertySchema): void { - if (!isset($propertyData[self::JSON_FIELD_CONTAINS])) { + if (!isset($propertySchema->getJson()[self::JSON_FIELD_CONTAINS])) { return; } @@ -227,7 +238,7 @@ private function addContainsValidation(PropertyInterface $property, array $prope $this->schemaProcessor, $this->schema, "item of array {$property->getName()}", - $propertyData[self::JSON_FIELD_CONTAINS] + $propertySchema->withJson($propertySchema->getJson()[self::JSON_FIELD_CONTAINS]) ); $property->addValidator( diff --git a/src/PropertyProcessor/Property/BaseProcessor.php b/src/PropertyProcessor/Property/BaseProcessor.php index 0f6b54e..5cc7462 100644 --- a/src/PropertyProcessor/Property/BaseProcessor.php +++ b/src/PropertyProcessor/Property/BaseProcessor.php @@ -13,6 +13,7 @@ use PHPModelGenerator\Exception\SchemaException; use PHPModelGenerator\Model\Property\BaseProperty; use PHPModelGenerator\Model\Property\PropertyInterface; +use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; use PHPModelGenerator\Model\Validator; use PHPModelGenerator\Model\Validator\AbstractComposedPropertyValidator; use PHPModelGenerator\Model\Validator\AdditionalPropertiesValidator; @@ -33,11 +34,21 @@ class BaseProcessor extends AbstractPropertyProcessor { protected const TYPE = 'object'; + private const COUNT_PROPERTIES = + 'count( + array_unique( + array_merge( + array_keys($this->rawModelDataInput), + array_keys($modelData) + ) + ) + )'; + /** * @inheritdoc * * @param string $propertyName - * @param array $propertyData + * @param JsonSchema $propertySchema * * @return PropertyInterface * @@ -46,22 +57,22 @@ class BaseProcessor extends AbstractPropertyProcessor * @throws SyntaxErrorException * @throws UndefinedSymbolException */ - public function process(string $propertyName, array $propertyData): PropertyInterface + public function process(string $propertyName, JsonSchema $propertySchema): PropertyInterface { $this->schema ->getSchemaDictionary() - ->setUpDefinitionDictionary($propertyData, $this->schemaProcessor, $this->schema); + ->setUpDefinitionDictionary($this->schemaProcessor, $this->schema); // create a property which is used to gather composed properties validators. - $property = new BaseProperty($propertyName, static::TYPE); - $this->generateValidators($property, $propertyData); + $property = new BaseProperty($propertyName, static::TYPE, $propertySchema); + $this->generateValidators($property, $propertySchema); - $this->addPropertyNamesValidator($propertyData); - $this->addAdditionalPropertiesValidator($propertyData); - $this->addMinPropertiesValidator($propertyName, $propertyData); - $this->addMaxPropertiesValidator($propertyName, $propertyData); + $this->addPropertyNamesValidator($propertySchema); + $this->addAdditionalPropertiesValidator($propertySchema); + $this->addMinPropertiesValidator($propertyName, $propertySchema); + $this->addMaxPropertiesValidator($propertyName, $propertySchema); - $this->addPropertiesToSchema($propertyData); + $this->addPropertiesToSchema($propertySchema); $this->transferComposedPropertiesToSchema($property); return $property; @@ -70,16 +81,16 @@ public function process(string $propertyName, array $propertyData): PropertyInte /** * Add a validator to check all provided property names * - * @param array $propertyData + * @param JsonSchema $propertySchema * * @throws SchemaException * @throws FileSystemException * @throws SyntaxErrorException * @throws UndefinedSymbolException */ - protected function addPropertyNamesValidator(array $propertyData): void + protected function addPropertyNamesValidator(JsonSchema $propertySchema): void { - if (!isset($propertyData['propertyNames'])) { + if (!isset($propertySchema->getJson()['propertyNames'])) { return; } @@ -87,7 +98,7 @@ protected function addPropertyNamesValidator(array $propertyData): void new PropertyNamesValidator( $this->schemaProcessor, $this->schema, - $propertyData['propertyNames'] + $propertySchema->withJson($propertySchema->getJson()['propertyNames']) ) ); } @@ -95,25 +106,27 @@ protected function addPropertyNamesValidator(array $propertyData): void /** * Add an object validator to disallow properties which are not defined in the schema * - * @param array $propertyData + * @param JsonSchema $propertySchema * * @throws FileSystemException * @throws SchemaException * @throws SyntaxErrorException * @throws UndefinedSymbolException */ - protected function addAdditionalPropertiesValidator(array $propertyData): void + protected function addAdditionalPropertiesValidator(JsonSchema $propertySchema): void { - if (!isset($propertyData['additionalProperties']) || $propertyData['additionalProperties'] === true) { + $json = $propertySchema->getJson(); + + if (!isset($json['additionalProperties']) || $json['additionalProperties'] === true) { return; } - if (!is_bool($propertyData['additionalProperties'])) { + if (!is_bool($json['additionalProperties'])) { $this->schema->addBaseValidator( new AdditionalPropertiesValidator( $this->schemaProcessor, $this->schema, - $propertyData + $propertySchema ) ); @@ -124,7 +137,7 @@ protected function addAdditionalPropertiesValidator(array $propertyData): void new PropertyValidator( sprintf( '$additionalProperties = array_diff(array_keys($modelData), %s)', - preg_replace('(\d+\s=>)', '', var_export(array_keys($propertyData['properties'] ?? []), true)) + preg_replace('(\d+\s=>)', '', var_export(array_keys($json['properties'] ?? []), true)) ), AdditionalPropertiesException::class, [$this->schema->getClassName(), '&$additionalProperties'] @@ -136,19 +149,25 @@ protected function addAdditionalPropertiesValidator(array $propertyData): void * Add an object validator to limit the amount of provided properties * * @param string $propertyName - * @param array $propertyData + * @param JsonSchema $propertySchema */ - protected function addMaxPropertiesValidator(string $propertyName, array $propertyData): void + protected function addMaxPropertiesValidator(string $propertyName, JsonSchema $propertySchema): void { - if (!isset($propertyData['maxProperties'])) { + $json = $propertySchema->getJson(); + + if (!isset($json['maxProperties'])) { return; } $this->schema->addBaseValidator( new PropertyValidator( - sprintf('count($modelData) > %d', $propertyData['maxProperties']), + sprintf( + '%s > %d', + self::COUNT_PROPERTIES, + $json['maxProperties'] + ), MaxPropertiesException::class, - [$propertyName, $propertyData['maxProperties']] + [$propertyName, $json['maxProperties']] ) ); } @@ -157,19 +176,25 @@ protected function addMaxPropertiesValidator(string $propertyName, array $proper * Add an object validator to force at least the defined amount of properties to be provided * * @param string $propertyName - * @param array $propertyData + * @param JsonSchema $propertySchema */ - protected function addMinPropertiesValidator(string $propertyName, array $propertyData): void + protected function addMinPropertiesValidator(string $propertyName, JsonSchema $propertySchema): void { - if (!isset($propertyData['minProperties'])) { + $json = $propertySchema->getJson(); + + if (!isset($json['minProperties'])) { return; } $this->schema->addBaseValidator( new PropertyValidator( - sprintf('count($modelData) < %d', $propertyData['minProperties']), + sprintf( + '%s < %d', + self::COUNT_PROPERTIES, + $json['minProperties'] + ), MinPropertiesException::class, - [$propertyName, $propertyData['minProperties']] + [$propertyName, $json['minProperties']] ) ); } @@ -177,26 +202,28 @@ protected function addMinPropertiesValidator(string $propertyName, array $proper /** * Add the properties defined in the JSON schema to the current schema model * - * @param array $propertyData + * @param JsonSchema $propertySchema * * @throws SchemaException */ - protected function addPropertiesToSchema(array $propertyData) + protected function addPropertiesToSchema(JsonSchema $propertySchema): void { + $json = $propertySchema->getJson(); + $propertyFactory = new PropertyFactory(new PropertyProcessorFactory()); $propertyMetaDataCollection = new PropertyMetaDataCollection( - $propertyData['required'] ?? [], - $propertyData['dependencies'] ?? [] + $json['required'] ?? [], + $json['dependencies'] ?? [] ); - foreach ($propertyData['properties'] ?? [] as $propertyName => $propertyStructure) { + foreach ($json['properties'] ?? [] as $propertyName => $propertyStructure) { $this->schema->addProperty( $propertyFactory->create( $propertyMetaDataCollection, $this->schemaProcessor, $this->schema, $propertyName, - $propertyStructure + $propertySchema->withJson($propertyStructure) ) ); } @@ -210,7 +237,7 @@ protected function addPropertiesToSchema(array $propertyData) * * @throws SchemaException */ - protected function transferComposedPropertiesToSchema(PropertyInterface $property) + protected function transferComposedPropertiesToSchema(PropertyInterface $property): void { foreach ($property->getValidators() as $validator) { $validator = $validator->getValidator(); @@ -227,7 +254,13 @@ protected function transferComposedPropertiesToSchema(PropertyInterface $propert foreach ($validator->getComposedProperties() as $composedProperty) { if (!$composedProperty->getNestedSchema()) { - throw new SchemaException('No nested schema for composed property found'); + throw new SchemaException( + sprintf( + "No nested schema for composed property %s in file %s found", + $property->getName(), + $property->getJsonSchema()->getFile() + ) + ); } foreach ($composedProperty->getNestedSchema()->getProperties() as $property) { diff --git a/src/PropertyProcessor/Property/BasereferenceProcessor.php b/src/PropertyProcessor/Property/BasereferenceProcessor.php index 18c4060..e37dcbc 100644 --- a/src/PropertyProcessor/Property/BasereferenceProcessor.php +++ b/src/PropertyProcessor/Property/BasereferenceProcessor.php @@ -6,6 +6,7 @@ use PHPModelGenerator\Exception\SchemaException; use PHPModelGenerator\Model\Property\PropertyInterface; +use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; /** * Class BaseReferenceProcessor @@ -19,18 +20,22 @@ class BasereferenceProcessor extends ReferenceProcessor * * @throws SchemaException */ - public function process(string $propertyName, array $propertyData): PropertyInterface + public function process(string $propertyName, JsonSchema $propertySchema): PropertyInterface { // make sure definitions are available. By default the definition dictionary is set up by the BaseProcessor $this->schema ->getSchemaDictionary() - ->setUpDefinitionDictionary($propertyData, $this->schemaProcessor, $this->schema); + ->setUpDefinitionDictionary($this->schemaProcessor, $this->schema); - $property = parent::process($propertyName, $propertyData); + $property = parent::process($propertyName, $propertySchema); if (!$property->getNestedSchema()) { throw new SchemaException( - "A referenced schema on base level must provide an object definition [$propertyName]" + sprintf( + 'A referenced schema on base level must provide an object definition for property %s in file %s', + $propertyName, + $propertySchema->getFile() + ) ); } diff --git a/src/PropertyProcessor/Property/ConstProcessor.php b/src/PropertyProcessor/Property/ConstProcessor.php index bf106d0..a2f6a48 100644 --- a/src/PropertyProcessor/Property/ConstProcessor.php +++ b/src/PropertyProcessor/Property/ConstProcessor.php @@ -7,6 +7,7 @@ use PHPModelGenerator\Exception\Generic\InvalidConstException; use PHPModelGenerator\Model\Property\Property; use PHPModelGenerator\Model\Property\PropertyInterface; +use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; use PHPModelGenerator\Model\Validator\PropertyValidator; use PHPModelGenerator\PropertyProcessor\PropertyProcessorInterface; @@ -20,14 +21,21 @@ class ConstProcessor implements PropertyProcessorInterface /** * @inheritdoc */ - public function process(string $propertyName, array $propertyData): PropertyInterface + public function process(string $propertyName, JsonSchema $propertySchema): PropertyInterface { - return (new Property($propertyName, gettype($propertyData['const']), $propertyData['description'] ?? '')) + $json = $propertySchema->getJson(); + + return (new Property( + $propertyName, + gettype($json['const']), + $propertySchema, + $json['description'] ?? '' + )) ->setRequired(true) ->addValidator(new PropertyValidator( - '$value !== ' . var_export($propertyData['const'], true), + '$value !== ' . var_export($json['const'], true), InvalidConstException::class, - [$propertyName, $propertyData['const']] + [$propertyName, $json['const']] )); } } diff --git a/src/PropertyProcessor/Property/MultiTypeProcessor.php b/src/PropertyProcessor/Property/MultiTypeProcessor.php index b97c2b7..93efe8b 100644 --- a/src/PropertyProcessor/Property/MultiTypeProcessor.php +++ b/src/PropertyProcessor/Property/MultiTypeProcessor.php @@ -7,6 +7,7 @@ use PHPModelGenerator\Exception\SchemaException; use PHPModelGenerator\Model\Property\PropertyInterface; use PHPModelGenerator\Model\Schema; +use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; use PHPModelGenerator\Model\Validator\MultiTypeCheckValidator; use PHPModelGenerator\Model\Validator\TypeCheckInterface; use PHPModelGenerator\PropertyProcessor\Decorator\Property\PropertyTransferDecorator; @@ -65,22 +66,22 @@ public function __construct( * Process a property * * @param string $propertyName The name of the property - * @param array $propertyData An array containing the data of the property + * @param JsonSchema $propertySchema The schema of the property * * @return PropertyInterface * * @throws SchemaException * @throws ReflectionException */ - public function process(string $propertyName, array $propertyData): PropertyInterface + public function process(string $propertyName, JsonSchema $propertySchema): PropertyInterface { - $property = parent::process($propertyName, $propertyData); + $property = parent::process($propertyName, $propertySchema); foreach ($property->getValidators() as $validator) { $this->checks[] = $validator->getValidator()->getCheck(); } - $subProperties = $this->processSubProperties($propertyName, $propertyData, $property); + $subProperties = $this->processSubProperties($propertyName, $propertySchema, $property); if (empty($this->allowedPropertyTypes)) { return $property; @@ -135,7 +136,7 @@ protected function transferValidators(PropertyInterface $source, PropertyInterfa /** * @param string $propertyName - * @param array $propertyData + * @param JsonSchema $propertySchema * @param PropertyInterface $property * * @return PropertyInterface[] @@ -144,21 +145,22 @@ protected function transferValidators(PropertyInterface $source, PropertyInterfa */ protected function processSubProperties( string $propertyName, - array $propertyData, + JsonSchema $propertySchema, PropertyInterface $property ): array { $defaultValue = null; $invalidDefaultValueException = null; $invalidDefaultValues = 0; $subProperties = []; + $json = $propertySchema->getJson(); - if (isset($propertyData['default'])) { - $defaultValue = $propertyData['default']; - unset($propertyData['default']); + if (isset($json['default'])) { + $defaultValue = $json['default']; + unset($json['default']); } foreach ($this->propertyProcessors as $propertyProcessor) { - $subProperty = $propertyProcessor->process($propertyName, $propertyData); + $subProperty = $propertyProcessor->process($propertyName, $propertySchema->withJson($json)); $this->transferValidators($subProperty, $property); if ($subProperty->hasDecorators()) { @@ -167,7 +169,7 @@ protected function processSubProperties( if ($defaultValue !== null && $propertyProcessor instanceof AbstractTypedValueProcessor) { try { - $propertyProcessor->setDefaultValue($property, $defaultValue); + $propertyProcessor->setDefaultValue($property, $defaultValue, $propertySchema); } catch (SchemaException $e) { $invalidDefaultValues++; $invalidDefaultValueException = $e; diff --git a/src/PropertyProcessor/Property/NullProcessor.php b/src/PropertyProcessor/Property/NullProcessor.php index 3fedc46..7002e64 100644 --- a/src/PropertyProcessor/Property/NullProcessor.php +++ b/src/PropertyProcessor/Property/NullProcessor.php @@ -5,6 +5,7 @@ namespace PHPModelGenerator\PropertyProcessor\Property; use PHPModelGenerator\Model\Property\PropertyInterface; +use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; /** * Class NullProcessor @@ -19,11 +20,12 @@ class NullProcessor extends AbstractTypedValueProcessor * Explicitly unset the type of the property * * @param string $propertyName - * @param array $propertyData + * @param JsonSchema $propertySchema + * * @return PropertyInterface */ - public function process(string $propertyName, array $propertyData): PropertyInterface + public function process(string $propertyName, JsonSchema $propertySchema): PropertyInterface { - return (parent::process($propertyName, $propertyData))->setType(''); + return (parent::process($propertyName, $propertySchema))->setType(''); } } diff --git a/src/PropertyProcessor/Property/NumberProcessor.php b/src/PropertyProcessor/Property/NumberProcessor.php index 95b37cb..fe7946b 100644 --- a/src/PropertyProcessor/Property/NumberProcessor.php +++ b/src/PropertyProcessor/Property/NumberProcessor.php @@ -5,6 +5,7 @@ namespace PHPModelGenerator\PropertyProcessor\Property; use PHPModelGenerator\Model\Property\PropertyInterface; +use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; use PHPModelGenerator\PropertyProcessor\Decorator\Property\IntToFloatCastDecorator; /** @@ -19,8 +20,8 @@ class NumberProcessor extends AbstractNumericProcessor /** * @inheritdoc */ - public function process(string $propertyName, array $propertyData): PropertyInterface + public function process(string $propertyName, JsonSchema $propertySchema): PropertyInterface { - return parent::process($propertyName, $propertyData)->addDecorator(new IntToFloatCastDecorator()); + return parent::process($propertyName, $propertySchema)->addDecorator(new IntToFloatCastDecorator()); } } diff --git a/src/PropertyProcessor/Property/ObjectProcessor.php b/src/PropertyProcessor/Property/ObjectProcessor.php index 0f2ed5a..0ec0826 100644 --- a/src/PropertyProcessor/Property/ObjectProcessor.php +++ b/src/PropertyProcessor/Property/ObjectProcessor.php @@ -6,6 +6,7 @@ use PHPModelGenerator\Exception\SchemaException; use PHPModelGenerator\Model\Property\PropertyInterface; +use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; use PHPModelGenerator\Model\Validator\InstanceOfValidator; use PHPModelGenerator\PropertyProcessor\Decorator\Property\ObjectInstantiationDecorator; use PHPModelGenerator\PropertyProcessor\Decorator\SchemaNamespaceTransferDecorator; @@ -24,30 +25,26 @@ class ObjectProcessor extends AbstractTypedValueProcessor * * @throws SchemaException */ - public function process(string $propertyName, array $propertyData): PropertyInterface + public function process(string $propertyName, JsonSchema $propertySchema): PropertyInterface { - $property = parent::process($propertyName, $propertyData); + $property = parent::process($propertyName, $propertySchema); $className = $this->schemaProcessor->getGeneratorConfiguration()->getClassNameGenerator()->getClassName( $propertyName, - $propertyData, + $propertySchema, false, $this->schemaProcessor->getCurrentClassName() ); $schema = $this->schemaProcessor->processSchema( - $propertyData, + $propertySchema, $this->schemaProcessor->getCurrentClassPath(), $className, $this->schema->getSchemaDictionary() ); - if ($schema === null) { - throw new SchemaException("Failed to process schema for object property $propertyName"); - } - // if the generated schema is located in a different namespace (the schema for the given structure in - // $propertyData is duplicated) add used classes to the current schema. By importing the class which is + // $propertySchema is duplicated) add used classes to the current schema. By importing the class which is // represented by $schema and by transferring all imports of $schema as well as imports for all properties // of $schema to $this->schema the already generated schema can be used if ($schema->getClassPath() !== $this->schema->getClassPath() || diff --git a/src/PropertyProcessor/Property/ReferenceProcessor.php b/src/PropertyProcessor/Property/ReferenceProcessor.php index a352437..ae97293 100644 --- a/src/PropertyProcessor/Property/ReferenceProcessor.php +++ b/src/PropertyProcessor/Property/ReferenceProcessor.php @@ -7,6 +7,7 @@ use Exception; use PHPModelGenerator\Exception\SchemaException; use PHPModelGenerator\Model\Property\PropertyInterface; +use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; use PHPModelGenerator\PropertyProcessor\Decorator\SchemaNamespaceTransferDecorator; /** @@ -21,10 +22,10 @@ class ReferenceProcessor extends AbstractTypedValueProcessor * * @throws SchemaException */ - public function process(string $propertyName, array $propertyData): PropertyInterface + public function process(string $propertyName, JsonSchema $propertySchema): PropertyInterface { $path = []; - $reference = $propertyData['$ref']; + $reference = $propertySchema->getJson()['$ref']; $dictionary = $this->schema->getSchemaDictionary(); try { @@ -42,9 +43,13 @@ public function process(string $propertyName, array $propertyData): PropertyInte return $definition->resolveReference($propertyName, $path, $this->propertyMetaDataCollection); } } catch (Exception $exception) { - throw new SchemaException("Unresolved Reference: $reference", 0, $exception); + throw new SchemaException( + "Unresolved Reference $reference in file {$propertySchema->getFile()}", + 0, + $exception + ); } - throw new SchemaException("Unresolved Reference: $reference"); + throw new SchemaException("Unresolved Reference $reference in file {$propertySchema->getFile()}"); } } diff --git a/src/PropertyProcessor/Property/StringProcessor.php b/src/PropertyProcessor/Property/StringProcessor.php index a80c6ab..f548fba 100644 --- a/src/PropertyProcessor/Property/StringProcessor.php +++ b/src/PropertyProcessor/Property/StringProcessor.php @@ -9,6 +9,7 @@ use PHPModelGenerator\Exception\String\MinLengthException; use PHPModelGenerator\Exception\String\PatternException; use PHPModelGenerator\Model\Property\PropertyInterface; +use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; use PHPModelGenerator\Model\Validator\PropertyValidator; /** @@ -26,38 +27,40 @@ class StringProcessor extends AbstractTypedValueProcessor /** * @param PropertyInterface $property - * @param array $propertyData + * @param JsonSchema $propertySchema * * @throws SchemaException */ - protected function generateValidators(PropertyInterface $property, array $propertyData): void + protected function generateValidators(PropertyInterface $property, JsonSchema $propertySchema): void { - parent::generateValidators($property, $propertyData); + parent::generateValidators($property, $propertySchema); - $this->addPatternValidator($property, $propertyData); - $this->addLengthValidator($property, $propertyData); - $this->addFormatValidator($property, $propertyData); + $this->addPatternValidator($property, $propertySchema); + $this->addLengthValidator($property, $propertySchema); + $this->addFormatValidator($property, $propertySchema); } /** * Add a regex pattern validator * * @param PropertyInterface $property - * @param array $propertyData + * @param JsonSchema $propertySchema */ - protected function addPatternValidator(PropertyInterface $property, array $propertyData): void + protected function addPatternValidator(PropertyInterface $property, JsonSchema $propertySchema): void { - if (!isset($propertyData[static::JSON_FIELD_PATTERN])) { + $json = $propertySchema->getJson(); + + if (!isset($json[static::JSON_FIELD_PATTERN])) { return; } - $propertyData[static::JSON_FIELD_PATTERN] = addcslashes($propertyData[static::JSON_FIELD_PATTERN], "'"); + $json[static::JSON_FIELD_PATTERN] = addcslashes($json[static::JSON_FIELD_PATTERN], "'"); $property->addValidator( new PropertyValidator( - $this->getTypeCheck() . "!preg_match('/{$propertyData[static::JSON_FIELD_PATTERN]}/', \$value)", + $this->getTypeCheck() . "!preg_match('/{$json[static::JSON_FIELD_PATTERN]}/', \$value)", PatternException::class, - [$property->getName(), $propertyData[static::JSON_FIELD_PATTERN]] + [$property->getName(), $json[static::JSON_FIELD_PATTERN]] ) ); } @@ -65,27 +68,29 @@ protected function addPatternValidator(PropertyInterface $property, array $prope /** * Add min and max length validator * - * @param $property - * @param $propertyData + * @param PropertyInterface $property + * @param JsonSchema $propertySchema */ - protected function addLengthValidator(PropertyInterface $property, array $propertyData): void + protected function addLengthValidator(PropertyInterface $property, JsonSchema $propertySchema): void { - if (isset($propertyData[static::JSON_FIELD_MIN_LENGTH])) { + $json = $propertySchema->getJson(); + + if (isset($json[static::JSON_FIELD_MIN_LENGTH])) { $property->addValidator( new PropertyValidator( - $this->getTypeCheck() . "mb_strlen(\$value) < {$propertyData[static::JSON_FIELD_MIN_LENGTH]}", + $this->getTypeCheck() . "mb_strlen(\$value) < {$json[static::JSON_FIELD_MIN_LENGTH]}", MinLengthException::class, - [$property->getName(), $propertyData[static::JSON_FIELD_MIN_LENGTH]] + [$property->getName(), $json[static::JSON_FIELD_MIN_LENGTH]] ) ); } - if (isset($propertyData[static::JSON_FIELD_MAX_LENGTH])) { + if (isset($json[static::JSON_FIELD_MAX_LENGTH])) { $property->addValidator( new PropertyValidator( - $this->getTypeCheck() . "mb_strlen(\$value) > {$propertyData[static::JSON_FIELD_MAX_LENGTH]}", + $this->getTypeCheck() . "mb_strlen(\$value) > {$json[static::JSON_FIELD_MAX_LENGTH]}", MaxLengthException::class, - [$property->getName(), $propertyData[static::JSON_FIELD_MAX_LENGTH]] + [$property->getName(), $json[static::JSON_FIELD_MAX_LENGTH]] ) ); } @@ -95,14 +100,20 @@ protected function addLengthValidator(PropertyInterface $property, array $proper * TODO: implement format validations * * @param PropertyInterface $property - * @param array $propertyData + * @param JsonSchema $propertySchema * * @throws SchemaException */ - protected function addFormatValidator(PropertyInterface $property, array $propertyData): void + protected function addFormatValidator(PropertyInterface $property, JsonSchema $propertySchema): void { - if (isset($propertyData['format'])) { - throw new SchemaException('Format is currently not supported'); + if (isset($propertySchema->getJson()['format'])) { + throw new SchemaException( + sprintf( + 'Format is currently not supported for property %s in file %s', + $property->getName(), + $propertySchema->getFile() + ) + ); } } } diff --git a/src/PropertyProcessor/PropertyFactory.php b/src/PropertyProcessor/PropertyFactory.php index 3875368..adee28f 100644 --- a/src/PropertyProcessor/PropertyFactory.php +++ b/src/PropertyProcessor/PropertyFactory.php @@ -7,6 +7,7 @@ use PHPModelGenerator\Exception\SchemaException; use PHPModelGenerator\Model\Property\PropertyInterface; use PHPModelGenerator\Model\Schema; +use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; use PHPModelGenerator\SchemaProcessor\SchemaProcessor; /** @@ -36,7 +37,7 @@ public function __construct(ProcessorFactoryInterface $processorFactory) * @param SchemaProcessor $schemaProcessor * @param Schema $schema * @param string $propertyName - * @param array $propertyStructure + * @param JsonSchema $propertySchema * * @return PropertyInterface * @throws SchemaException @@ -46,26 +47,28 @@ public function create( SchemaProcessor $schemaProcessor, Schema $schema, string $propertyName, - array $propertyStructure + JsonSchema $propertySchema ): PropertyInterface { + $json = $propertySchema->getJson(); + // redirect properties with a constant value to the ConstProcessor - if (isset($propertyStructure['const'])) { - $propertyStructure['type'] = 'const'; + if (isset($json['const'])) { + $json['type'] = 'const'; } // redirect references to the ReferenceProcessor - if (isset($propertyStructure['$ref'])) { - $propertyStructure['type'] = isset($propertyStructure['type']) && $propertyStructure['type'] === 'base' + if (isset($json['$ref'])) { + $json['type'] = isset($json['type']) && $json['type'] === 'base' ? 'baseReference' : 'reference'; } return $this->processorFactory ->getProcessor( - $propertyStructure['type'] ?? 'any', + $json['type'] ?? 'any', $propertyMetaDataCollection, $schemaProcessor, $schema ) - ->process($propertyName, $propertyStructure); + ->process($propertyName, $propertySchema); } } \ No newline at end of file diff --git a/src/PropertyProcessor/PropertyProcessorFactory.php b/src/PropertyProcessor/PropertyProcessorFactory.php index bed2b12..05599dc 100644 --- a/src/PropertyProcessor/PropertyProcessorFactory.php +++ b/src/PropertyProcessor/PropertyProcessorFactory.php @@ -42,7 +42,13 @@ public function getProcessor( return new MultiTypeProcessor($this, $type, $propertyMetaDataCollection, $schemaProcessor, $schema); } - throw new SchemaException('Invalid property type'); + throw new SchemaException( + sprintf( + 'Invalid property type %s in file %s', + $type, + $schema->getJsonSchema()->getFile() + ) + ); } /** @@ -62,7 +68,13 @@ protected function getSingleTypePropertyProcessor( ): PropertyProcessorInterface { $processor = '\\PHPModelGenerator\\PropertyProcessor\\Property\\' . ucfirst(strtolower($type)) . 'Processor'; if (!class_exists($processor)) { - throw new SchemaException("Unsupported property type $type"); + throw new SchemaException( + sprintf( + 'Unsupported property type %s in file %s', + $type, + $schema->getJsonSchema()->getFile() + ) + ); } return new $processor($propertyMetaDataCollection, $schemaProcessor, $schema); diff --git a/src/PropertyProcessor/PropertyProcessorInterface.php b/src/PropertyProcessor/PropertyProcessorInterface.php index f00c71a..4d74410 100644 --- a/src/PropertyProcessor/PropertyProcessorInterface.php +++ b/src/PropertyProcessor/PropertyProcessorInterface.php @@ -5,6 +5,7 @@ namespace PHPModelGenerator\PropertyProcessor; use PHPModelGenerator\Model\Property\PropertyInterface; +use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; /** * Class PropertyProcessorInterface @@ -17,9 +18,9 @@ interface PropertyProcessorInterface * Process a property * * @param string $propertyName The name of the property - * @param array $propertyData An array containing the data of the property + * @param JsonSchema $propertySchema The schema of the property * * @return PropertyInterface */ - public function process(string $propertyName, array $propertyData): PropertyInterface; + public function process(string $propertyName, JsonSchema $propertySchema): PropertyInterface; } diff --git a/src/SchemaProcessor/PostProcessor/PopulatePostProcessor.php b/src/SchemaProcessor/PostProcessor/PopulatePostProcessor.php new file mode 100644 index 0000000..a3e5e08 --- /dev/null +++ b/src/SchemaProcessor/PostProcessor/PopulatePostProcessor.php @@ -0,0 +1,21 @@ +addMethod('populate', new RenderedMethod($schema, $generatorConfiguration, 'Populate.phptpl')); + } +} diff --git a/src/SchemaProcessor/PostProcessor/PostProcessorInterface.php b/src/SchemaProcessor/PostProcessor/PostProcessorInterface.php new file mode 100644 index 0000000..802f04a --- /dev/null +++ b/src/SchemaProcessor/PostProcessor/PostProcessorInterface.php @@ -0,0 +1,19 @@ +schema = $schema; + $this->generatorConfiguration = $generatorConfiguration; + $this->template = $template; + $this->templateValues = $templateValues; + } + + /** + * @inheritDoc + * + * @throws FileSystemException + * @throws SyntaxErrorException + * @throws UndefinedSymbolException + */ + public function getCode(): string + { + return $this->getRenderer()->renderTemplate( + $this->template, + array_merge( + [ + 'schema' => $this->schema, + 'viewHelper' => new RenderHelper($this->generatorConfiguration), + 'generatorConfiguration' => $this->generatorConfiguration, + ], + $this->templateValues + ) + ); + } + + /** + * @return Render + */ + protected function getRenderer(): Render + { + if (!self::$renderer) { + self::$renderer = new Render(__DIR__ . DIRECTORY_SEPARATOR . 'Templates' . DIRECTORY_SEPARATOR); + } + + return self::$renderer; + } +} \ No newline at end of file diff --git a/src/SchemaProcessor/PostProcessor/SerializationPostProcessor.php b/src/SchemaProcessor/PostProcessor/SerializationPostProcessor.php new file mode 100644 index 0000000..89b2d88 --- /dev/null +++ b/src/SchemaProcessor/PostProcessor/SerializationPostProcessor.php @@ -0,0 +1,77 @@ +addTrait(SerializableTrait::class) + ->addInterface(JsonSerializable::class) + ->addInterface(SerializationInterface::class); + + $this->addSerializeFunctionsForTransformingFilters($schema, $generatorConfiguration); + } + + /** + * Each transforming filter must provide a method to serialize the value. Add a method to the schema to call the + * serialization for each property with a transforming filter + * + * @param Schema $schema + * @param GeneratorConfiguration $generatorConfiguration + */ + private function addSerializeFunctionsForTransformingFilters( + Schema $schema, + GeneratorConfiguration $generatorConfiguration + ): void { + foreach ($schema->getProperties() as $property) { + foreach ($property->getValidators() as $validator) { + $validator = $validator->getValidator(); + + if ($validator instanceof FilterValidator && + $validator->getFilter() instanceof TransformingFilterInterface + ) { + [$serializerClass, $serializerMethod] = $validator->getFilter()->getSerializer(); + + $schema->addMethod( + "serialize{$property->getAttribute()}", + new RenderedMethod( + $schema, + $generatorConfiguration, + 'TransformingFilterSerializer.phptpl', + [ + + 'property' => $property->getAttribute(), + 'serializerClass' => $serializerClass, + 'serializerMethod' => $serializerMethod, + 'serializerOptions' => var_export($validator->getFilterOptions(), true), + ] + ) + ); + } + } + } + } +} diff --git a/src/SchemaProcessor/PostProcessor/Templates/Populate.phptpl b/src/SchemaProcessor/PostProcessor/Templates/Populate.phptpl new file mode 100644 index 0000000..5d38bf8 --- /dev/null +++ b/src/SchemaProcessor/PostProcessor/Templates/Populate.phptpl @@ -0,0 +1,42 @@ +/** + * Update the model with the provided data. If the update fails due to any violations an exception will be thrown and + * no properties of the model will be updated. + * + * @param array $modelData May contain any subset of the models properties + * + * @return self + * + * @throws \Exception + */ +public function populate(array $modelData): self +{ + {% if generatorConfiguration.collectErrors() %} + $this->errorRegistry = new {{ viewHelper.getSimpleClassName(generatorConfiguration.getErrorRegistryClass()) }}(); + {% endif%} + $rollbackValues = []; + + {% if schema.getBaseValidators() %} + $this->executeBaseValidators($modelData); + {% endif %} + + {% foreach schema.getProperties() as property %} + if (array_key_exists('{{ property.getName() }}', $modelData)) { + $rollbackValues['{{ property.getAttribute() }}'] = $this->{{ property.getAttribute() }}; + $this->process{{ viewHelper.ucfirst(property.getAttribute()) }}($modelData); + } + {% endforeach %} + + {% if generatorConfiguration.collectErrors() %} + if (count($this->errorRegistry->getErrors())) { + foreach ($rollbackValues as $property => $value) { + $this->{$property} = $value; + } + + throw $this->errorRegistry; + } + {% endif %} + + $this->rawModelDataInput = array_merge($this->rawModelDataInput, $modelData); + + return $this; +} diff --git a/src/Templates/Serializer/TransformingFilterSerializer.phptpl b/src/SchemaProcessor/PostProcessor/Templates/TransformingFilterSerializer.phptpl similarity index 100% rename from src/Templates/Serializer/TransformingFilterSerializer.phptpl rename to src/SchemaProcessor/PostProcessor/Templates/TransformingFilterSerializer.phptpl diff --git a/src/SchemaProcessor/RenderQueue.php b/src/SchemaProcessor/RenderQueue.php index 82f6400..d58c70b 100644 --- a/src/SchemaProcessor/RenderQueue.php +++ b/src/SchemaProcessor/RenderQueue.php @@ -8,6 +8,7 @@ use PHPModelGenerator\Exception\RenderException; use PHPModelGenerator\Model\GeneratorConfiguration; use PHPModelGenerator\Model\RenderJob; +use PHPModelGenerator\SchemaProcessor\PostProcessor\PostProcessorInterface; /** * Class RenderQueue @@ -34,15 +35,20 @@ public function addRenderJob(RenderJob $renderJob): self /** * Render all collected jobs of the RenderProxy and clear the queue * - * @param string $destination - * @param GeneratorConfiguration $generatorConfiguration + * @param string $destination + * @param GeneratorConfiguration $generatorConfiguration + * @param PostProcessorInterface[] $postProcessors * * @throws FileSystemException * @throws RenderException */ - public function execute(string $destination, GeneratorConfiguration $generatorConfiguration): void - { + public function execute( + string $destination, + GeneratorConfiguration $generatorConfiguration, + array $postProcessors + ): void { foreach ($this->jobs as $job) { + $job->postProcess($postProcessors, $generatorConfiguration); $job->render($destination, $generatorConfiguration); } diff --git a/src/SchemaProcessor/SchemaProcessor.php b/src/SchemaProcessor/SchemaProcessor.php index 1048206..e355860 100644 --- a/src/SchemaProcessor/SchemaProcessor.php +++ b/src/SchemaProcessor/SchemaProcessor.php @@ -8,6 +8,7 @@ use PHPModelGenerator\Model\GeneratorConfiguration; use PHPModelGenerator\Model\RenderJob; use PHPModelGenerator\Model\Schema; +use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; use PHPModelGenerator\Model\SchemaDefinition\SchemaDefinitionDictionary; use PHPModelGenerator\PropertyProcessor\PropertyMetaDataCollection; use PHPModelGenerator\PropertyProcessor\PropertyFactory; @@ -62,16 +63,15 @@ public function __construct( /** * Process a given json schema file * - * @param array $jsonSchema - * @param string $sourcePath + * @param JsonSchema $jsonSchema * * @throws SchemaException */ - public function process(array $jsonSchema, string $sourcePath): void + public function process(JsonSchema $jsonSchema): void { - $this->setCurrentClassPath($sourcePath); + $this->setCurrentClassPath($jsonSchema->getFile()); $this->currentClassName = $this->generatorConfiguration->getClassNameGenerator()->getClassName( - str_ireplace('.json', '', basename($sourcePath)), + str_ireplace('.json', '', basename($jsonSchema->getFile())), $jsonSchema, false ); @@ -80,7 +80,7 @@ public function process(array $jsonSchema, string $sourcePath): void $jsonSchema, $this->currentClassPath, $this->currentClassName, - new SchemaDefinitionDictionary(dirname($sourcePath)), + new SchemaDefinitionDictionary(dirname($jsonSchema->getFile())), true ); } @@ -88,7 +88,7 @@ public function process(array $jsonSchema, string $sourcePath): void /** * Process a JSON schema stored as an associative array * - * @param array $jsonSchema + * @param JsonSchema $jsonSchema * @param string $classPath * @param string $className * @param SchemaDefinitionDictionary $dictionary If a nested object of a schema is processed import the @@ -101,14 +101,14 @@ public function process(array $jsonSchema, string $sourcePath): void * @throws SchemaException */ public function processSchema( - array $jsonSchema, + JsonSchema $jsonSchema, string $classPath, string $className, SchemaDefinitionDictionary $dictionary, bool $initialClass = false ): ?Schema { - if ((!isset($jsonSchema['type']) || $jsonSchema['type'] !== 'object') && - !array_intersect(array_keys($jsonSchema), ['anyOf', 'allOf', 'oneOf', 'if', '$ref']) + if ((!isset($jsonSchema->getJson()['type']) || $jsonSchema->getJson()['type'] !== 'object') && + !array_intersect(array_keys($jsonSchema->getJson()), ['anyOf', 'allOf', 'oneOf', 'if', '$ref']) ) { // skip the JSON schema as neither an object, a reference nor a composition is defined on the root level return null; @@ -122,7 +122,7 @@ public function processSchema( * * @param string $classPath * @param string $className - * @param array $structure + * @param JsonSchema $jsonSchema * @param SchemaDefinitionDictionary $dictionary * @param bool $initialClass * @@ -133,11 +133,11 @@ public function processSchema( protected function generateModel( string $classPath, string $className, - array $structure, + JsonSchema $jsonSchema, SchemaDefinitionDictionary $dictionary, bool $initialClass ): Schema { - $schemaSignature = md5(json_encode($structure)); + $schemaSignature = md5(json_encode($jsonSchema->getJson())); if (!$initialClass && isset($this->processedSchema[$schemaSignature])) { if ($this->generatorConfiguration->isOutputEnabled()) { @@ -148,16 +148,18 @@ protected function generateModel( return $this->processedSchema[$schemaSignature]; } - $schema = new Schema($classPath, $className, $dictionary); + $schema = new Schema($classPath, $className, $jsonSchema, $dictionary); + $this->processedSchema[$schemaSignature] = $schema; - $structure['type'] = 'base'; + $json = $jsonSchema->getJson(); + $json['type'] = 'base'; (new PropertyFactory(new PropertyProcessorFactory()))->create( - new PropertyMetaDataCollection($structure['required'] ?? []), + new PropertyMetaDataCollection($jsonSchema->getJson()['required'] ?? []), $this, $schema, $className, - $structure + $jsonSchema->withJson($json) ); $this->generateClassFile($classPath, $className, $schema, $initialClass); diff --git a/src/SchemaProvider/OpenAPIv3Provider.php b/src/SchemaProvider/OpenAPIv3Provider.php index 7e52c32..dc7e36f 100644 --- a/src/SchemaProvider/OpenAPIv3Provider.php +++ b/src/SchemaProvider/OpenAPIv3Provider.php @@ -5,6 +5,7 @@ namespace PHPModelGenerator\SchemaProvider; use PHPModelGenerator\Exception\SchemaException; +use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; /** * Class OpenAPIv3Provider @@ -52,7 +53,7 @@ public function getSchemas(): iterable $schema['$id'] = $schemaKey; } - yield [$this->sourceFile, $schema]; + yield new JsonSchema($this->sourceFile, $schema); } } diff --git a/src/SchemaProvider/RecursiveDirectoryProvider.php b/src/SchemaProvider/RecursiveDirectoryProvider.php index 4a1ddb9..9dd84cd 100644 --- a/src/SchemaProvider/RecursiveDirectoryProvider.php +++ b/src/SchemaProvider/RecursiveDirectoryProvider.php @@ -6,6 +6,7 @@ use PHPModelGenerator\Exception\FileSystemException; use PHPModelGenerator\Exception\SchemaException; +use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use RecursiveRegexIterator; @@ -54,7 +55,7 @@ public function getSchemas(): iterable throw new SchemaException("Invalid JSON-Schema file {$file[0]}"); } - yield [$file[0], $decodedJsonSchema]; + yield new JsonSchema($file[0], $decodedJsonSchema); } } diff --git a/src/SchemaProvider/SchemaProviderInterface.php b/src/SchemaProvider/SchemaProviderInterface.php index aeb648f..01b04f0 100644 --- a/src/SchemaProvider/SchemaProviderInterface.php +++ b/src/SchemaProvider/SchemaProviderInterface.php @@ -4,6 +4,8 @@ namespace PHPModelGenerator\SchemaProvider; +use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; + /** * Interface SchemaProviderInterface * @@ -13,11 +15,9 @@ interface SchemaProviderInterface { /** * Provide an iterable containing all schemas which should be processed. - * Each entry must be represented by a data tuple [string $sourceFile, array $jsonSchema] where $sourceFile contains - * the full file path of the file which contains the JSON schema (used for namespacing) and $jsonSchema must contain - * the decoded schema. + * Each entry must be a JsonSchema object containing the decoded schema and meta information about the schema. * - * @return iterable + * @return JsonSchema[] */ public function getSchemas(): iterable; diff --git a/src/Templates/Model.phptpl b/src/Templates/Model.phptpl index 285fb1c..78cf514 100644 --- a/src/Templates/Model.phptpl +++ b/src/Templates/Model.phptpl @@ -16,17 +16,16 @@ declare(strict_types = 1); * Class {{ class }} {% if namespace %} * @package {{ namespace }} {% endif %} */ -class {{ class }} implements \PHPModelGenerator\Interfaces\JSONModelInterface -{% if generatorConfiguration.hasSerializationEnabled() %}, \PHPModelGenerator\Interfaces\SerializationInterface, \JsonSerializable{% endif %} +class {{ class }} {% if schema.getInterfaces() %}implements {{ viewHelper.joinClassNames(schema.getInterfaces()) }}{% endif %} { - {% if generatorConfiguration.hasSerializationEnabled() %}use \PHPModelGenerator\Traits\SerializableTrait;{% endif %} + {% if schema.getTraits() %}use {{ viewHelper.joinClassNames(schema.getTraits()) }};{% endif %} - {% foreach properties as property %} + {% foreach schema.getProperties() as property %} /**{% if viewHelper.getTypeHintAnnotation(property, true) %} @var {{ viewHelper.getTypeHintAnnotation(property, true) }}{% endif %}{% if property.getDescription() %} {{ property.getDescription() }}{% endif %} */ protected ${{ property.getAttribute() }}{% if not viewHelper.isNull(property.getDefaultValue()) %} = {{ property.getDefaultValue() }}{% endif %}; {% endforeach %} /** @var array */ - private $rawModelDataInput; + private $rawModelDataInput = []; {% if generatorConfiguration.collectErrors() %} /** @var {{ viewHelper.getSimpleClassName(generatorConfiguration.getErrorRegistryClass()) }} Collect all validation errors */ @@ -57,23 +56,14 @@ class {{ class }} implements \PHPModelGenerator\Interfaces\JSONModelInterface {% endif %} {% endif%} - {% if baseValidators %} - $value = $modelData; + {% if schema.getBaseValidators() %} + $this->executeBaseValidators($modelData); {% endif %} - {% foreach baseValidators as validator %} - {{ validator.getValidatorSetUp() }} - if ({{ validator.getCheck() }}) { - {{ viewHelper.validationError(validator) }} - } - {% endforeach %} - - {% foreach properties as property %} + {% foreach schema.getProperties() as property %} $this->process{{ viewHelper.ucfirst(property.getAttribute()) }}($modelData); {% endforeach %} - $this->rawModelDataInput = $modelData; - {% if generatorConfiguration.collectErrors() %} {% if initialClass %} if (count($this->errorRegistry->getErrors())) { @@ -81,8 +71,24 @@ class {{ class }} implements \PHPModelGenerator\Interfaces\JSONModelInterface } {% endif%} {% endif%} + + $this->rawModelDataInput = $modelData; } + {% if schema.getBaseValidators() %} + private function executeBaseValidators(array &$modelData): void + { + $value = $modelData; + + {% foreach schema.getBaseValidators() as validator %} + {{ validator.getValidatorSetUp() }} + if ({{ validator.getCheck() }}) { + {{ viewHelper.validationError(validator) }} + } + {% endforeach %} + } + {% endif %} + /** * Get the raw input used to set up the model * @@ -93,7 +99,7 @@ class {{ class }} implements \PHPModelGenerator\Interfaces\JSONModelInterface return $this->rawModelDataInput; } - {% foreach properties as property %} + {% foreach schema.getProperties() as property %} /** * Get the value of {{ property.getName() }}. * @@ -180,9 +186,7 @@ class {{ class }} implements \PHPModelGenerator\Interfaces\JSONModelInterface } {% endforeach %} - {% if generatorConfiguration.hasSerializationEnabled() %} - {% foreach customSerializer as serializer %} - {{ serializer.getSerializer(generatorConfiguration) }} - {% endforeach %} - {% endif %} + {% foreach schema.getMethods() as method %} + {{ method.getCode(generatorConfiguration) }} + {% endforeach %} } diff --git a/src/Utils/ClassNameGenerator.php b/src/Utils/ClassNameGenerator.php index 4b5dc64..35ab6aa 100644 --- a/src/Utils/ClassNameGenerator.php +++ b/src/Utils/ClassNameGenerator.php @@ -4,6 +4,8 @@ namespace PHPModelGenerator\Utils; +use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; + /** * Class ClassNameGenerator * @@ -16,7 +18,7 @@ class ClassNameGenerator implements ClassNameGeneratorInterface */ public function getClassName( string $propertyName, - array $schema, + JsonSchema $schema, bool $isMergeClass, string $currentClassName = '' ): string { @@ -24,8 +26,8 @@ public function getClassName( $isMergeClass ? '%s_Merged_%s' : '%s_%s', $currentClassName, ucfirst( - isset($schema['$id']) - ? str_replace('#', '', $schema['$id']) + isset($schema->getJson()['$id']) + ? str_replace('#', '', $schema->getJson()['$id']) : ($propertyName . ($currentClassName ? uniqid() : '')) ) ); diff --git a/src/Utils/ClassNameGeneratorInterface.php b/src/Utils/ClassNameGeneratorInterface.php index e72b8ac..38ebc0a 100644 --- a/src/Utils/ClassNameGeneratorInterface.php +++ b/src/Utils/ClassNameGeneratorInterface.php @@ -4,6 +4,8 @@ namespace PHPModelGenerator\Utils; +use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; + /** * Interface ClassNameGeneratorInterface * @@ -14,17 +16,17 @@ interface ClassNameGeneratorInterface /** * Hook into the class name generation. Returns the name of a class. * - * @param string $propertyName If a json file is handled, contains the name of the file. - * Otherwise the name of the property which contains the nested object - * @param array $schema The structure of the schema which is represented by the generated class - * @param bool $isMergeClass Is it a merge class? example: allOf schema composition - * @param string $currentClassName The class name of the parent class if a class for a nested object is generated + * @param string $propertyName If a json file is handled, contains the name of the file. + * Otherwise the name of the property which contains the nested object + * @param JsonSchema $schema The structure of the schema which is represented by the generated class + * @param bool $isMergeClass Is it a merge class? example: allOf schema composition + * @param string $currentClassName The class name of the parent class if the class represents a nested object * * @return string */ public function getClassName( string $propertyName, - array $schema, + JsonSchema $schema, bool $isMergeClass, string $currentClassName = '' ): string; diff --git a/src/Utils/RenderHelper.php b/src/Utils/RenderHelper.php index 25d297f..8c53e26 100644 --- a/src/Utils/RenderHelper.php +++ b/src/Utils/RenderHelper.php @@ -60,6 +60,16 @@ public function getSimpleClassName(string $fqcn): string return end($parts); } + /** + * @param array $fqcns + * + * @return string + */ + public function joinClassNames(array $fqcns): string + { + return join(', ', array_map([$this, 'getSimpleClassName'], $fqcns)); + } + /** * Resolve all associated decorators of a property * diff --git a/tests/AbstractPHPModelGeneratorTest.php b/tests/AbstractPHPModelGeneratorTest.php index 618313c..48524c4 100644 --- a/tests/AbstractPHPModelGeneratorTest.php +++ b/tests/AbstractPHPModelGeneratorTest.php @@ -4,6 +4,7 @@ use Exception; use FilesystemIterator; +use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; use PHPModelGenerator\SchemaProvider\OpenAPIv3Provider; use PHPModelGenerator\SchemaProvider\RecursiveDirectoryProvider; use PHPModelGenerator\Utils\ClassNameGenerator; @@ -29,6 +30,7 @@ abstract class AbstractPHPModelGeneratorTest extends TestCase { protected const EXTERNAL_JSON_DIRECTORIES = []; + protected const POST_PROCESSORS = []; private $names = []; @@ -200,7 +202,7 @@ protected function generateClass( $generatorConfiguration->setClassNameGenerator(new class extends ClassNameGenerator { public function getClassName( string $propertyName, - array $schema, + JsonSchema $schema, bool $isMergeClass, string $currentClassName = '' ): string { @@ -238,7 +240,12 @@ public function getClassName( default: throw new Exception("Schema provider $schemaProviderClass not supported"); } - $generatedFiles = (new ModelGenerator($generatorConfiguration))->generateModels( + $generator = new ModelGenerator($generatorConfiguration); + foreach (static::POST_PROCESSORS as $postProcessor) { + $generator->addPostProcessor(new $postProcessor()); + } + + $generatedFiles = $generator->generateModels( $schemaProvider, $baseDir . DIRECTORY_SEPARATOR . 'Models' . DIRECTORY_SEPARATOR ); diff --git a/tests/ComposedValue/ComposedAllOfTest.php b/tests/ComposedValue/ComposedAllOfTest.php index f947fa9..2ca14f0 100644 --- a/tests/ComposedValue/ComposedAllOfTest.php +++ b/tests/ComposedValue/ComposedAllOfTest.php @@ -2,6 +2,7 @@ namespace PHPModelGenerator\Tests\ComposedValue; +use PHPModelGenerator\Exception\SchemaException; use PHPModelGenerator\Exception\ValidationException; use PHPModelGenerator\Tests\AbstractPHPModelGeneratorTest; use ReflectionMethod; @@ -447,6 +448,14 @@ public function nestedObjectDataProvider() ]; } + public function testNoNestedSchemaThrowsAnException(): void + { + $this->expectException(SchemaException::class); + $this->expectExceptionMessage('No nested schema for composed property'); + + $this->generateClassFromFile('NoNestedSchema.json'); + } + /* public function testObjectLevelCompositionConditional() { diff --git a/tests/ComposedValue/ComposedIfTest.php b/tests/ComposedValue/ComposedIfTest.php index cc9324f..917efa9 100644 --- a/tests/ComposedValue/ComposedIfTest.php +++ b/tests/ComposedValue/ComposedIfTest.php @@ -95,4 +95,12 @@ public function invalidConditionalObjectPropertyDataProvider(): array ] ); } + + public function testIncompleteCompositionThrowsAnException(): void + { + $this->expectException(SchemaException::class); + $this->expectExceptionMessage('Incomplete conditional composition for property'); + + $this->generateClassFromFile('IncompleteConditional.json'); + } } diff --git a/tests/Objects/ReferencePropertyTest.php b/tests/Objects/ReferencePropertyTest.php index ac77c73..309fa7b 100644 --- a/tests/Objects/ReferencePropertyTest.php +++ b/tests/Objects/ReferencePropertyTest.php @@ -31,7 +31,7 @@ class ReferencePropertyTest extends AbstractPHPModelGeneratorTest public function testNotResolvedReferenceThrowsAnException(string $reference): void { $this->expectException(SchemaException::class); - $this->expectExceptionMessage("Unresolved Reference: $reference"); + $this->expectExceptionMessage("Unresolved Reference $reference in file"); $this->generateClassFromFileTemplate('NotResolvedReference.json', [$reference]); } diff --git a/tests/PostProcessor/PopulatePostProcessorTest.php b/tests/PostProcessor/PopulatePostProcessorTest.php new file mode 100644 index 0000000..bed118f --- /dev/null +++ b/tests/PostProcessor/PopulatePostProcessorTest.php @@ -0,0 +1,126 @@ +generateClassFromFile( + 'BasicSchema.json', + (new GeneratorConfiguration())->setSerialization(true) + ); + $object = new $className(['name' => 'Albert']); + + $this->assertTrue(is_callable([$object, 'populate'])); + + // test an empty populate call doesn't change the internal behaviour + $object->populate([]); + $this->assertSame(['name' => 'Albert'], $object->getRawModelDataInput()); + $this->assertSame(['name' => 'Albert', 'age' => null], $object->toArray()); + + // test adding an additional property to the model + $object->populate(['birthdate' => '10.10.1990']); + $this->assertSame(['name' => 'Albert', 'birthdate' => '10.10.1990'], $object->getRawModelDataInput()); + $this->assertSame(['name' => 'Albert', 'age' => null], $object->toArray()); + + // test overwriting a single property + $object->populate(['age' => 30]); + $this->assertSame( + ['name' => 'Albert', 'birthdate' => '10.10.1990', 'age' => 30], + $object->getRawModelDataInput() + ); + $this->assertSame(['name' => 'Albert', 'age' => 30], $object->toArray()); + + // test overwriting multiple properties + $object->populate(['age' => 26, 'name' => 'Harry']); + $this->assertSame( + ['name' => 'Harry', 'birthdate' => '10.10.1990', 'age' => 26], + $object->getRawModelDataInput() + ); + $this->assertSame(['name' => 'Harry', 'age' => 26], $object->toArray()); + } + + /** + * @dataProvider invalidPopulateDataProvider + */ + public function testInvalidPopulateThrowsAnException( + array $data, + bool $collectErrors, + string $expectedException, + string $expectedMessage + ): void { + $this->expectException($expectedException); + $this->expectExceptionMessage($expectedMessage); + + $className = $this->generateClassFromFile( + 'BasicSchema.json', + (new GeneratorConfiguration())->setCollectErrors($collectErrors) + ); + $object = new $className(['name' => 'Albert', 'age' => 30]); + + try { + $object->populate($data); + $this->fail('No exception thrown'); + } catch (Exception $exception) { + // test if the internal state hasn't been changed + $this->assertSame(['name' => 'Albert', 'age' => 30], $object->getRawModelDataInput()); + + throw $exception; + } + } + + public function invalidPopulateDataProvider(): array + { + return [ + 'No error collection - multiple violations' => [ + ['name' => 'Anne-Marie', 'age' => false], + false, + PatternException::class, + "Value for name doesn't match pattern ^[a-zA-Z]*$" + ], + 'Error collection - multiple violations' => [ + ['name' => 'Anne-Marie', 'age' => false], + true, + ErrorRegistryException::class, + "Value for name doesn't match pattern ^[a-zA-Z]*$ +Value for name must not be longer than 8 +Invalid type for age. Requires int, got boolean" + ], + 'Invalid additional property' => [ + ['numeric' => 9], + false, + InvalidAdditionalPropertiesException::class, + "contains invalid additional properties. + - invalid additional property 'numeric' + * Invalid type for additional property. Requires string, got integer" + ], + 'Invalid additional property name' => [ + ['invalid name' => 'Hi'], + false, + InvalidPropertyNamesException::class, + "contains properties with invalid names. + - invalid property 'invalid name' + * Value for property name doesn't match pattern ^[a-zA-Z]*$" + ], + 'Too many properties' => [ + ['additional' => 'Hi', 'another' => 'Ho'], + false, + MaxPropertiesException::class, + "must not contain more than 3 properties" + ], + ]; + } +} diff --git a/tests/PropertyProcessor/PropertyProcessorFactoryTest.php b/tests/PropertyProcessor/PropertyProcessorFactoryTest.php index 7c72100..c3a9e52 100644 --- a/tests/PropertyProcessor/PropertyProcessorFactoryTest.php +++ b/tests/PropertyProcessor/PropertyProcessorFactoryTest.php @@ -5,6 +5,7 @@ use PHPModelGenerator\Exception\SchemaException; use PHPModelGenerator\Model\GeneratorConfiguration; use PHPModelGenerator\Model\Schema; +use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; use PHPModelGenerator\PropertyProcessor\Property\ArrayProcessor; use PHPModelGenerator\PropertyProcessor\Property\BooleanProcessor; use PHPModelGenerator\PropertyProcessor\Property\IntegerProcessor; @@ -40,7 +41,7 @@ public function testGetPropertyProcessor(string $type, string $expectedClass): v $type, new PropertyMetaDataCollection(), new SchemaProcessor('', '', new GeneratorConfiguration(), new RenderQueue()), - new Schema('', '') + new Schema('', '', new JsonSchema('', [])) ); $this->assertInstanceOf($expectedClass, $propertyProcessor); @@ -78,7 +79,7 @@ public function testGetInvalidPropertyProcessorThrowsAnException() 'Hello', new PropertyMetaDataCollection(), new SchemaProcessor('', '', new GeneratorConfiguration(), new RenderQueue()), - new Schema('', '') + new Schema('', '', new JsonSchema('', [])) ); } } diff --git a/tests/Schema/ComposedAllOfTest/NoNestedSchema.json b/tests/Schema/ComposedAllOfTest/NoNestedSchema.json new file mode 100644 index 0000000..4826413 --- /dev/null +++ b/tests/Schema/ComposedAllOfTest/NoNestedSchema.json @@ -0,0 +1,9 @@ +{ + "type": "object", + "allOf": [ + { + "type": "integer", + "minimum": 10 + } + ] +} \ No newline at end of file diff --git a/tests/Schema/ComposedIfTest/IncompleteConditional.json b/tests/Schema/ComposedIfTest/IncompleteConditional.json new file mode 100644 index 0000000..03f1b81 --- /dev/null +++ b/tests/Schema/ComposedIfTest/IncompleteConditional.json @@ -0,0 +1,18 @@ +{ + "type": "object", + "properties": { + "country": { + "enum": [ + "USA", + "Canada" + ] + } + }, + "if": { + "properties": { + "country": { + "const": "USA" + } + } + } +} diff --git a/tests/Schema/PopulatePostProcessorTest/BasicSchema.json b/tests/Schema/PopulatePostProcessorTest/BasicSchema.json new file mode 100644 index 0000000..18dcf8e --- /dev/null +++ b/tests/Schema/PopulatePostProcessorTest/BasicSchema.json @@ -0,0 +1,24 @@ +{ + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 8, + "pattern": "^[a-zA-Z]*$" + }, + "age": { + "type": "integer", + "minimum": 0 + } + }, + "additionalProperties": { + "type": "string" + }, + "propertyNames": { + "pattern": "^[a-zA-Z]*$" + }, + "maxProperties": 3, + "required": [ + "name" + ] +} \ No newline at end of file