diff --git a/modules/openapi-generator/src/main/resources/php-nextgen/ObjectSerializer.mustache b/modules/openapi-generator/src/main/resources/php-nextgen/ObjectSerializer.mustache index afba5582f7e5..401561cfc353 100644 --- a/modules/openapi-generator/src/main/resources/php-nextgen/ObjectSerializer.mustache +++ b/modules/openapi-generator/src/main/resources/php-nextgen/ObjectSerializer.mustache @@ -499,7 +499,7 @@ class ObjectSerializer // If a discriminator is defined and points to a valid subclass, use it. $discriminator = $class::DISCRIMINATOR; if (!empty($discriminator) && isset($data->{$discriminator}) && is_string($data->{$discriminator})) { - $subclass = '\{{invokerPackage}}\Model\\' . $data->{$discriminator}; + $subclass = '\{{modelPackage}}\\' . $data->{$discriminator}; if (is_subclass_of($subclass, $class)) { $class = $subclass; } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/php/PhpNextgenClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/php/PhpNextgenClientCodegenTest.java index 02f58f48f64e..88ad980ac167 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/php/PhpNextgenClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/php/PhpNextgenClientCodegenTest.java @@ -120,6 +120,45 @@ public void testEnumUnknownDefaultCaseDeserializationEnabled() throws Exception Assert.assertListNotContains(modelContent, a -> a.equals("\"Invalid value '%s' for 'color', must be one of '%s'\","), ""); } + @Test + public void testDiscriminatorUsesModelPackageNamespace() throws Exception { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + OpenAPI openAPI = new OpenAPIParser() + .readLocation("src/test/resources/3_0/php-nextgen/petstore-with-fake-endpoints-models-for-testing.yaml", null, new ParseOptions()).getOpenAPI(); + + codegen.setOutputDir(output.getAbsolutePath()); + // Set invokerPackage="MyApp" and modelPackage="Entities" (relative suffix). + // AbstractPhpCodegen.processOpts() will produce final modelPackage = "MyApp\Entities". + // The old bug would have emitted '\MyApp\Model\' (invokerPackage + \Model\). + codegen.additionalProperties().put(CodegenConstants.INVOKER_PACKAGE, "MyApp"); + codegen.additionalProperties().put(CodegenConstants.MODEL_PACKAGE, "Entities"); + + ClientOptInput input = new ClientOptInput() + .openAPI(openAPI) + .config(codegen); + + DefaultGenerator generator = new DefaultGenerator(); + Map files = generator.opts(input).generate().stream() + .collect(Collectors.toMap(File::getName, Function.identity())); + + List objectSerializerContent = Files + .readAllLines(files.get("ObjectSerializer.php").toPath()) + .stream() + .map(String::trim) + .collect(Collectors.toList()); + + // The discriminator subclass lookup must use modelPackage (\MyApp\Entities\), + // NOT invokerPackage + '\Model' (\MyApp\Model\). + Assert.assertListContains(objectSerializerContent, + a -> a.contains("'\\MyApp\\Entities\\\\'"), + "ObjectSerializer discriminator subclass lookup must use modelPackage namespace"); + Assert.assertListNotContains(objectSerializerContent, + a -> a.contains("'\\MyApp\\Model\\\\'"), + "ObjectSerializer discriminator must NOT use invokerPackage\\Model namespace"); + } + @Test public void testEnumUnknownDefaultCaseDeserializationDisabled() throws Exception { File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); diff --git a/modules/openapi-generator/src/test/resources/3_0/php-nextgen/petstore-with-fake-endpoints-models-for-testing.yaml b/modules/openapi-generator/src/test/resources/3_0/php-nextgen/petstore-with-fake-endpoints-models-for-testing.yaml index bc56de575adb..6f5a52aaf851 100644 --- a/modules/openapi-generator/src/test/resources/3_0/php-nextgen/petstore-with-fake-endpoints-models-for-testing.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/php-nextgen/petstore-with-fake-endpoints-models-for-testing.yaml @@ -2291,3 +2291,22 @@ components: description: "Optional array of multiple errors encountered during processing" required: - error + + DiscriminatorBase: + type: object + discriminator: + propertyName: type + required: + - type + properties: + type: + type: string + + DiscriminatorChild: + allOf: + - $ref: '#/components/schemas/DiscriminatorBase' + - type: object + properties: + childProperty: + type: string + diff --git a/samples/client/petstore/php-nextgen/OpenAPIClient-php/.openapi-generator/FILES b/samples/client/petstore/php-nextgen/OpenAPIClient-php/.openapi-generator/FILES index 66030f86f31f..b45d2d263ee9 100644 --- a/samples/client/petstore/php-nextgen/OpenAPIClient-php/.openapi-generator/FILES +++ b/samples/client/petstore/php-nextgen/OpenAPIClient-php/.openapi-generator/FILES @@ -25,6 +25,8 @@ docs/Model/ChildWithNullable.md docs/Model/ClassModel.md docs/Model/Client.md docs/Model/DeprecatedObject.md +docs/Model/DiscriminatorBase.md +docs/Model/DiscriminatorChild.md docs/Model/Dog.md docs/Model/EnumArrays.md docs/Model/EnumClass.md @@ -91,6 +93,8 @@ src/Model/ChildWithNullable.php src/Model/ClassModel.php src/Model/Client.php src/Model/DeprecatedObject.php +src/Model/DiscriminatorBase.php +src/Model/DiscriminatorChild.php src/Model/Dog.php src/Model/EnumArrays.php src/Model/EnumClass.php diff --git a/samples/client/petstore/php-nextgen/OpenAPIClient-php/README.md b/samples/client/petstore/php-nextgen/OpenAPIClient-php/README.md index fa06252083f3..c967c54a34f0 100644 --- a/samples/client/petstore/php-nextgen/OpenAPIClient-php/README.md +++ b/samples/client/petstore/php-nextgen/OpenAPIClient-php/README.md @@ -142,6 +142,8 @@ Class | Method | HTTP request | Description - [ClassModel](docs/Model/ClassModel.md) - [Client](docs/Model/Client.md) - [DeprecatedObject](docs/Model/DeprecatedObject.md) +- [DiscriminatorBase](docs/Model/DiscriminatorBase.md) +- [DiscriminatorChild](docs/Model/DiscriminatorChild.md) - [Dog](docs/Model/Dog.md) - [EnumArrays](docs/Model/EnumArrays.md) - [EnumClass](docs/Model/EnumClass.md) diff --git a/samples/client/petstore/php-nextgen/OpenAPIClient-php/docs/Model/DiscriminatorBase.md b/samples/client/petstore/php-nextgen/OpenAPIClient-php/docs/Model/DiscriminatorBase.md new file mode 100644 index 000000000000..b7a29c2f8012 --- /dev/null +++ b/samples/client/petstore/php-nextgen/OpenAPIClient-php/docs/Model/DiscriminatorBase.md @@ -0,0 +1,9 @@ +# # DiscriminatorBase + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**type** | **string** | | + +[[Back to Model list]](../../README.md#models) [[Back to API list]](../../README.md#endpoints) [[Back to README]](../../README.md) diff --git a/samples/client/petstore/php-nextgen/OpenAPIClient-php/docs/Model/DiscriminatorChild.md b/samples/client/petstore/php-nextgen/OpenAPIClient-php/docs/Model/DiscriminatorChild.md new file mode 100644 index 000000000000..14aad19f0ec5 --- /dev/null +++ b/samples/client/petstore/php-nextgen/OpenAPIClient-php/docs/Model/DiscriminatorChild.md @@ -0,0 +1,9 @@ +# # DiscriminatorChild + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**child_property** | **string** | | [optional] + +[[Back to Model list]](../../README.md#models) [[Back to API list]](../../README.md#endpoints) [[Back to README]](../../README.md) diff --git a/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Model/DiscriminatorBase.php b/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Model/DiscriminatorBase.php new file mode 100644 index 000000000000..bcd5890714a3 --- /dev/null +++ b/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Model/DiscriminatorBase.php @@ -0,0 +1,414 @@ + + */ +class DiscriminatorBase implements ModelInterface, ArrayAccess, JsonSerializable +{ + public const DISCRIMINATOR = 'type'; + + /** + * The original name of the model. + * + * @var string + */ + protected static string $openAPIModelName = 'DiscriminatorBase'; + + /** + * Array of property to type mappings. Used for (de)serialization + * + * @var array + */ + protected static array $openAPITypes = [ + 'type' => 'string' + ]; + + /** + * Array of property to format mappings. Used for (de)serialization + * + * @var array + */ + protected static array $openAPIFormats = [ + 'type' => null + ]; + + /** + * Array of nullable properties. Used for (de)serialization + * + * @var array + */ + protected static array $openAPINullables = [ + 'type' => false + ]; + + /** + * If a nullable field gets set to null, insert it here + * + * @var array + */ + protected array $openAPINullablesSetToNull = []; + + /** + * Array of property to type mappings. Used for (de)serialization + * + * @return array + */ + public static function openAPITypes(): array + { + return self::$openAPITypes; + } + + /** + * Array of property to format mappings. Used for (de)serialization + * + * @return array + */ + public static function openAPIFormats(): array + { + return self::$openAPIFormats; + } + + /** + * Array of nullable properties + * + * @return array + */ + protected static function openAPINullables(): array + { + return self::$openAPINullables; + } + + /** + * Array of nullable field names deliberately set to null + * + * @return array + */ + private function getOpenAPINullablesSetToNull(): array + { + return $this->openAPINullablesSetToNull; + } + + /** + * Setter - Array of nullable field names deliberately set to null + * + * @param array $openAPINullablesSetToNull + */ + private function setOpenAPINullablesSetToNull(array $openAPINullablesSetToNull): void + { + $this->openAPINullablesSetToNull = $openAPINullablesSetToNull; + } + + /** + * Checks if a property is nullable + * + * @param string $property + * @return bool + */ + public static function isNullable(string $property): bool + { + return self::openAPINullables()[$property] ?? false; + } + + /** + * Checks if a nullable property is set to null. + * + * @param string $property + * @return bool + */ + public function isNullableSetToNull(string $property): bool + { + return in_array($property, $this->getOpenAPINullablesSetToNull(), true); + } + + /** + * Array of attributes where the key is the local name, + * and the value is the original name + * + * @var array + */ + protected static array $attributeMap = [ + 'type' => 'type' + ]; + + /** + * Array of attributes to setter functions (for deserialization of responses) + * + * @var array + */ + protected static array $setters = [ + 'type' => 'setType' + ]; + + /** + * Array of attributes to getter functions (for serialization of requests) + * + * @var array + */ + protected static array $getters = [ + 'type' => 'getType' + ]; + + /** + * Array of attributes where the key is the local name, + * and the value is the original name + * + * @return array + */ + public static function attributeMap(): array + { + return self::$attributeMap; + } + + /** + * Array of attributes to setter functions (for deserialization of responses) + * + * @return array + */ + public static function setters(): array + { + return self::$setters; + } + + /** + * Array of attributes to getter functions (for serialization of requests) + * + * @return array + */ + public static function getters(): array + { + return self::$getters; + } + + /** + * The original name of the model. + * + * @return string + */ + public function getModelName(): string + { + return self::$openAPIModelName; + } + + public const TYPE_DISCRIMINATOR_CHILD = 'DiscriminatorChild'; + + /** + * Associative array for storing property values + * + * @var array + */ + protected array $container = []; + + /** + * Constructor + * + * @param array $data Associated array of property values initializing the model + */ + public function __construct(?array $data = null) + { + // Initialize discriminator property with the model name. + $this->container['type'] = static::$openAPIModelName; + + $this->setIfExists('type', $data ?? [], null); + } + + /** + * Sets $this->container[$variableName] to the given data or to the given default Value; if $variableName + * is nullable and its value is set to null in the $fields array, then mark it as "set to null" in the + * $this->openAPINullablesSetToNull array + * + * @param string $variableName + * @param array $fields + * @param mixed $defaultValue + */ + private function setIfExists(string $variableName, array $fields, mixed $defaultValue): void + { + if (self::isNullable($variableName) && array_key_exists($variableName, $fields) && is_null($fields[$variableName])) { + $this->openAPINullablesSetToNull[] = $variableName; + } + + $this->container[$variableName] = $fields[$variableName] ?? $defaultValue; + } + + /** + * Show all the invalid properties with reasons. + * + * @return string[] invalid properties with reasons + */ + public function listInvalidProperties(): array + { + $invalidProperties = []; + + if ($this->container['type'] === null) { + $invalidProperties[] = "'type' can't be null"; + } + return $invalidProperties; + } + + /** + * Validate all the properties in the model + * return true if all passed + * + * @return bool True if all properties are valid + */ + public function valid(): bool + { + return count($this->listInvalidProperties()) === 0; + } + + + /** + * Gets type + * + * @return string + */ + public function getType(): string + { + return $this->container['type']; + } + + /** + * Sets type + * + * @param string $type type + * + * @return $this + */ + public function setType(string $type): static + { + if (is_null($type)) { + throw new InvalidArgumentException('non-nullable type cannot be null'); + } + $this->container['type'] = $type; + + return $this; + } + /** + * Returns true if offset exists. False otherwise. + * + * @param integer $offset Offset + * + * @return boolean + */ + public function offsetExists(mixed $offset): bool + { + return isset($this->container[$offset]); + } + + /** + * Gets offset. + * + * @param integer $offset Offset + * + * @return mixed|null + */ + #[ReturnTypeWillChange] + public function offsetGet(mixed $offset): mixed + { + return $this->container[$offset] ?? null; + } + + /** + * Sets value based on offset. + * + * @param int|null $offset Offset + * @param mixed $value Value to be set + * + * @return void + */ + public function offsetSet(mixed $offset, mixed $value): void + { + if (is_null($offset)) { + $this->container[] = $value; + } else { + $this->container[$offset] = $value; + } + } + + /** + * Unsets offset. + * + * @param integer $offset Offset + * + * @return void + */ + public function offsetUnset(mixed $offset): void + { + unset($this->container[$offset]); + } + + /** + * Serializes the object to a value that can be serialized natively by json_encode(). + * @link https://www.php.net/manual/en/jsonserializable.jsonserialize.php + * + * @return mixed Returns data which can be serialized by json_encode(), which is a value + * of any type other than a resource. + */ + #[ReturnTypeWillChange] + public function jsonSerialize(): mixed + { + return ObjectSerializer::sanitizeForSerialization($this); + } + + /** + * Gets the string presentation of the object + * + * @return string + */ + public function __toString(): string + { + return json_encode( + ObjectSerializer::sanitizeForSerialization($this), + JSON_PRETTY_PRINT + ); + } + + /** + * Gets a header-safe presentation of the object + * + * @return string + */ + public function toHeaderValue(): string + { + return json_encode(ObjectSerializer::sanitizeForSerialization($this)); + } +} + + diff --git a/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Model/DiscriminatorChild.php b/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Model/DiscriminatorChild.php new file mode 100644 index 000000000000..385d408d70d8 --- /dev/null +++ b/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Model/DiscriminatorChild.php @@ -0,0 +1,397 @@ + + */ +class DiscriminatorChild extends DiscriminatorBase +{ + public const DISCRIMINATOR = null; + + /** + * The original name of the model. + * + * @var string + */ + protected static string $openAPIModelName = 'DiscriminatorChild'; + + /** + * Array of property to type mappings. Used for (de)serialization + * + * @var array + */ + protected static array $openAPITypes = [ + 'child_property' => 'string' + ]; + + /** + * Array of property to format mappings. Used for (de)serialization + * + * @var array + */ + protected static array $openAPIFormats = [ + 'child_property' => null + ]; + + /** + * Array of nullable properties. Used for (de)serialization + * + * @var array + */ + protected static array $openAPINullables = [ + 'child_property' => false + ]; + + /** + * If a nullable field gets set to null, insert it here + * + * @var array + */ + protected array $openAPINullablesSetToNull = []; + + /** + * Array of property to type mappings. Used for (de)serialization + * + * @return array + */ + public static function openAPITypes(): array + { + return self::$openAPITypes + parent::openAPITypes(); + } + + /** + * Array of property to format mappings. Used for (de)serialization + * + * @return array + */ + public static function openAPIFormats(): array + { + return self::$openAPIFormats + parent::openAPIFormats(); + } + + /** + * Array of nullable properties + * + * @return array + */ + protected static function openAPINullables(): array + { + return self::$openAPINullables + parent::openAPINullables(); + } + + /** + * Array of nullable field names deliberately set to null + * + * @return array + */ + private function getOpenAPINullablesSetToNull(): array + { + return $this->openAPINullablesSetToNull; + } + + /** + * Setter - Array of nullable field names deliberately set to null + * + * @param array $openAPINullablesSetToNull + */ + private function setOpenAPINullablesSetToNull(array $openAPINullablesSetToNull): void + { + $this->openAPINullablesSetToNull = $openAPINullablesSetToNull; + } + + /** + * Checks if a property is nullable + * + * @param string $property + * @return bool + */ + public static function isNullable(string $property): bool + { + return self::openAPINullables()[$property] ?? false; + } + + /** + * Checks if a nullable property is set to null. + * + * @param string $property + * @return bool + */ + public function isNullableSetToNull(string $property): bool + { + return in_array($property, $this->getOpenAPINullablesSetToNull(), true); + } + + /** + * Array of attributes where the key is the local name, + * and the value is the original name + * + * @var array + */ + protected static array $attributeMap = [ + 'child_property' => 'childProperty' + ]; + + /** + * Array of attributes to setter functions (for deserialization of responses) + * + * @var array + */ + protected static array $setters = [ + 'child_property' => 'setChildProperty' + ]; + + /** + * Array of attributes to getter functions (for serialization of requests) + * + * @var array + */ + protected static array $getters = [ + 'child_property' => 'getChildProperty' + ]; + + /** + * Array of attributes where the key is the local name, + * and the value is the original name + * + * @return array + */ + public static function attributeMap(): array + { + return parent::attributeMap() + self::$attributeMap; + } + + /** + * Array of attributes to setter functions (for deserialization of responses) + * + * @return array + */ + public static function setters(): array + { + return parent::setters() + self::$setters; + } + + /** + * Array of attributes to getter functions (for serialization of requests) + * + * @return array + */ + public static function getters(): array + { + return parent::getters() + self::$getters; + } + + /** + * The original name of the model. + * + * @return string + */ + public function getModelName(): string + { + return self::$openAPIModelName; + } + + + + /** + * Constructor + * + * @param array $data Associated array of property values initializing the model + */ + public function __construct(?array $data = null) + { + parent::__construct($data); + + $this->setIfExists('child_property', $data ?? [], null); + } + + /** + * Sets $this->container[$variableName] to the given data or to the given default Value; if $variableName + * is nullable and its value is set to null in the $fields array, then mark it as "set to null" in the + * $this->openAPINullablesSetToNull array + * + * @param string $variableName + * @param array $fields + * @param mixed $defaultValue + */ + private function setIfExists(string $variableName, array $fields, mixed $defaultValue): void + { + if (self::isNullable($variableName) && array_key_exists($variableName, $fields) && is_null($fields[$variableName])) { + $this->openAPINullablesSetToNull[] = $variableName; + } + + $this->container[$variableName] = $fields[$variableName] ?? $defaultValue; + } + + /** + * Show all the invalid properties with reasons. + * + * @return string[] invalid properties with reasons + */ + public function listInvalidProperties(): array + { + $invalidProperties = parent::listInvalidProperties(); + + return $invalidProperties; + } + + /** + * Validate all the properties in the model + * return true if all passed + * + * @return bool True if all properties are valid + */ + public function valid(): bool + { + return count($this->listInvalidProperties()) === 0; + } + + + /** + * Gets child_property + * + * @return string|null + */ + public function getChildProperty(): ?string + { + return $this->container['child_property']; + } + + /** + * Sets child_property + * + * @param string|null $child_property child_property + * + * @return $this + */ + public function setChildProperty(?string $child_property): static + { + if (is_null($child_property)) { + throw new InvalidArgumentException('non-nullable child_property cannot be null'); + } + $this->container['child_property'] = $child_property; + + return $this; + } + /** + * Returns true if offset exists. False otherwise. + * + * @param integer $offset Offset + * + * @return boolean + */ + public function offsetExists(mixed $offset): bool + { + return isset($this->container[$offset]); + } + + /** + * Gets offset. + * + * @param integer $offset Offset + * + * @return mixed|null + */ + #[ReturnTypeWillChange] + public function offsetGet(mixed $offset): mixed + { + return $this->container[$offset] ?? null; + } + + /** + * Sets value based on offset. + * + * @param int|null $offset Offset + * @param mixed $value Value to be set + * + * @return void + */ + public function offsetSet(mixed $offset, mixed $value): void + { + if (is_null($offset)) { + $this->container[] = $value; + } else { + $this->container[$offset] = $value; + } + } + + /** + * Unsets offset. + * + * @param integer $offset Offset + * + * @return void + */ + public function offsetUnset(mixed $offset): void + { + unset($this->container[$offset]); + } + + /** + * Serializes the object to a value that can be serialized natively by json_encode(). + * @link https://www.php.net/manual/en/jsonserializable.jsonserialize.php + * + * @return mixed Returns data which can be serialized by json_encode(), which is a value + * of any type other than a resource. + */ + #[ReturnTypeWillChange] + public function jsonSerialize(): mixed + { + return ObjectSerializer::sanitizeForSerialization($this); + } + + /** + * Gets the string presentation of the object + * + * @return string + */ + public function __toString(): string + { + return json_encode( + ObjectSerializer::sanitizeForSerialization($this), + JSON_PRETTY_PRINT + ); + } + + /** + * Gets a header-safe presentation of the object + * + * @return string + */ + public function toHeaderValue(): string + { + return json_encode(ObjectSerializer::sanitizeForSerialization($this)); + } +} + + diff --git a/samples/client/petstore/php-nextgen/OpenAPIClient-php/tests/Model/DiscriminatorBaseTest.php b/samples/client/petstore/php-nextgen/OpenAPIClient-php/tests/Model/DiscriminatorBaseTest.php new file mode 100644 index 000000000000..7732b6f86f40 --- /dev/null +++ b/samples/client/petstore/php-nextgen/OpenAPIClient-php/tests/Model/DiscriminatorBaseTest.php @@ -0,0 +1,88 @@ +