diff --git a/src/Model/Validator/ExtractedMethodValidator.php b/src/Model/Validator/ExtractedMethodValidator.php index c1b8cc56..6a63b616 100644 --- a/src/Model/Validator/ExtractedMethodValidator.php +++ b/src/Model/Validator/ExtractedMethodValidator.php @@ -59,7 +59,7 @@ public function __construct( public function getCode(): string { $renderHelper = new RenderHelper($this->generatorConfiguration); - return "private function {$this->validator->getExtractedMethodName()}(&\$value): void { + return "private function {$this->validator->getExtractedMethodName()}(&\$value, \$modelData): void { {$this->validator->getValidatorSetUp()} if ({$this->validator->getCheck()}) { diff --git a/src/PropertyProcessor/Property/ConstProcessor.php b/src/PropertyProcessor/Property/ConstProcessor.php index 9ca86d8e..d5ad8097 100644 --- a/src/PropertyProcessor/Property/ConstProcessor.php +++ b/src/PropertyProcessor/Property/ConstProcessor.php @@ -10,7 +10,7 @@ use PHPModelGenerator\Model\Property\PropertyType; use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; use PHPModelGenerator\Model\Validator\PropertyValidator; -use PHPModelGenerator\PropertyProcessor\PropertyProcessorInterface; +use PHPModelGenerator\Utils\RenderHelper; use PHPModelGenerator\Utils\TypeConverter; /** @@ -18,7 +18,7 @@ * * @package PHPModelGenerator\PropertyProcessor\Property */ -class ConstProcessor implements PropertyProcessorInterface +class ConstProcessor extends AbstractPropertyProcessor { /** * @inheritdoc @@ -34,13 +34,26 @@ public function process(string $propertyName, JsonSchema $propertySchema): Prope $json['description'] ?? '', ); - return $property - ->setRequired(true) - ->addValidator(new PropertyValidator( - $property, - '$value !== ' . var_export($json['const'], true), - InvalidConstException::class, - [$json['const']], - )); + $property->setRequired($this->propertyMetaDataCollection->isAttributeRequired($propertyName)); + + $check = match(true) { + $property->isRequired() + => '$value !== ' . var_export($json['const'], true), + $this->isImplicitNullAllowed($property) + => '!in_array($value, ' . RenderHelper::varExportArray([$json['const'], null]) . ', true)', + default + => "array_key_exists('{$property->getName()}', \$modelData) && \$value !== " . var_export($json['const'], true), + }; + + $property->addValidator(new PropertyValidator( + $property, + $check, + InvalidConstException::class, + [$json['const']], + )); + + $this->generateValidators($property, $propertySchema); + + return $property; } } diff --git a/src/Templates/Validator/AdditionalProperties.phptpl b/src/Templates/Validator/AdditionalProperties.phptpl index 87557485..96c93e64 100644 --- a/src/Templates/Validator/AdditionalProperties.phptpl +++ b/src/Templates/Validator/AdditionalProperties.phptpl @@ -1,4 +1,4 @@ -(function () use ($properties, &$invalidProperties) { +(function () use ($properties, &$invalidProperties, $modelData) { {% if generatorConfiguration.collectErrors() %} $originalErrorRegistry = $this->_errorRegistry; {% endif %} diff --git a/src/Templates/Validator/ArrayItem.phptpl b/src/Templates/Validator/ArrayItem.phptpl index 0c14f334..cdbea591 100644 --- a/src/Templates/Validator/ArrayItem.phptpl +++ b/src/Templates/Validator/ArrayItem.phptpl @@ -1,4 +1,4 @@ -is_array($value) && (function (&$items) use (&$invalidItems{{ suffix }}) { +is_array($value) && (function (&$items) use (&$invalidItems{{ suffix }}, $modelData) { {% if generatorConfiguration.collectErrors() %} $originalErrorRegistry = $this->_errorRegistry; {% endif %} diff --git a/src/Templates/Validator/ArrayTuple.phptpl b/src/Templates/Validator/ArrayTuple.phptpl index 425e9d11..d6ce49ea 100644 --- a/src/Templates/Validator/ArrayTuple.phptpl +++ b/src/Templates/Validator/ArrayTuple.phptpl @@ -1,4 +1,4 @@ -is_array($value) && (function (&$items) use (&$invalidTuples) { +is_array($value) && (function (&$items) use (&$invalidTuples, $modelData) { {% if generatorConfiguration.collectErrors() %} $originalErrorRegistry = $this->_errorRegistry; {% endif %} diff --git a/src/Templates/Validator/PatternProperties.phptpl b/src/Templates/Validator/PatternProperties.phptpl index b69d4c13..42bb0094 100644 --- a/src/Templates/Validator/PatternProperties.phptpl +++ b/src/Templates/Validator/PatternProperties.phptpl @@ -1,4 +1,4 @@ -(function () use ($properties, &$invalidProperties) { +(function () use ($properties, &$invalidProperties, $modelData) { {% if generatorConfiguration.collectErrors() %} $originalErrorRegistry = $this->_errorRegistry; {% endif %} diff --git a/src/Utils/RenderHelper.php b/src/Utils/RenderHelper.php index 4d249350..c69daaad 100644 --- a/src/Utils/RenderHelper.php +++ b/src/Utils/RenderHelper.php @@ -182,7 +182,7 @@ public function renderValidator(PropertyValidatorInterface $validator, Schema $s $schema->addMethod($validator->getExtractedMethodName(), $validator->getMethod()); } - return "\$this->{$validator->getExtractedMethodName()}(\$value);"; + return "\$this->{$validator->getExtractedMethodName()}(\$value, \$modelData);"; } public function renderMethods(Schema $schema): string diff --git a/tests/Objects/ConstPropertyTest.php b/tests/Objects/ConstPropertyTest.php index 3f87ec06..e4a225ae 100644 --- a/tests/Objects/ConstPropertyTest.php +++ b/tests/Objects/ConstPropertyTest.php @@ -4,10 +4,15 @@ namespace PHPModelGenerator\Tests\Objects; +use PHPModelGenerator\Exception\Arrays\InvalidTupleException; +use PHPModelGenerator\Exception\ComposedValue\OneOfException; +use PHPModelGenerator\Exception\ErrorRegistryException; use PHPModelGenerator\Exception\FileSystemException; +use PHPModelGenerator\Exception\Object\RequiredValueException; use PHPModelGenerator\Exception\ValidationException; use PHPModelGenerator\Exception\RenderException; use PHPModelGenerator\Exception\SchemaException; +use PHPModelGenerator\Model\GeneratorConfiguration; use PHPModelGenerator\Tests\AbstractPHPModelGeneratorTestCase; use stdClass; @@ -38,14 +43,38 @@ public function testProvidedConstPropertyIsValid(): void * @throws RenderException * @throws SchemaException */ - public function testNotProvidedConstPropertyThrowsAnException(): void + public function testProvidedArrayItemConstPropertyIsValid(): void { - $this->expectException(ValidationException::class); - $this->expectExceptionMessage('Invalid value for stringProperty declined by const constraint'); + $className = $this->generateClassFromFile('ArrayItemConstProperty.json'); - $className = $this->generateClassFromFile('ConstProperty.json'); + $object = new $className(['property' => ['red', 'red']]); - new $className([]); + $this->assertIsArray($object->getProperty()); + $this->assertSame(['red', 'red'], $object->getProperty()); + } + + /** + * @dataProvider stringIntDataProvider + * + * @throws FileSystemException + * @throws RenderException + * @throws SchemaException + */ + public function testProvidedAnyOfConstPropertyIsValid(string|int $propertyValue): void + { + $className = $this->generateClassFromFile('AnyOfConstProperty.json'); + + $object = new $className(['property' => $propertyValue]); + + $this->assertSame($propertyValue, $object->getProperty()); + } + + public function stringIntDataProvider(): array + { + return [ + ['red'], + [1], + ]; } /** @@ -62,7 +91,7 @@ public function testNotMatchingProvidedDataThrowsAnException($propertyValue): vo $this->expectException(ValidationException::class); $this->expectExceptionMessage('Invalid value for stringProperty declined by const constraint'); - $className = $this->generateClassFromFile('ConstProperty.json'); + $className = $this->generateClassFromFile('ConstProperty.json', null, false, false); new $className(['stringProperty' => $propertyValue]); } @@ -79,4 +108,170 @@ public function invalidPropertyDataProvider(): array 'null' => [null], ]; } + + /** + * @throws FileSystemException + * @throws RenderException + * @throws SchemaException + */ + public function testNotMatchingArrayItemConstPropertyThrowsAnException(): void + { + $this->expectException(InvalidTupleException::class); + $this->expectExceptionMessage('Invalid tuple item in array property'); + + $className = $this->generateClassFromFile('ArrayItemConstProperty.json'); + + new $className(['property' => ['green']]); + } + + /** + * @throws FileSystemException + * @throws RenderException + * @throws SchemaException + */ + public function testNotMatchingArrayItemConstPropertyThrowsAnException1(): void + { + $this->expectException(OneOfException::class); + $this->expectExceptionMessage('Invalid value for property declined by composition constraint'); + + $className = $this->generateClassFromFile('AnyOfConstProperty.json'); + + new $className(['property' => 'green']); + } + + /** + * @throws FileSystemException + * @throws RenderException + * @throws SchemaException + */ + public function testProvidedConstOnlyRequiredPropertyIsValid(): void + { + $className = $this->generateClassFromFile('RequiredAndOptionalConstProperties.json'); + + $object = new $className(['requiredProperty' => 'red']); + + $this->assertSame('red', $object->getRequiredProperty()); + $this->assertNull($object->getOptionalProperty()); + } + + /** + * @throws FileSystemException + * @throws RenderException + * @throws SchemaException + */ + public function testProvidedNullOptionalPropertyConstPropertyIsValid(): void + { + $className = $this->generateClassFromFile('RequiredAndOptionalConstProperties.json'); + + $object = new $className(['requiredProperty' => 'red', 'optionalProperty' => null]); + + $this->assertSame('red', $object->getRequiredProperty()); + $this->assertNull($object->getOptionalProperty()); + } + + /** + * @dataProvider requiredAndOptionalPropertiesDataProvider + * + * @throws FileSystemException + * @throws RenderException + * @throws SchemaException + */ + public function testProvidedConstPropertiesIsValidWithDifferentImplicitNull( + bool $implicitNull, + string $reqPropertyValue, + string $optPropertyValue + ): void + { + $className = $this->generateClassFromFile( + 'RequiredAndOptionalConstProperties.json', + new GeneratorConfiguration(), + false, + $implicitNull, + ); + + $object = new $className(['requiredProperty' => $reqPropertyValue, 'optionalProperty' => $optPropertyValue]); + + $this->assertSame($reqPropertyValue, $object->getRequiredProperty()); + $this->assertSame($optPropertyValue, $object->getOptionalProperty()); + } + + public function requiredAndOptionalPropertiesDataProvider(): array + { + return $this->combineDataProvider( + $this->implicitNullDataProvider(), + [ + ['red', 'green'], + ], + ); + } + + /** + * @throws FileSystemException + * @throws RenderException + * @throws SchemaException + */ + public function testNotProvidedRequiredPropertyThrowsAnException(): void + { + $this->expectException(RequiredValueException::class); + $this->expectExceptionMessage('Missing required value for requiredProperty'); + + $className = $this->generateClassFromFile('RequiredAndOptionalConstProperties.json'); + + new $className([]); + } + + /** + * @dataProvider invalidRequiredAndOptionalConstPropertiesDataProvider + * + * @throws FileSystemException + * @throws RenderException + * @throws SchemaException + */ + public function testNotMatchingRequiredAndOptionalProvidedDataThrowsAnException( + bool $implicitNull, + string $reqPropertyValue, + ?string $optPropertyValue, + string $exceptionMessage + ): void + { + $className = $this->generateClassFromFile( + 'RequiredAndOptionalConstProperties.json', + new GeneratorConfiguration(), + false, + $implicitNull, + ); + + $this->expectException(ErrorRegistryException::class); + $this->expectExceptionMessage($exceptionMessage); + + new $className(['requiredProperty' => $reqPropertyValue, 'optionalProperty' => $optPropertyValue]); + } + + public function invalidRequiredAndOptionalConstPropertiesDataProvider(): array + { + return $this->combineDataProvider( + $this->implicitNullDataProvider(), + [ + ['blue', 'green', 'Invalid value for requiredProperty declined by const constraint'], + ['blue', null, 'Invalid value for requiredProperty declined by const constraint'], + ['red', 'blue', 'Invalid value for optionalProperty declined by const constraint'], + ['red', '0', 'Invalid value for optionalProperty declined by const constraint'], + ['red', '', 'Invalid value for optionalProperty declined by const constraint'], + ], + ); + } + + /** + * @throws FileSystemException + * @throws RenderException + * @throws SchemaException + */ + public function testProvidedNullValueConstPropertyIsValid(): void + { + $className = $this->generateClassFromFile('NullValueConstProperty.json', null, false, false); + + $object = new $className(['nullProperty' => null]); + + $this->assertNull($object->getNullProperty()); + } } diff --git a/tests/Schema/ConstPropertyTest/AnyOfConstProperty.json b/tests/Schema/ConstPropertyTest/AnyOfConstProperty.json new file mode 100644 index 00000000..94d9539f --- /dev/null +++ b/tests/Schema/ConstPropertyTest/AnyOfConstProperty.json @@ -0,0 +1,15 @@ +{ + "type": "object", + "properties": { + "property": { + "oneOf": [ + { + "const": "red" + }, + { + "const": 1 + } + ] + } + } +} \ No newline at end of file diff --git a/tests/Schema/ConstPropertyTest/ArrayItemConstProperty.json b/tests/Schema/ConstPropertyTest/ArrayItemConstProperty.json new file mode 100644 index 00000000..c111e74b --- /dev/null +++ b/tests/Schema/ConstPropertyTest/ArrayItemConstProperty.json @@ -0,0 +1,13 @@ +{ + "type": "object", + "properties": { + "property": { + "type": "array", + "items": [ + { + "const": "red" + } + ] + } + } +} \ No newline at end of file diff --git a/tests/Schema/ConstPropertyTest/NullValueConstProperty.json b/tests/Schema/ConstPropertyTest/NullValueConstProperty.json new file mode 100644 index 00000000..8ee4dc45 --- /dev/null +++ b/tests/Schema/ConstPropertyTest/NullValueConstProperty.json @@ -0,0 +1,11 @@ +{ + "type": "object", + "properties": { + "nullProperty": { + "const": null + } + }, + "required": [ + "nullProperty" + ] +} \ No newline at end of file diff --git a/tests/Schema/ConstPropertyTest/RequiredAndOptionalConstProperties.json b/tests/Schema/ConstPropertyTest/RequiredAndOptionalConstProperties.json new file mode 100644 index 00000000..06987a0a --- /dev/null +++ b/tests/Schema/ConstPropertyTest/RequiredAndOptionalConstProperties.json @@ -0,0 +1,14 @@ +{ + "type": "object", + "properties": { + "requiredProperty": { + "const": "red" + }, + "optionalProperty": { + "const": "green" + } + }, + "required": [ + "requiredProperty" + ] +} \ No newline at end of file