diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpNextgenClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpNextgenClientCodegen.java index 4986432cb624..80f9e3f2b07b 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpNextgenClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpNextgenClientCodegen.java @@ -125,6 +125,7 @@ public void processOpts() { supportingFiles.add(new SupportingFile("ObjectSerializer.mustache", toSrcPath(invokerPackage, srcBasePath), "ObjectSerializer.php")); supportingFiles.add(new SupportingFile("ModelInterface.mustache", toSrcPath(modelPackage, srcBasePath), "ModelInterface.php")); supportingFiles.add(new SupportingFile("OneOfInterface.mustache", toSrcPath(modelPackage, srcBasePath), "OneOfInterface.php")); + supportingFiles.add(new SupportingFile("AnyOfInterface.mustache", toSrcPath(modelPackage, srcBasePath), "AnyOfInterface.php")); supportingFiles.add(new SupportingFile("HeaderSelector.mustache", toSrcPath(invokerPackage, srcBasePath), "HeaderSelector.php")); supportingFiles.add(new SupportingFile("composer.mustache", "", "composer.json")); supportingFiles.add(new SupportingFile("README.mustache", "", "README.md")); @@ -143,40 +144,80 @@ public void processOpts() { public Map postProcessAllModels(Map objs) { final Map processed = super.postProcessAllModels(objs); - Map oneOfTypeHints = new HashMap<>(); + Map composedTypeHints = new HashMap<>(); for (ModelsMap modelsMap : processed.values()) { for (ModelMap m : modelsMap.getModels()) { - collectOneOfTypeHint(m.getModel(), oneOfTypeHints); + collectComposedTypeHint(m.getModel(), composedTypeHints); } } + flattenComposedTypeHints(composedTypeHints); for (Map.Entry entry : processed.entrySet()) { - entry.setValue(postProcessModelsMap(entry.getValue(), oneOfTypeHints)); + entry.setValue(postProcessModelsMap(entry.getValue(), composedTypeHints)); } return processed; } /** - * If the given model is a oneOf composition, record the PHP union type that should be used - * wherever the model is referenced. + * If the given model is a oneOf or anyOf composition, record the PHP union type that should be + * used wherever the model is referenced. A model that declares both contributes all members. */ - private void collectOneOfTypeHint(CodegenModel model, Map oneOfTypeHints) { + private void collectComposedTypeHint(CodegenModel model, Map composedTypeHints) { if (model == null || model.getComposedSchemas() == null) { return; } - List oneOf = model.getComposedSchemas().getOneOf(); - if (oneOf == null || oneOf.isEmpty()) { + CodegenComposedSchemas composed = model.getComposedSchemas(); + List members = new ArrayList<>(); + if (composed.getOneOf() != null) { + members.addAll(composed.getOneOf()); + } + if (composed.getAnyOf() != null) { + members.addAll(composed.getAnyOf()); + } + if (members.isEmpty()) { return; } Set memberTypes = new LinkedHashSet<>(); - for (CodegenProperty member : oneOf) { + for (CodegenProperty member : members) { memberTypes.add((member.isArray || member.isMap) ? "array" : member.dataType); } - oneOfTypeHints.put("\\" + modelPackage + "\\" + model.classname, String.join("|", memberTypes)); + composedTypeHints.put("\\" + modelPackage + "\\" + model.classname, String.join("|", memberTypes)); + } + + /** + * Expand each composed type's union hint transitively: a member that is itself a composed type + * is replaced by its own leaf members. {@see ObjectSerializer::deserializeComposed()} unwraps + * nested composition down to the leaf instance, so a property typed with an intermediate + * composed member would otherwise reject the leaf the deserializer actually returns. + */ + private void flattenComposedTypeHints(Map composedTypeHints) { + Map resolved = new HashMap<>(); + for (String composedType : composedTypeHints.keySet()) { + Set leaves = new LinkedHashSet<>(); + collectLeafTypes(composedType, composedTypeHints, new LinkedHashSet<>(), leaves); + resolved.put(composedType, String.join("|", leaves)); + } + composedTypeHints.putAll(resolved); + } + + /** + * Accumulate into {@code leaves} the non-composed member types reachable from {@code type}. A + * member that is itself a composed type (a key in {@code composedTypeHints}) is expanded + * recursively; {@code visiting} guards against cycles in self-referential schemas. + */ + private void collectLeafTypes(String type, Map composedTypeHints, Set visiting, Set leaves) { + if (!composedTypeHints.containsKey(type) || !visiting.add(type)) { + leaves.add(type); + return; + } + for (String member : composedTypeHints.get(type).split("\\|")) { + collectLeafTypes(member, composedTypeHints, visiting, leaves); + } + visiting.remove(type); } /** @@ -189,19 +230,19 @@ private static String makeNullable(String phpType) { /** * The base PHP type hint for a single element: a container collapses to {@code array} (PHP - * cannot type-hint {@code Foo[]}), a oneOf alias expands to the union of its members, and + * cannot type-hint {@code Foo[]}), a composed (oneOf/anyOf) alias expands to the union of its members, and * everything else stays its {@code dataType}. */ - private String phpBaseType(String dataType, boolean isContainer, Map oneOfTypeHints) { - return isContainer ? "array" : oneOfTypeHints.getOrDefault(dataType, dataType); + private String phpBaseType(String dataType, boolean isContainer, Map composedTypeHints) { + return isContainer ? "array" : composedTypeHints.getOrDefault(dataType, dataType); } /** * The PHP signature type hint: the {@link #phpBaseType base type}, made nullable when the * element is optional or nullable - except {@code mixed}, which already admits null. */ - private String phpSignatureType(String dataType, boolean isContainer, boolean nullable, Map oneOfTypeHints) { - String base = phpBaseType(dataType, isContainer, oneOfTypeHints); + private String phpSignatureType(String dataType, boolean isContainer, boolean nullable, Map composedTypeHints) { + String base = phpBaseType(dataType, isContainer, composedTypeHints); return (nullable && !base.equals("mixed")) ? makeNullable(base) : base; } @@ -209,7 +250,7 @@ private String phpSignatureType(String dataType, boolean isContainer, boolean nu * Wrap an expanded inner union back into container phpdoc notation: {@code (Apple|Banana)[]} * for arrays (parenthesised so {@code []} binds to the whole union, not just its last member) * and {@code array} for maps. A {@code null} inner propagates, signalling - * "no oneOf in this type". + * "no composed schema in this type". */ private static String wrapContainerDoc(boolean isArray, String inner) { if (inner == null) { @@ -220,50 +261,50 @@ private static String wrapContainerDoc(boolean isArray, String inner) { } /** - * The phpdoc type with any reference to a oneOf model expanded to the union of its members. - * A oneOf model is only a deserialization dispatcher, so its members do not inherit from it + * The phpdoc type with any reference to a composed (oneOf/anyOf) model expanded to the union of its members. + * A composed model is only a deserialization dispatcher, so its members do not inherit from it * and {@code @param Fruit} would be a lie — {@code @param Apple|Banana} is the truth. - * Returns {@code null} when no oneOf is involved, so the caller can leave the original + * Returns {@code null} when no composed model is involved, so the caller can leave the original * {@code dataType} phpdoc untouched. */ - private String oneOfDocType(CodegenProperty prop, Map oneOfTypeHints) { - return docTypeOf(prop.isArray, prop.isMap, prop.items, prop.dataType, oneOfTypeHints); + private String composedDocType(CodegenProperty prop, Map composedTypeHints) { + return docTypeOf(prop.isArray, prop.isMap, prop.items, prop.dataType, composedTypeHints); } - /** @see #oneOfDocType(CodegenProperty, Map) */ - private String oneOfDocType(CodegenParameter param, Map oneOfTypeHints) { - return docTypeOf(param.isArray, param.isMap, param.items, param.dataType, oneOfTypeHints); + /** @see #composedDocType(CodegenProperty, Map) */ + private String composedDocType(CodegenParameter param, Map composedTypeHints) { + return docTypeOf(param.isArray, param.isMap, param.items, param.dataType, composedTypeHints); } - /** @see #oneOfDocType(CodegenProperty, Map) */ - private String oneOfDocType(CodegenResponse response, Map oneOfTypeHints) { - return docTypeOf(response.isArray, response.isMap, response.items, response.dataType, oneOfTypeHints); + /** @see #composedDocType(CodegenProperty, Map) */ + private String composedDocType(CodegenResponse response, Map composedTypeHints) { + return docTypeOf(response.isArray, response.isMap, response.items, response.dataType, composedTypeHints); } /** - * The shared core of the {@code oneOfDocType} overloads: expands a oneOf {@code dataType} to the - * union of its members (recursing through array/map items so the expansion reaches nested oneOfs), - * or returns {@code null} when no oneOf is involved. See {@link #oneOfDocType(CodegenProperty, Map)}. + * The shared core of the {@code composedDocType} overloads: expands a composed {@code dataType} to the + * union of its members (recursing through array/map items so the expansion reaches nested composed schemas), + * or returns {@code null} when no composed model is involved. See {@link #composedDocType(CodegenProperty, Map)}. */ - private String docTypeOf(boolean isArray, boolean isMap, CodegenProperty items, String dataType, Map oneOfTypeHints) { + private String docTypeOf(boolean isArray, boolean isMap, CodegenProperty items, String dataType, Map composedTypeHints) { if ((isArray || isMap) && items != null) { - return wrapContainerDoc(isArray, oneOfDocType(items, oneOfTypeHints)); + return wrapContainerDoc(isArray, composedDocType(items, composedTypeHints)); } - return oneOfTypeHints.get(dataType); + return composedTypeHints.get(dataType); } /** - * The final phpdoc type, ready for the template to emit verbatim: the oneOf-expanded type (or the - * unchanged {@code dataType} when no oneOf is involved), with a {@code |null} member appended + * The final phpdoc type, ready for the template to emit verbatim: the union-expanded type (or the + * unchanged {@code dataType} when no composed model is involved), with a {@code |null} member appended * when the element is optional or nullable. phpdoc unions always spell out {@code |null} * rather than using the {@code ?T} shorthand. */ - private String phpDocType(CodegenProperty prop, Map oneOfTypeHints) { - return bakeDocType(oneOfDocType(prop, oneOfTypeHints), prop.dataType, prop.notRequiredOrIsNullable()); + private String phpDocType(CodegenProperty prop, Map composedTypeHints) { + return bakeDocType(composedDocType(prop, composedTypeHints), prop.dataType, prop.notRequiredOrIsNullable()); } - private String phpDocType(CodegenParameter param, Map oneOfTypeHints) { - return bakeDocType(oneOfDocType(param, oneOfTypeHints), param.dataType, param.notRequiredOrIsNullable()); + private String phpDocType(CodegenParameter param, Map composedTypeHints) { + return bakeDocType(composedDocType(param, composedTypeHints), param.dataType, param.notRequiredOrIsNullable()); } /** @@ -277,34 +318,34 @@ private static String bakeDocType(String expandedType, String dataType, boolean } /** - * A oneOf model is an abstract dispatcher, so the default doc example ({@code new Mammal()}) + * A composed model is an abstract dispatcher, so the default doc example ({@code new Mammal()}) * instantiates a type that cannot be used. Rewrite the example to instantiate the first member - * of the union instead ({@code new Whale()}). Handles a oneOf parameter directly as well as a - * container whose items are a oneOf. + * of the union instead ({@code new Whale()}). Handles a composed parameter directly as well as a + * container whose items are composed. */ - private void useFirstOneOfMemberInExample(CodegenParameter param, Map oneOfTypeHints) { + private void useFirstComposedMemberInExample(CodegenParameter param, Map composedTypeHints) { if (param.example == null) { return; } - String alias = oneOfTypeHints.containsKey(param.dataType) ? param.dataType - : (param.items != null && oneOfTypeHints.containsKey(param.items.dataType) ? param.items.dataType : null); + String alias = composedTypeHints.containsKey(param.dataType) ? param.dataType + : (param.items != null && composedTypeHints.containsKey(param.items.dataType) ? param.items.dataType : null); if (alias == null) { return; } - String firstMember = oneOfTypeHints.get(alias).split("\\|", 2)[0]; + String firstMember = composedTypeHints.get(alias).split("\\|", 2)[0]; if (firstMember.startsWith("\\")) { // a concrete class we can instantiate param.example = param.example.replace(alias, firstMember); } } - private ModelsMap postProcessModelsMap(ModelsMap objs, Map oneOfTypeHints) { + private ModelsMap postProcessModelsMap(ModelsMap objs, Map composedTypeHints) { for (ModelMap m : objs.getModels()) { CodegenModel model = m.getModel(); for (CodegenProperty prop : model.vars) { prop.vendorExtensions.putIfAbsent("x-php-prop-type", - phpSignatureType(prop.dataType, prop.isArray || prop.isMap, prop.notRequiredOrIsNullable(), oneOfTypeHints)); - prop.vendorExtensions.putIfAbsent("x-php-prop-doc-type", phpDocType(prop, oneOfTypeHints)); + phpSignatureType(prop.dataType, prop.isArray || prop.isMap, prop.notRequiredOrIsNullable(), composedTypeHints)); + prop.vendorExtensions.putIfAbsent("x-php-prop-doc-type", phpDocType(prop, composedTypeHints)); } } return objs; @@ -314,10 +355,11 @@ private ModelsMap postProcessModelsMap(ModelsMap objs, Map oneOf public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List allModels) { objs = super.postProcessOperationsWithModels(objs, allModels); - Map oneOfTypeHints = new HashMap<>(); + Map composedTypeHints = new HashMap<>(); for (ModelMap m : allModels) { - collectOneOfTypeHint(m.getModel(), oneOfTypeHints); + collectComposedTypeHint(m.getModel(), composedTypeHints); } + flattenComposedTypeHints(composedTypeHints); OperationMap operations = objs.getOperations(); for (CodegenOperation operation : operations.getOperation()) { @@ -328,9 +370,9 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, Listpartial_header}} + +/** + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +namespace {{modelPackage}}; + +/** + * Interface implemented by models generated from an `anyOf` schema. + * + * An `anyOf` schema is not represented by a value object; instead a value is one of the + * member types. Classes implementing this interface only carry the metadata that + * {@see ObjectSerializer::deserialize()} needs to resolve the concrete member type. + * + * @package {{modelPackage}} + * @author OpenAPI Generator team + */ +interface AnyOfInterface +{ + /** + * List of the types a value of this `anyOf` schema may be. Each entry uses the same + * notation as the values of {@see ModelInterface::openAPITypes()}. + * + * @return string[] + */ + public static function getAnyOfTypes(): array; + + /** + * Name of the discriminator property used to resolve the concrete member type, or null + * when the schema has no discriminator. + * + * @return string|null + */ + public static function getAnyOfDiscriminator(): ?string; + + /** + * Mapping of discriminator values to the concrete member type. Empty when the schema has + * no discriminator. + * + * @return array + */ + public static function getAnyOfDiscriminatorMappings(): array; +} 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 3cc87ee6add4..483c17795e06 100644 --- a/modules/openapi-generator/src/main/resources/php-nextgen/ObjectSerializer.mustache +++ b/modules/openapi-generator/src/main/resources/php-nextgen/ObjectSerializer.mustache @@ -492,14 +492,18 @@ class ObjectSerializer } else { $data = is_string($data) ? json_decode($data) : $data; - if (is_array($data)) { - $data = (object) $data; - } - - // A oneOf schema is not a value object: resolve the data to one of its member types. + // A oneOf/anyOf schema is not a value object: dispatch here, after json_decode (so + // members get the decoded value) but before the array-to-object cast (which breaks arrays). if (is_subclass_of($class, '\{{modelPackage}}\OneOfInterface')) { return self::deserializeOneOf($data, $class, $httpHeaders); } + if (is_subclass_of($class, '\{{modelPackage}}\AnyOfInterface')) { + return self::deserializeAnyOf($data, $class, $httpHeaders); + } + + if (is_array($data)) { + $data = (object) $data; + } // If a discriminator is defined and points to a valid subclass, use it. $discriminator = $class::DISCRIMINATOR; @@ -543,7 +547,7 @@ class ObjectSerializer * Otherwise each member type is tried in turn and the first one that yields a valid model * (or a non-null primitive) is returned. * - * @param mixed $data the data already decoded to an object + * @param mixed $data the decoded data to resolve to a member type * @param string $class a class name implementing OneOfInterface * @param string[]|null $httpHeaders HTTP headers * @@ -551,11 +555,16 @@ class ObjectSerializer */ private static function deserializeOneOf(mixed $data, string $class, ?array $httpHeaders): mixed { + // $data is already decoded (see deserialize()). Read the discriminator from an object view + // of it, but deserialize each member from the original $data so array and scalar members + // keep their own type handling instead of being coerced to an object here. + $probe = is_array($data) ? (object) $data : $data; + $discriminator = $class::getOneOfDiscriminator(); - if ($discriminator !== null && isset($data->{$discriminator}) && is_string($data->{$discriminator})) { + if ($discriminator !== null && is_object($probe) && isset($probe->{$discriminator}) && is_string($probe->{$discriminator})) { $mappings = $class::getOneOfDiscriminatorMappings(); - if (isset($mappings[$data->{$discriminator}])) { - return self::deserialize($data, $mappings[$data->{$discriminator}], $httpHeaders); + if (isset($mappings[$probe->{$discriminator}])) { + return self::deserialize($data, $mappings[$probe->{$discriminator}], $httpHeaders); } } @@ -563,7 +572,9 @@ class ObjectSerializer try { $instance = self::deserialize($data, $type, $httpHeaders); } catch (\Throwable $e) { - // The data does not match this member type, try the next one. + // Any failure means the data does not match this member type; try the next one. + // The broad catch is intentional: a real failure is not hidden, since the loop + // throws below when no member type matches. continue; } @@ -579,6 +590,56 @@ class ObjectSerializer throw new \InvalidArgumentException(sprintf('No matching schema in oneOf %s for the given data', $class)); } + /** + * Deserialize data into one of the member types of an `anyOf` schema. + * + * When the schema declares a discriminator, its value selects the member type directly. + * Otherwise each member type is tried in turn and the first one that yields a valid model + * (or a non-null primitive) is returned. + * + * @param mixed $data the decoded data to resolve to a member type + * @param string $class a class name implementing AnyOfInterface + * @param string[]|null $httpHeaders HTTP headers + * + * @return mixed an instance of one of the `anyOf` member types + */ + private static function deserializeAnyOf(mixed $data, string $class, ?array $httpHeaders): mixed + { + // $data is already decoded (see deserialize()). Read the discriminator from an object view + // of it, but deserialize each member from the original $data so array and scalar members + // keep their own type handling instead of being coerced to an object here. + $probe = is_array($data) ? (object) $data : $data; + + $discriminator = $class::getAnyOfDiscriminator(); + if ($discriminator !== null && is_object($probe) && isset($probe->{$discriminator}) && is_string($probe->{$discriminator})) { + $mappings = $class::getAnyOfDiscriminatorMappings(); + if (isset($mappings[$probe->{$discriminator}])) { + return self::deserialize($data, $mappings[$probe->{$discriminator}], $httpHeaders); + } + } + + foreach ($class::getAnyOfTypes() as $type) { + try { + $instance = self::deserialize($data, $type, $httpHeaders); + } catch (\Throwable $e) { + // Any failure means the data does not match this member type; try the next one. + // The broad catch is intentional: a real failure is not hidden, since the loop + // throws below when no member type matches. + continue; + } + + if ($instance instanceof ModelInterface) { + if ($instance->valid()) { + return $instance; + } + } elseif ($instance !== null) { + return $instance; + } + } + + throw new \InvalidArgumentException(sprintf('No matching schema in anyOf %s for the given data', $class)); + } + /** * Build a query string from an array of key value pairs. * diff --git a/modules/openapi-generator/src/main/resources/php-nextgen/model.mustache b/modules/openapi-generator/src/main/resources/php-nextgen/model.mustache index 20be97852f30..dea69ff25df5 100644 --- a/modules/openapi-generator/src/main/resources/php-nextgen/model.mustache +++ b/modules/openapi-generator/src/main/resources/php-nextgen/model.mustache @@ -22,6 +22,7 @@ namespace {{modelPackage}}; {{^isEnum}} {{^oneOf}} +{{^anyOf}} {{^parentSchema}} use ArrayAccess; @@ -30,6 +31,7 @@ use JsonSerializable; use InvalidArgumentException; use ReturnTypeWillChange; use {{invokerPackage}}\ObjectSerializer; +{{/anyOf}} {{/oneOf}} {{/isEnum}} @@ -45,10 +47,12 @@ use {{invokerPackage}}\ObjectSerializer; {{^parentSchema}} {{^isEnum}} {{^oneOf}} +{{^anyOf}} * @implements ArrayAccess +{{/anyOf}} {{/oneOf}} {{/isEnum}} {{/parentSchema}} */ -{{#isEnum}}{{>model_enum}}{{/isEnum}}{{^isEnum}}{{#oneOf}}{{#-first}}{{>model_oneof}}{{/-first}}{{/oneOf}}{{^oneOf}}{{>model_generic}}{{/oneOf}}{{/isEnum}} +{{#isEnum}}{{>model_enum}}{{/isEnum}}{{^isEnum}}{{#oneOf}}{{#-first}}{{>model_oneof}}{{/-first}}{{/oneOf}}{{^oneOf}}{{#anyOf}}{{#-first}}{{>model_anyof}}{{/-first}}{{/anyOf}}{{^anyOf}}{{>model_generic}}{{/anyOf}}{{/oneOf}}{{/isEnum}} {{/model}}{{/models}} diff --git a/modules/openapi-generator/src/main/resources/php-nextgen/model_anyof.mustache b/modules/openapi-generator/src/main/resources/php-nextgen/model_anyof.mustache new file mode 100644 index 000000000000..5f042a851703 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/php-nextgen/model_anyof.mustache @@ -0,0 +1,46 @@ +class {{classname}} implements AnyOfInterface +{ + /** + * The original name of the model. + * + * @var string + */ + public const MODEL_NAME = '{{name}}'; + + /** + * The discriminator property name, or null when the schema has no discriminator. + * + * @var string|null + */ + public const DISCRIMINATOR = {{#discriminator}}'{{propertyBaseName}}'{{/discriminator}}{{^discriminator}}null{{/discriminator}}; + + /** + * {@inheritdoc} + */ + public static function getAnyOfTypes(): array + { + return [ + {{#composedSchemas}}{{#anyOf}}'{{{dataType}}}'{{^-last}}, + {{/-last}}{{/anyOf}}{{/composedSchemas}} + ]; + } + + /** + * {@inheritdoc} + */ + public static function getAnyOfDiscriminator(): ?string + { + return self::DISCRIMINATOR; + } + + /** + * {@inheritdoc} + */ + public static function getAnyOfDiscriminatorMappings(): array + { + return [ + {{#discriminator}}{{#mappedModels}}'{{mappingName}}' => '\{{modelPackage}}\{{modelName}}'{{^-last}}, + {{/-last}}{{/mappedModels}}{{/discriminator}} + ]; + } +} diff --git a/modules/openapi-generator/src/main/resources/php-nextgen/model_doc.mustache b/modules/openapi-generator/src/main/resources/php-nextgen/model_doc.mustache index 24981a866bcc..3f333297ce17 100644 --- a/modules/openapi-generator/src/main/resources/php-nextgen/model_doc.mustache +++ b/modules/openapi-generator/src/main/resources/php-nextgen/model_doc.mustache @@ -1,5 +1,6 @@ {{#models}}{{#model}}# {{classname}} {{#oneOf.isEmpty}} +{{#anyOf.isEmpty}} ## Properties @@ -7,6 +8,24 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- {{#vars}}**{{name}}** | {{#isPrimitiveType}}**{{{dataType}}}**{{/isPrimitiveType}}{{^isPrimitiveType}}[**{{{dataType}}}**]({{complexType}}.md){{/isPrimitiveType}} | {{description}} |{{^required}} [optional]{{/required}}{{#isReadOnly}} [readonly]{{/isReadOnly}}{{#defaultValue}} [default to {{{.}}}]{{/defaultValue}} {{/vars}} +{{/anyOf.isEmpty}} +{{^anyOf.isEmpty}} + +This model is an `anyOf` wrapper: a value is at least one of the member types listed below. +It is never instantiated directly — use one of the concrete types. + +## anyOf + +{{#composedSchemas}} +{{#anyOf}} +- {{#complexType}}[**{{{dataType}}}**]({{complexType}}.md){{/complexType}}{{^complexType}}**{{{dataType}}}**{{/complexType}} +{{/anyOf}} +{{/composedSchemas}} +{{#discriminator}} + +The concrete type is selected by the `{{propertyName}}` discriminator property. +{{/discriminator}} +{{/anyOf.isEmpty}} {{/oneOf.isEmpty}} {{^oneOf.isEmpty}} diff --git a/modules/openapi-generator/src/main/resources/php-nextgen/model_test.mustache b/modules/openapi-generator/src/main/resources/php-nextgen/model_test.mustache index c3aad321d158..e271c45be40d 100644 --- a/modules/openapi-generator/src/main/resources/php-nextgen/model_test.mustache +++ b/modules/openapi-generator/src/main/resources/php-nextgen/model_test.mustache @@ -70,6 +70,10 @@ class {{classname}}Test extends TestCase // TODO: implement self::markTestIncomplete('Not implemented'); } +{{! Composed (oneOf/anyOf) models are dispatchers with no own properties: their `vars` are the }} +{{! members' flattened properties, so per-property test stubs would be meaningless. Skip them. }} +{{^oneOf}} +{{^anyOf}} {{#vars}} /** @@ -81,6 +85,8 @@ class {{classname}}Test extends TestCase self::markTestIncomplete('Not implemented'); } {{/vars}} +{{/anyOf}} +{{/oneOf}} } {{/model}} {{/models}} 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 890de3f02096..ed809cf4a24a 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 @@ -450,6 +450,136 @@ public void testOneOfPolymorphism() throws IOException { "ObjectSerializer resolves oneOf schemas"); } + @Test + public void testAnyOfPolymorphism() throws IOException { + 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()); + 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())); + + // anyOf models get their own dedicated interface, separate from oneOf. + Assert.assertTrue(files.containsKey("AnyOfInterface.php"), "Expected AnyOfInterface.php to be generated."); + + // An anyOf without a discriminator becomes a dispatcher exposing its member types. + List smoothie = Files.readAllLines(files.get("Smoothie.php").toPath()) + .stream().map(String::trim).collect(Collectors.toList()); + Assert.assertListContains(smoothie, a -> a.equals("class Smoothie implements AnyOfInterface"), + "Smoothie must implement AnyOfInterface"); + Assert.assertListContains(smoothie, a -> a.equals("public const DISCRIMINATOR = null;"), + "Smoothie has no discriminator"); + Assert.assertListContains(smoothie, a -> a.equals("'\\OpenAPI\\Client\\Model\\Apple',"), + "Smoothie lists Apple as a member type"); + Assert.assertListContains(smoothie, a -> a.equals("'\\OpenAPI\\Client\\Model\\Banana'"), + "Smoothie lists Banana as a member type"); + + // An anyOf with a discriminator exposes its property name and value mapping. + List reptile = Files.readAllLines(files.get("Reptile.php").toPath()) + .stream().map(String::trim).collect(Collectors.toList()); + Assert.assertListContains(reptile, a -> a.equals("class Reptile implements AnyOfInterface"), + "Reptile must implement AnyOfInterface"); + Assert.assertListContains(reptile, a -> a.equals("public const DISCRIMINATOR = 'reptileType';"), + "Reptile exposes its discriminator property by its wire (base) name"); + Assert.assertListContains(reptile, a -> a.equals("'lizard' => '\\OpenAPI\\Client\\Model\\Lizard',"), + "Reptile maps the lizard discriminator value"); + Assert.assertListContains(reptile, a -> a.equals("'snake' => '\\OpenAPI\\Client\\Model\\Snake'"), + "Reptile maps the snake discriminator value"); + + // The anyOf model doc documents its concrete member types, not the umbrella's flattened + // properties. + List reptileDoc = Files.readAllLines(files.get("Reptile.md").toPath()) + .stream().map(String::trim).collect(Collectors.toList()); + Assert.assertListContains(reptileDoc, + a -> a.contains("[**\\OpenAPI\\Client\\Model\\Lizard**](Lizard.md)"), + "Reptile doc links to its Lizard member"); + Assert.assertListContains(reptileDoc, + a -> a.contains("[**\\OpenAPI\\Client\\Model\\Snake**](Snake.md)"), + "Reptile doc links to its Snake member"); + } + + @Test + public void testAnyOfAsPropertyType() throws IOException { + 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()); + 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())); + + // Zoo references the anyOf schemas Reptile (with discriminator) and Smoothie (without) as + // property types, so the accessors must be hinted with the PHP union of the member types. + List zoo = Files.readAllLines(files.get("Zoo.php").toPath()) + .stream().map(String::trim).collect(Collectors.toList()); + + // A required anyOf property is hinted as the bare union (no `|null`). + Assert.assertListContains(zoo, + a -> a.equals("public function getFavoriteReptile(): \\OpenAPI\\Client\\Model\\Lizard|\\OpenAPI\\Client\\Model\\Snake"), + "required Reptile property getter is hinted as the member union"); + Assert.assertListContains(zoo, + a -> a.equals("public function setFavoriteReptile(\\OpenAPI\\Client\\Model\\Lizard|\\OpenAPI\\Client\\Model\\Snake $favorite_reptile): static"), + "required Reptile property setter is hinted as the member union"); + + // PHP forbids `?` on unions, so an optional anyOf property gains an explicit `|null` member. + Assert.assertListContains(zoo, + a -> a.equals("public function getDrink(): \\OpenAPI\\Client\\Model\\Apple|\\OpenAPI\\Client\\Model\\Banana|null"), + "optional Smoothie property getter appends |null to the union"); + Assert.assertListNotContains(zoo, + a -> a.contains("?\\OpenAPI\\Client\\Model\\Lizard") || a.contains("?\\OpenAPI\\Client\\Model\\Apple"), + "a union type must never use the nullable shorthand `?`"); + } + + @Test + public void testNestedComposedPropertyTypeIsFlattenedToLeaves() throws IOException { + 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()); + 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())); + + // Zoo.featuredCreature references Creature = anyOf(Mammal, Reptile), and BOTH members are + // themselves composed (Mammal oneOf Whale/Zebra, Reptile anyOf Lizard/Snake). + // deserializeComposed unwraps to the leaf instance, so the property union must flatten the + // intermediate composed types to their leaves: Whale|Zebra|Lizard|Snake. + List zoo = Files.readAllLines(files.get("Zoo.php").toPath()) + .stream().map(String::trim).collect(Collectors.toList()); + + Assert.assertListContains(zoo, + a -> a.equals("public function getFeaturedCreature(): \\OpenAPI\\Client\\Model\\Whale|\\OpenAPI\\Client\\Model\\Zebra|\\OpenAPI\\Client\\Model\\Lizard|\\OpenAPI\\Client\\Model\\Snake|null"), + "nested composed property union flattens intermediate composed members to their leaves"); + Assert.assertListNotContains(zoo, + a -> a.contains("FeaturedCreature") && (a.contains("\\OpenAPI\\Client\\Model\\Mammal") || a.contains("\\OpenAPI\\Client\\Model\\Reptile")), + "intermediate composed types (Mammal, Reptile) must not appear in the flattened union"); + } + @Test public void testOneOfAsPropertyType() throws IOException { 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 50f73a8c048b..8de20de092c1 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 @@ -2387,9 +2387,10 @@ components: Zoo: type: object - description: A model that uses oneOf schemas as property types. + description: A model that uses oneOf and anyOf schemas as property types. required: - favoriteMammal + - favoriteReptile properties: favoriteMammal: # required, discriminated oneOf -> non-nullable PHP union @@ -2405,4 +2406,57 @@ components: type: array items: $ref: '#/components/schemas/Mammal' + favoriteReptile: + # required, discriminated anyOf -> non-nullable PHP union + $ref: '#/components/schemas/Reptile' + drink: + # optional, discriminator-less anyOf -> nullable PHP union + $ref: '#/components/schemas/Smoothie' + featuredCreature: + # optional anyOf whose members are THEMSELVES composed (Mammal, Reptile) -> the union + # must flatten transitively to their leaves, not the intermediate composed types + $ref: '#/components/schemas/Creature' + + Lizard: + type: object + required: + - reptileType + properties: + lovesRocks: + type: boolean + reptileType: + type: string + + Snake: + type: object + required: + - reptileType + properties: + venomous: + type: boolean + reptileType: + type: string + + Reptile: + anyOf: + - $ref: '#/components/schemas/Lizard' + - $ref: '#/components/schemas/Snake' + discriminator: + propertyName: reptileType + mapping: + lizard: '#/components/schemas/Lizard' + snake: '#/components/schemas/Snake' + + Smoothie: + anyOf: + - $ref: '#/components/schemas/Apple' + - $ref: '#/components/schemas/Banana' + + Creature: + # anyOf whose members are themselves composed: Mammal (oneOf Whale/Zebra) and Reptile + # (anyOf Lizard/Snake). A property referencing Creature must flatten transitively to the + # leaves (Whale|Zebra|Lizard|Snake), since the deserializer unwraps to the leaf instance. + anyOf: + - $ref: '#/components/schemas/Mammal' + - $ref: '#/components/schemas/Reptile' diff --git a/samples/client/echo_api/php-nextgen-streaming/.openapi-generator/FILES b/samples/client/echo_api/php-nextgen-streaming/.openapi-generator/FILES index 335f2a415a74..bcb3e98ad2b3 100644 --- a/samples/client/echo_api/php-nextgen-streaming/.openapi-generator/FILES +++ b/samples/client/echo_api/php-nextgen-streaming/.openapi-generator/FILES @@ -34,6 +34,7 @@ src/ApiException.php src/Configuration.php src/FormDataProcessor.php src/HeaderSelector.php +src/Model/AnyOfInterface.php src/Model/Bird.php src/Model/Category.php src/Model/DataQuery.php diff --git a/samples/client/echo_api/php-nextgen-streaming/src/Model/AnyOfInterface.php b/samples/client/echo_api/php-nextgen-streaming/src/Model/AnyOfInterface.php new file mode 100644 index 000000000000..7a0dfa0a0c8b --- /dev/null +++ b/samples/client/echo_api/php-nextgen-streaming/src/Model/AnyOfInterface.php @@ -0,0 +1,66 @@ + + */ + public static function getAnyOfDiscriminatorMappings(): array; +} diff --git a/samples/client/echo_api/php-nextgen-streaming/src/ObjectSerializer.php b/samples/client/echo_api/php-nextgen-streaming/src/ObjectSerializer.php index 6ddb0e57e263..ac1e0b6bec78 100644 --- a/samples/client/echo_api/php-nextgen-streaming/src/ObjectSerializer.php +++ b/samples/client/echo_api/php-nextgen-streaming/src/ObjectSerializer.php @@ -501,14 +501,18 @@ public static function deserialize(mixed $data, string $class, ?array $httpHeade } else { $data = is_string($data) ? json_decode($data) : $data; - if (is_array($data)) { - $data = (object) $data; - } - - // A oneOf schema is not a value object: resolve the data to one of its member types. + // A oneOf/anyOf schema is not a value object: dispatch here, after json_decode (so + // members get the decoded value) but before the array-to-object cast (which breaks arrays). if (is_subclass_of($class, '\OpenAPI\Client\Model\OneOfInterface')) { return self::deserializeOneOf($data, $class, $httpHeaders); } + if (is_subclass_of($class, '\OpenAPI\Client\Model\AnyOfInterface')) { + return self::deserializeAnyOf($data, $class, $httpHeaders); + } + + if (is_array($data)) { + $data = (object) $data; + } // If a discriminator is defined and points to a valid subclass, use it. $discriminator = $class::DISCRIMINATOR; @@ -552,7 +556,7 @@ public static function deserialize(mixed $data, string $class, ?array $httpHeade * Otherwise each member type is tried in turn and the first one that yields a valid model * (or a non-null primitive) is returned. * - * @param mixed $data the data already decoded to an object + * @param mixed $data the decoded data to resolve to a member type * @param string $class a class name implementing OneOfInterface * @param string[]|null $httpHeaders HTTP headers * @@ -560,11 +564,16 @@ public static function deserialize(mixed $data, string $class, ?array $httpHeade */ private static function deserializeOneOf(mixed $data, string $class, ?array $httpHeaders): mixed { + // $data is already decoded (see deserialize()). Read the discriminator from an object view + // of it, but deserialize each member from the original $data so array and scalar members + // keep their own type handling instead of being coerced to an object here. + $probe = is_array($data) ? (object) $data : $data; + $discriminator = $class::getOneOfDiscriminator(); - if ($discriminator !== null && isset($data->{$discriminator}) && is_string($data->{$discriminator})) { + if ($discriminator !== null && is_object($probe) && isset($probe->{$discriminator}) && is_string($probe->{$discriminator})) { $mappings = $class::getOneOfDiscriminatorMappings(); - if (isset($mappings[$data->{$discriminator}])) { - return self::deserialize($data, $mappings[$data->{$discriminator}], $httpHeaders); + if (isset($mappings[$probe->{$discriminator}])) { + return self::deserialize($data, $mappings[$probe->{$discriminator}], $httpHeaders); } } @@ -572,7 +581,9 @@ private static function deserializeOneOf(mixed $data, string $class, ?array $htt try { $instance = self::deserialize($data, $type, $httpHeaders); } catch (\Throwable $e) { - // The data does not match this member type, try the next one. + // Any failure means the data does not match this member type; try the next one. + // The broad catch is intentional: a real failure is not hidden, since the loop + // throws below when no member type matches. continue; } @@ -588,6 +599,56 @@ private static function deserializeOneOf(mixed $data, string $class, ?array $htt throw new \InvalidArgumentException(sprintf('No matching schema in oneOf %s for the given data', $class)); } + /** + * Deserialize data into one of the member types of an `anyOf` schema. + * + * When the schema declares a discriminator, its value selects the member type directly. + * Otherwise each member type is tried in turn and the first one that yields a valid model + * (or a non-null primitive) is returned. + * + * @param mixed $data the decoded data to resolve to a member type + * @param string $class a class name implementing AnyOfInterface + * @param string[]|null $httpHeaders HTTP headers + * + * @return mixed an instance of one of the `anyOf` member types + */ + private static function deserializeAnyOf(mixed $data, string $class, ?array $httpHeaders): mixed + { + // $data is already decoded (see deserialize()). Read the discriminator from an object view + // of it, but deserialize each member from the original $data so array and scalar members + // keep their own type handling instead of being coerced to an object here. + $probe = is_array($data) ? (object) $data : $data; + + $discriminator = $class::getAnyOfDiscriminator(); + if ($discriminator !== null && is_object($probe) && isset($probe->{$discriminator}) && is_string($probe->{$discriminator})) { + $mappings = $class::getAnyOfDiscriminatorMappings(); + if (isset($mappings[$probe->{$discriminator}])) { + return self::deserialize($data, $mappings[$probe->{$discriminator}], $httpHeaders); + } + } + + foreach ($class::getAnyOfTypes() as $type) { + try { + $instance = self::deserialize($data, $type, $httpHeaders); + } catch (\Throwable $e) { + // Any failure means the data does not match this member type; try the next one. + // The broad catch is intentional: a real failure is not hidden, since the loop + // throws below when no member type matches. + continue; + } + + if ($instance instanceof ModelInterface) { + if ($instance->valid()) { + return $instance; + } + } elseif ($instance !== null) { + return $instance; + } + } + + throw new \InvalidArgumentException(sprintf('No matching schema in anyOf %s for the given data', $class)); + } + /** * Build a query string from an array of key value pairs. * diff --git a/samples/client/echo_api/php-nextgen/.openapi-generator/FILES b/samples/client/echo_api/php-nextgen/.openapi-generator/FILES index 335f2a415a74..bcb3e98ad2b3 100644 --- a/samples/client/echo_api/php-nextgen/.openapi-generator/FILES +++ b/samples/client/echo_api/php-nextgen/.openapi-generator/FILES @@ -34,6 +34,7 @@ src/ApiException.php src/Configuration.php src/FormDataProcessor.php src/HeaderSelector.php +src/Model/AnyOfInterface.php src/Model/Bird.php src/Model/Category.php src/Model/DataQuery.php diff --git a/samples/client/echo_api/php-nextgen/src/Model/AnyOfInterface.php b/samples/client/echo_api/php-nextgen/src/Model/AnyOfInterface.php new file mode 100644 index 000000000000..7a0dfa0a0c8b --- /dev/null +++ b/samples/client/echo_api/php-nextgen/src/Model/AnyOfInterface.php @@ -0,0 +1,66 @@ + + */ + public static function getAnyOfDiscriminatorMappings(): array; +} diff --git a/samples/client/echo_api/php-nextgen/src/ObjectSerializer.php b/samples/client/echo_api/php-nextgen/src/ObjectSerializer.php index 6ddb0e57e263..ac1e0b6bec78 100644 --- a/samples/client/echo_api/php-nextgen/src/ObjectSerializer.php +++ b/samples/client/echo_api/php-nextgen/src/ObjectSerializer.php @@ -501,14 +501,18 @@ public static function deserialize(mixed $data, string $class, ?array $httpHeade } else { $data = is_string($data) ? json_decode($data) : $data; - if (is_array($data)) { - $data = (object) $data; - } - - // A oneOf schema is not a value object: resolve the data to one of its member types. + // A oneOf/anyOf schema is not a value object: dispatch here, after json_decode (so + // members get the decoded value) but before the array-to-object cast (which breaks arrays). if (is_subclass_of($class, '\OpenAPI\Client\Model\OneOfInterface')) { return self::deserializeOneOf($data, $class, $httpHeaders); } + if (is_subclass_of($class, '\OpenAPI\Client\Model\AnyOfInterface')) { + return self::deserializeAnyOf($data, $class, $httpHeaders); + } + + if (is_array($data)) { + $data = (object) $data; + } // If a discriminator is defined and points to a valid subclass, use it. $discriminator = $class::DISCRIMINATOR; @@ -552,7 +556,7 @@ public static function deserialize(mixed $data, string $class, ?array $httpHeade * Otherwise each member type is tried in turn and the first one that yields a valid model * (or a non-null primitive) is returned. * - * @param mixed $data the data already decoded to an object + * @param mixed $data the decoded data to resolve to a member type * @param string $class a class name implementing OneOfInterface * @param string[]|null $httpHeaders HTTP headers * @@ -560,11 +564,16 @@ public static function deserialize(mixed $data, string $class, ?array $httpHeade */ private static function deserializeOneOf(mixed $data, string $class, ?array $httpHeaders): mixed { + // $data is already decoded (see deserialize()). Read the discriminator from an object view + // of it, but deserialize each member from the original $data so array and scalar members + // keep their own type handling instead of being coerced to an object here. + $probe = is_array($data) ? (object) $data : $data; + $discriminator = $class::getOneOfDiscriminator(); - if ($discriminator !== null && isset($data->{$discriminator}) && is_string($data->{$discriminator})) { + if ($discriminator !== null && is_object($probe) && isset($probe->{$discriminator}) && is_string($probe->{$discriminator})) { $mappings = $class::getOneOfDiscriminatorMappings(); - if (isset($mappings[$data->{$discriminator}])) { - return self::deserialize($data, $mappings[$data->{$discriminator}], $httpHeaders); + if (isset($mappings[$probe->{$discriminator}])) { + return self::deserialize($data, $mappings[$probe->{$discriminator}], $httpHeaders); } } @@ -572,7 +581,9 @@ private static function deserializeOneOf(mixed $data, string $class, ?array $htt try { $instance = self::deserialize($data, $type, $httpHeaders); } catch (\Throwable $e) { - // The data does not match this member type, try the next one. + // Any failure means the data does not match this member type; try the next one. + // The broad catch is intentional: a real failure is not hidden, since the loop + // throws below when no member type matches. continue; } @@ -588,6 +599,56 @@ private static function deserializeOneOf(mixed $data, string $class, ?array $htt throw new \InvalidArgumentException(sprintf('No matching schema in oneOf %s for the given data', $class)); } + /** + * Deserialize data into one of the member types of an `anyOf` schema. + * + * When the schema declares a discriminator, its value selects the member type directly. + * Otherwise each member type is tried in turn and the first one that yields a valid model + * (or a non-null primitive) is returned. + * + * @param mixed $data the decoded data to resolve to a member type + * @param string $class a class name implementing AnyOfInterface + * @param string[]|null $httpHeaders HTTP headers + * + * @return mixed an instance of one of the `anyOf` member types + */ + private static function deserializeAnyOf(mixed $data, string $class, ?array $httpHeaders): mixed + { + // $data is already decoded (see deserialize()). Read the discriminator from an object view + // of it, but deserialize each member from the original $data so array and scalar members + // keep their own type handling instead of being coerced to an object here. + $probe = is_array($data) ? (object) $data : $data; + + $discriminator = $class::getAnyOfDiscriminator(); + if ($discriminator !== null && is_object($probe) && isset($probe->{$discriminator}) && is_string($probe->{$discriminator})) { + $mappings = $class::getAnyOfDiscriminatorMappings(); + if (isset($mappings[$probe->{$discriminator}])) { + return self::deserialize($data, $mappings[$probe->{$discriminator}], $httpHeaders); + } + } + + foreach ($class::getAnyOfTypes() as $type) { + try { + $instance = self::deserialize($data, $type, $httpHeaders); + } catch (\Throwable $e) { + // Any failure means the data does not match this member type; try the next one. + // The broad catch is intentional: a real failure is not hidden, since the loop + // throws below when no member type matches. + continue; + } + + if ($instance instanceof ModelInterface) { + if ($instance->valid()) { + return $instance; + } + } elseif ($instance !== null) { + return $instance; + } + } + + throw new \InvalidArgumentException(sprintf('No matching schema in anyOf %s for the given data', $class)); + } + /** * Build a query string from an array of key value pairs. * 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 3ef9d6be392b..ad56f3439435 100644 --- a/samples/client/petstore/php-nextgen/OpenAPIClient-php/.openapi-generator/FILES +++ b/samples/client/petstore/php-nextgen/OpenAPIClient-php/.openapi-generator/FILES @@ -26,6 +26,7 @@ docs/Model/Category.md docs/Model/ChildWithNullable.md docs/Model/ClassModel.md docs/Model/Client.md +docs/Model/Creature.md docs/Model/DeprecatedObject.md docs/Model/DiscriminatorBase.md docs/Model/DiscriminatorChild.md @@ -45,6 +46,7 @@ docs/Model/FormatTest.md docs/Model/Fruit.md docs/Model/HasOnlyReadOnly.md docs/Model/HealthCheckResult.md +docs/Model/Lizard.md docs/Model/Mammal.md docs/Model/MapTest.md docs/Model/MixedPropertiesAndAdditionalPropertiesClass.md @@ -65,7 +67,10 @@ docs/Model/OuterObjectWithEnumProperty.md docs/Model/ParentWithNullable.md docs/Model/Pet.md docs/Model/ReadOnlyFirst.md +docs/Model/Reptile.md docs/Model/SingleRefType.md +docs/Model/Smoothie.md +docs/Model/Snake.md docs/Model/SpecialModelName.md docs/Model/Tag.md docs/Model/TestInlineFreeformAdditionalPropertiesRequest.md @@ -89,6 +94,7 @@ src/HeaderSelector.php src/Model/AdditionalPropertiesClass.php src/Model/AllOfWithSingleRef.php src/Model/Animal.php +src/Model/AnyOfInterface.php src/Model/ApiResponse.php src/Model/Apple.php src/Model/ArrayOfArrayOfNumberOnly.php @@ -120,6 +126,7 @@ src/Model/FormatTest.php src/Model/Fruit.php src/Model/HasOnlyReadOnly.php src/Model/HealthCheckResult.php +src/Model/Lizard.php src/Model/Mammal.php src/Model/MapTest.php src/Model/MixedPropertiesAndAdditionalPropertiesClass.php @@ -142,7 +149,10 @@ src/Model/OuterObjectWithEnumProperty.php src/Model/ParentWithNullable.php src/Model/Pet.php src/Model/ReadOnlyFirst.php +src/Model/Reptile.php src/Model/SingleRefType.php +src/Model/Smoothie.php +src/Model/Snake.php src/Model/SpecialModelName.php src/Model/Tag.php src/Model/TestInlineFreeformAdditionalPropertiesRequest.php diff --git a/samples/client/petstore/php-nextgen/OpenAPIClient-php/README.md b/samples/client/petstore/php-nextgen/OpenAPIClient-php/README.md index 74b41b5945da..e85c70117370 100644 --- a/samples/client/petstore/php-nextgen/OpenAPIClient-php/README.md +++ b/samples/client/petstore/php-nextgen/OpenAPIClient-php/README.md @@ -144,6 +144,7 @@ Class | Method | HTTP request | Description - [ChildWithNullable](docs/Model/ChildWithNullable.md) - [ClassModel](docs/Model/ClassModel.md) - [Client](docs/Model/Client.md) +- [Creature](docs/Model/Creature.md) - [DeprecatedObject](docs/Model/DeprecatedObject.md) - [DiscriminatorBase](docs/Model/DiscriminatorBase.md) - [DiscriminatorChild](docs/Model/DiscriminatorChild.md) @@ -163,6 +164,7 @@ Class | Method | HTTP request | Description - [Fruit](docs/Model/Fruit.md) - [HasOnlyReadOnly](docs/Model/HasOnlyReadOnly.md) - [HealthCheckResult](docs/Model/HealthCheckResult.md) +- [Lizard](docs/Model/Lizard.md) - [Mammal](docs/Model/Mammal.md) - [MapTest](docs/Model/MapTest.md) - [MixedPropertiesAndAdditionalPropertiesClass](docs/Model/MixedPropertiesAndAdditionalPropertiesClass.md) @@ -183,7 +185,10 @@ Class | Method | HTTP request | Description - [ParentWithNullable](docs/Model/ParentWithNullable.md) - [Pet](docs/Model/Pet.md) - [ReadOnlyFirst](docs/Model/ReadOnlyFirst.md) +- [Reptile](docs/Model/Reptile.md) - [SingleRefType](docs/Model/SingleRefType.md) +- [Smoothie](docs/Model/Smoothie.md) +- [Snake](docs/Model/Snake.md) - [SpecialModelName](docs/Model/SpecialModelName.md) - [Tag](docs/Model/Tag.md) - [TestInlineFreeformAdditionalPropertiesRequest](docs/Model/TestInlineFreeformAdditionalPropertiesRequest.md) diff --git a/samples/client/petstore/php-nextgen/OpenAPIClient-php/docs/Model/Creature.md b/samples/client/petstore/php-nextgen/OpenAPIClient-php/docs/Model/Creature.md new file mode 100644 index 000000000000..97bc775e0009 --- /dev/null +++ b/samples/client/petstore/php-nextgen/OpenAPIClient-php/docs/Model/Creature.md @@ -0,0 +1,11 @@ +# Creature + +This model is an `anyOf` wrapper: a value is at least one of the member types listed below. +It is never instantiated directly — use one of the concrete types. + +## anyOf + +- [**\OpenAPI\Client\Model\Mammal**](Mammal.md) +- [**\OpenAPI\Client\Model\Reptile**](Reptile.md) + +[[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/Lizard.md b/samples/client/petstore/php-nextgen/OpenAPIClient-php/docs/Model/Lizard.md new file mode 100644 index 000000000000..6c14150b507e --- /dev/null +++ b/samples/client/petstore/php-nextgen/OpenAPIClient-php/docs/Model/Lizard.md @@ -0,0 +1,10 @@ +# Lizard + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**loves_rocks** | **bool** | | [optional] +**reptile_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/Reptile.md b/samples/client/petstore/php-nextgen/OpenAPIClient-php/docs/Model/Reptile.md new file mode 100644 index 000000000000..667e2bf92584 --- /dev/null +++ b/samples/client/petstore/php-nextgen/OpenAPIClient-php/docs/Model/Reptile.md @@ -0,0 +1,13 @@ +# Reptile + +This model is an `anyOf` wrapper: a value is at least one of the member types listed below. +It is never instantiated directly — use one of the concrete types. + +## anyOf + +- [**\OpenAPI\Client\Model\Lizard**](Lizard.md) +- [**\OpenAPI\Client\Model\Snake**](Snake.md) + +The concrete type is selected by the `reptile_type` discriminator property. + +[[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/Smoothie.md b/samples/client/petstore/php-nextgen/OpenAPIClient-php/docs/Model/Smoothie.md new file mode 100644 index 000000000000..6227252e42e8 --- /dev/null +++ b/samples/client/petstore/php-nextgen/OpenAPIClient-php/docs/Model/Smoothie.md @@ -0,0 +1,11 @@ +# Smoothie + +This model is an `anyOf` wrapper: a value is at least one of the member types listed below. +It is never instantiated directly — use one of the concrete types. + +## anyOf + +- [**\OpenAPI\Client\Model\Apple**](Apple.md) +- [**\OpenAPI\Client\Model\Banana**](Banana.md) + +[[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/Snake.md b/samples/client/petstore/php-nextgen/OpenAPIClient-php/docs/Model/Snake.md new file mode 100644 index 000000000000..c3c26f377506 --- /dev/null +++ b/samples/client/petstore/php-nextgen/OpenAPIClient-php/docs/Model/Snake.md @@ -0,0 +1,10 @@ +# Snake + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**venomous** | **bool** | | [optional] +**reptile_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/Zoo.md b/samples/client/petstore/php-nextgen/OpenAPIClient-php/docs/Model/Zoo.md index a8007abf86c0..2c323f5746f2 100644 --- a/samples/client/petstore/php-nextgen/OpenAPIClient-php/docs/Model/Zoo.md +++ b/samples/client/petstore/php-nextgen/OpenAPIClient-php/docs/Model/Zoo.md @@ -8,5 +8,8 @@ Name | Type | Description | Notes **optional_mammal** | [**\OpenAPI\Client\Model\Mammal**](Mammal.md) | | [optional] **snack** | [**\OpenAPI\Client\Model\Fruit**](Fruit.md) | | [optional] **mammals** | [**\OpenAPI\Client\Model\Mammal[]**](Mammal.md) | | [optional] +**favorite_reptile** | [**\OpenAPI\Client\Model\Reptile**](Reptile.md) | | +**drink** | [**\OpenAPI\Client\Model\Smoothie**](Smoothie.md) | | [optional] +**featured_creature** | [**\OpenAPI\Client\Model\Creature**](Creature.md) | | [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/AnyOfInterface.php b/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Model/AnyOfInterface.php new file mode 100644 index 000000000000..7377e47905a4 --- /dev/null +++ b/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Model/AnyOfInterface.php @@ -0,0 +1,65 @@ + + */ + public static function getAnyOfDiscriminatorMappings(): array; +} diff --git a/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Model/Creature.php b/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Model/Creature.php new file mode 100644 index 000000000000..c0bf8b98ff23 --- /dev/null +++ b/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Model/Creature.php @@ -0,0 +1,83 @@ + + */ +class Lizard implements ModelInterface, ArrayAccess, JsonSerializable +{ + public const DISCRIMINATOR = null; + + /** + * The original name of the model. + * + * @var string + */ + protected static string $openAPIModelName = 'Lizard'; + + /** + * Array of property to type mappings. Used for (de)serialization + * + * @var array + */ + protected static array $openAPITypes = [ + 'loves_rocks' => 'bool', + 'reptile_type' => 'string' + ]; + + /** + * Array of property to format mappings. Used for (de)serialization + * + * @var array + */ + protected static array $openAPIFormats = [ + 'loves_rocks' => null, + 'reptile_type' => null + ]; + + /** + * Array of nullable properties. Used for (de)serialization + * + * @var array + */ + protected static array $openAPINullables = [ + 'loves_rocks' => false, + 'reptile_type' => false + ]; + + /** + * If a nullable field gets set to null, insert it here + * + * @var array + */ + protected array $openAPINullablesSetToNull = []; + + /** + * {@inheritdoc} + */ + public static function openAPITypes(): array + { + return self::$openAPITypes; + } + + /** + * {@inheritdoc} + */ + 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; + } + + /** + * {@inheritdoc} + */ + public static function isNullable(string $property): bool + { + return self::openAPINullables()[$property] ?? false; + } + + /** + * {@inheritdoc} + */ + 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 = [ + 'loves_rocks' => 'lovesRocks', + 'reptile_type' => 'reptileType' + ]; + + /** + * Array of attributes to setter functions (for deserialization of responses) + * + * @var array + */ + protected static array $setters = [ + 'loves_rocks' => 'setLovesRocks', + 'reptile_type' => 'setReptileType' + ]; + + /** + * Array of attributes to getter functions (for serialization of requests) + * + * @var array + */ + protected static array $getters = [ + 'loves_rocks' => 'getLovesRocks', + 'reptile_type' => 'getReptileType' + ]; + + /** + * {@inheritdoc} + */ + public static function attributeMap(): array + { + return self::$attributeMap; + } + + /** + * {@inheritdoc} + */ + public static function setters(): array + { + return self::$setters; + } + + /** + * {@inheritdoc} + */ + public static function getters(): array + { + return self::$getters; + } + + /** + * {@inheritdoc} + */ + public function getModelName(): string + { + return self::$openAPIModelName; + } + + + /** + * 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) + { + $this->setIfExists('loves_rocks', $data ?? [], null); + $this->setIfExists('reptile_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; + } + + /** + * {@inheritdoc} + */ + public function listInvalidProperties(): array + { + $invalidProperties = []; + + if ($this->container['reptile_type'] === null) { + $invalidProperties[] = "'reptile_type' can't be null"; + } + return $invalidProperties; + } + + /** + * {@inheritdoc} + */ + public function valid(): bool + { + return count($this->listInvalidProperties()) === 0; + } + + + /** + * Gets loves_rocks + * + * @return bool|null + */ + public function getLovesRocks(): ?bool + { + return $this->container['loves_rocks']; + } + + /** + * Sets loves_rocks + * + * @param bool|null $loves_rocks loves_rocks + * + * @return $this + */ + public function setLovesRocks(?bool $loves_rocks): static + { + if (is_null($loves_rocks)) { + throw new InvalidArgumentException('non-nullable loves_rocks cannot be null'); + } + $this->container['loves_rocks'] = $loves_rocks; + + return $this; + } + + /** + * Gets reptile_type + * + * @return string + */ + public function getReptileType(): string + { + return $this->container['reptile_type']; + } + + /** + * Sets reptile_type + * + * @param string $reptile_type reptile_type + * + * @return $this + */ + public function setReptileType(string $reptile_type): static + { + if (is_null($reptile_type)) { + throw new InvalidArgumentException('non-nullable reptile_type cannot be null'); + } + $this->container['reptile_type'] = $reptile_type; + + return $this; + } + /** + * Returns true if offset exists. False otherwise. + * + * @param int|string $offset Offset + * + * @return boolean + */ + public function offsetExists(mixed $offset): bool + { + return isset($this->container[$offset]); + } + + /** + * Gets offset. + * + * @param int|string $offset Offset + * + * @return mixed|null + */ + #[ReturnTypeWillChange] + public function offsetGet(mixed $offset): mixed + { + return $this->container[$offset] ?? null; + } + + /** + * Sets value based on offset. + * + * @param int|string|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 int|string $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/Reptile.php b/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Model/Reptile.php new file mode 100644 index 000000000000..179a9d328e80 --- /dev/null +++ b/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Model/Reptile.php @@ -0,0 +1,84 @@ + '\OpenAPI\Client\Model\Lizard', + 'snake' => '\OpenAPI\Client\Model\Snake' + ]; + } +} + + diff --git a/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Model/Smoothie.php b/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Model/Smoothie.php new file mode 100644 index 000000000000..4f66c044d706 --- /dev/null +++ b/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Model/Smoothie.php @@ -0,0 +1,83 @@ + + */ +class Snake implements ModelInterface, ArrayAccess, JsonSerializable +{ + public const DISCRIMINATOR = null; + + /** + * The original name of the model. + * + * @var string + */ + protected static string $openAPIModelName = 'Snake'; + + /** + * Array of property to type mappings. Used for (de)serialization + * + * @var array + */ + protected static array $openAPITypes = [ + 'venomous' => 'bool', + 'reptile_type' => 'string' + ]; + + /** + * Array of property to format mappings. Used for (de)serialization + * + * @var array + */ + protected static array $openAPIFormats = [ + 'venomous' => null, + 'reptile_type' => null + ]; + + /** + * Array of nullable properties. Used for (de)serialization + * + * @var array + */ + protected static array $openAPINullables = [ + 'venomous' => false, + 'reptile_type' => false + ]; + + /** + * If a nullable field gets set to null, insert it here + * + * @var array + */ + protected array $openAPINullablesSetToNull = []; + + /** + * {@inheritdoc} + */ + public static function openAPITypes(): array + { + return self::$openAPITypes; + } + + /** + * {@inheritdoc} + */ + 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; + } + + /** + * {@inheritdoc} + */ + public static function isNullable(string $property): bool + { + return self::openAPINullables()[$property] ?? false; + } + + /** + * {@inheritdoc} + */ + 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 = [ + 'venomous' => 'venomous', + 'reptile_type' => 'reptileType' + ]; + + /** + * Array of attributes to setter functions (for deserialization of responses) + * + * @var array + */ + protected static array $setters = [ + 'venomous' => 'setVenomous', + 'reptile_type' => 'setReptileType' + ]; + + /** + * Array of attributes to getter functions (for serialization of requests) + * + * @var array + */ + protected static array $getters = [ + 'venomous' => 'getVenomous', + 'reptile_type' => 'getReptileType' + ]; + + /** + * {@inheritdoc} + */ + public static function attributeMap(): array + { + return self::$attributeMap; + } + + /** + * {@inheritdoc} + */ + public static function setters(): array + { + return self::$setters; + } + + /** + * {@inheritdoc} + */ + public static function getters(): array + { + return self::$getters; + } + + /** + * {@inheritdoc} + */ + public function getModelName(): string + { + return self::$openAPIModelName; + } + + + /** + * 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) + { + $this->setIfExists('venomous', $data ?? [], null); + $this->setIfExists('reptile_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; + } + + /** + * {@inheritdoc} + */ + public function listInvalidProperties(): array + { + $invalidProperties = []; + + if ($this->container['reptile_type'] === null) { + $invalidProperties[] = "'reptile_type' can't be null"; + } + return $invalidProperties; + } + + /** + * {@inheritdoc} + */ + public function valid(): bool + { + return count($this->listInvalidProperties()) === 0; + } + + + /** + * Gets venomous + * + * @return bool|null + */ + public function getVenomous(): ?bool + { + return $this->container['venomous']; + } + + /** + * Sets venomous + * + * @param bool|null $venomous venomous + * + * @return $this + */ + public function setVenomous(?bool $venomous): static + { + if (is_null($venomous)) { + throw new InvalidArgumentException('non-nullable venomous cannot be null'); + } + $this->container['venomous'] = $venomous; + + return $this; + } + + /** + * Gets reptile_type + * + * @return string + */ + public function getReptileType(): string + { + return $this->container['reptile_type']; + } + + /** + * Sets reptile_type + * + * @param string $reptile_type reptile_type + * + * @return $this + */ + public function setReptileType(string $reptile_type): static + { + if (is_null($reptile_type)) { + throw new InvalidArgumentException('non-nullable reptile_type cannot be null'); + } + $this->container['reptile_type'] = $reptile_type; + + return $this; + } + /** + * Returns true if offset exists. False otherwise. + * + * @param int|string $offset Offset + * + * @return boolean + */ + public function offsetExists(mixed $offset): bool + { + return isset($this->container[$offset]); + } + + /** + * Gets offset. + * + * @param int|string $offset Offset + * + * @return mixed|null + */ + #[ReturnTypeWillChange] + public function offsetGet(mixed $offset): mixed + { + return $this->container[$offset] ?? null; + } + + /** + * Sets value based on offset. + * + * @param int|string|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 int|string $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/Zoo.php b/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Model/Zoo.php index c9c3392a4b73..13e501054428 100644 --- a/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Model/Zoo.php +++ b/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Model/Zoo.php @@ -36,7 +36,7 @@ /** * Zoo Class Doc Comment * - * @description A model that uses oneOf schemas as property types. + * @description A model that uses oneOf and anyOf schemas as property types. * @package OpenAPI\Client * @author OpenAPI Generator team * @link https://openapi-generator.tech @@ -62,7 +62,10 @@ class Zoo implements ModelInterface, ArrayAccess, JsonSerializable 'favorite_mammal' => '\OpenAPI\Client\Model\Mammal', 'optional_mammal' => '\OpenAPI\Client\Model\Mammal', 'snack' => '\OpenAPI\Client\Model\Fruit', - 'mammals' => '\OpenAPI\Client\Model\Mammal[]' + 'mammals' => '\OpenAPI\Client\Model\Mammal[]', + 'favorite_reptile' => '\OpenAPI\Client\Model\Reptile', + 'drink' => '\OpenAPI\Client\Model\Smoothie', + 'featured_creature' => '\OpenAPI\Client\Model\Creature' ]; /** @@ -74,7 +77,10 @@ class Zoo implements ModelInterface, ArrayAccess, JsonSerializable 'favorite_mammal' => null, 'optional_mammal' => null, 'snack' => null, - 'mammals' => null + 'mammals' => null, + 'favorite_reptile' => null, + 'drink' => null, + 'featured_creature' => null ]; /** @@ -86,7 +92,10 @@ class Zoo implements ModelInterface, ArrayAccess, JsonSerializable 'favorite_mammal' => false, 'optional_mammal' => false, 'snack' => false, - 'mammals' => false + 'mammals' => false, + 'favorite_reptile' => false, + 'drink' => false, + 'featured_creature' => false ]; /** @@ -168,7 +177,10 @@ public function isNullableSetToNull(string $property): bool 'favorite_mammal' => 'favoriteMammal', 'optional_mammal' => 'optionalMammal', 'snack' => 'snack', - 'mammals' => 'mammals' + 'mammals' => 'mammals', + 'favorite_reptile' => 'favoriteReptile', + 'drink' => 'drink', + 'featured_creature' => 'featuredCreature' ]; /** @@ -180,7 +192,10 @@ public function isNullableSetToNull(string $property): bool 'favorite_mammal' => 'setFavoriteMammal', 'optional_mammal' => 'setOptionalMammal', 'snack' => 'setSnack', - 'mammals' => 'setMammals' + 'mammals' => 'setMammals', + 'favorite_reptile' => 'setFavoriteReptile', + 'drink' => 'setDrink', + 'featured_creature' => 'setFeaturedCreature' ]; /** @@ -192,7 +207,10 @@ public function isNullableSetToNull(string $property): bool 'favorite_mammal' => 'getFavoriteMammal', 'optional_mammal' => 'getOptionalMammal', 'snack' => 'getSnack', - 'mammals' => 'getMammals' + 'mammals' => 'getMammals', + 'favorite_reptile' => 'getFavoriteReptile', + 'drink' => 'getDrink', + 'featured_creature' => 'getFeaturedCreature' ]; /** @@ -246,6 +264,9 @@ public function __construct(?array $data = null) $this->setIfExists('optional_mammal', $data ?? [], null); $this->setIfExists('snack', $data ?? [], null); $this->setIfExists('mammals', $data ?? [], null); + $this->setIfExists('favorite_reptile', $data ?? [], null); + $this->setIfExists('drink', $data ?? [], null); + $this->setIfExists('featured_creature', $data ?? [], null); } /** @@ -276,6 +297,9 @@ public function listInvalidProperties(): array if ($this->container['favorite_mammal'] === null) { $invalidProperties[] = "'favorite_mammal' can't be null"; } + if ($this->container['favorite_reptile'] === null) { + $invalidProperties[] = "'favorite_reptile' can't be null"; + } return $invalidProperties; } @@ -395,6 +419,87 @@ public function setMammals(?array $mammals): static return $this; } + + /** + * Gets favorite_reptile + * + * @return \OpenAPI\Client\Model\Lizard|\OpenAPI\Client\Model\Snake + */ + public function getFavoriteReptile(): \OpenAPI\Client\Model\Lizard|\OpenAPI\Client\Model\Snake + { + return $this->container['favorite_reptile']; + } + + /** + * Sets favorite_reptile + * + * @param \OpenAPI\Client\Model\Lizard|\OpenAPI\Client\Model\Snake $favorite_reptile favorite_reptile + * + * @return $this + */ + public function setFavoriteReptile(\OpenAPI\Client\Model\Lizard|\OpenAPI\Client\Model\Snake $favorite_reptile): static + { + if (is_null($favorite_reptile)) { + throw new InvalidArgumentException('non-nullable favorite_reptile cannot be null'); + } + $this->container['favorite_reptile'] = $favorite_reptile; + + return $this; + } + + /** + * Gets drink + * + * @return \OpenAPI\Client\Model\Apple|\OpenAPI\Client\Model\Banana|null + */ + public function getDrink(): \OpenAPI\Client\Model\Apple|\OpenAPI\Client\Model\Banana|null + { + return $this->container['drink']; + } + + /** + * Sets drink + * + * @param \OpenAPI\Client\Model\Apple|\OpenAPI\Client\Model\Banana|null $drink drink + * + * @return $this + */ + public function setDrink(\OpenAPI\Client\Model\Apple|\OpenAPI\Client\Model\Banana|null $drink): static + { + if (is_null($drink)) { + throw new InvalidArgumentException('non-nullable drink cannot be null'); + } + $this->container['drink'] = $drink; + + return $this; + } + + /** + * Gets featured_creature + * + * @return \OpenAPI\Client\Model\Whale|\OpenAPI\Client\Model\Zebra|\OpenAPI\Client\Model\Lizard|\OpenAPI\Client\Model\Snake|null + */ + public function getFeaturedCreature(): \OpenAPI\Client\Model\Whale|\OpenAPI\Client\Model\Zebra|\OpenAPI\Client\Model\Lizard|\OpenAPI\Client\Model\Snake|null + { + return $this->container['featured_creature']; + } + + /** + * Sets featured_creature + * + * @param \OpenAPI\Client\Model\Whale|\OpenAPI\Client\Model\Zebra|\OpenAPI\Client\Model\Lizard|\OpenAPI\Client\Model\Snake|null $featured_creature featured_creature + * + * @return $this + */ + public function setFeaturedCreature(\OpenAPI\Client\Model\Whale|\OpenAPI\Client\Model\Zebra|\OpenAPI\Client\Model\Lizard|\OpenAPI\Client\Model\Snake|null $featured_creature): static + { + if (is_null($featured_creature)) { + throw new InvalidArgumentException('non-nullable featured_creature cannot be null'); + } + $this->container['featured_creature'] = $featured_creature; + + return $this; + } /** * Returns true if offset exists. False otherwise. * diff --git a/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/ObjectSerializer.php b/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/ObjectSerializer.php index 61892c81162d..6233eee342f7 100644 --- a/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/ObjectSerializer.php +++ b/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/ObjectSerializer.php @@ -500,14 +500,18 @@ public static function deserialize(mixed $data, string $class, ?array $httpHeade } else { $data = is_string($data) ? json_decode($data) : $data; - if (is_array($data)) { - $data = (object) $data; - } - - // A oneOf schema is not a value object: resolve the data to one of its member types. + // A oneOf/anyOf schema is not a value object: dispatch here, after json_decode (so + // members get the decoded value) but before the array-to-object cast (which breaks arrays). if (is_subclass_of($class, '\OpenAPI\Client\Model\OneOfInterface')) { return self::deserializeOneOf($data, $class, $httpHeaders); } + if (is_subclass_of($class, '\OpenAPI\Client\Model\AnyOfInterface')) { + return self::deserializeAnyOf($data, $class, $httpHeaders); + } + + if (is_array($data)) { + $data = (object) $data; + } // If a discriminator is defined and points to a valid subclass, use it. $discriminator = $class::DISCRIMINATOR; @@ -551,7 +555,7 @@ public static function deserialize(mixed $data, string $class, ?array $httpHeade * Otherwise each member type is tried in turn and the first one that yields a valid model * (or a non-null primitive) is returned. * - * @param mixed $data the data already decoded to an object + * @param mixed $data the decoded data to resolve to a member type * @param string $class a class name implementing OneOfInterface * @param string[]|null $httpHeaders HTTP headers * @@ -559,11 +563,16 @@ public static function deserialize(mixed $data, string $class, ?array $httpHeade */ private static function deserializeOneOf(mixed $data, string $class, ?array $httpHeaders): mixed { + // $data is already decoded (see deserialize()). Read the discriminator from an object view + // of it, but deserialize each member from the original $data so array and scalar members + // keep their own type handling instead of being coerced to an object here. + $probe = is_array($data) ? (object) $data : $data; + $discriminator = $class::getOneOfDiscriminator(); - if ($discriminator !== null && isset($data->{$discriminator}) && is_string($data->{$discriminator})) { + if ($discriminator !== null && is_object($probe) && isset($probe->{$discriminator}) && is_string($probe->{$discriminator})) { $mappings = $class::getOneOfDiscriminatorMappings(); - if (isset($mappings[$data->{$discriminator}])) { - return self::deserialize($data, $mappings[$data->{$discriminator}], $httpHeaders); + if (isset($mappings[$probe->{$discriminator}])) { + return self::deserialize($data, $mappings[$probe->{$discriminator}], $httpHeaders); } } @@ -571,7 +580,9 @@ private static function deserializeOneOf(mixed $data, string $class, ?array $htt try { $instance = self::deserialize($data, $type, $httpHeaders); } catch (\Throwable $e) { - // The data does not match this member type, try the next one. + // Any failure means the data does not match this member type; try the next one. + // The broad catch is intentional: a real failure is not hidden, since the loop + // throws below when no member type matches. continue; } @@ -587,6 +598,56 @@ private static function deserializeOneOf(mixed $data, string $class, ?array $htt throw new \InvalidArgumentException(sprintf('No matching schema in oneOf %s for the given data', $class)); } + /** + * Deserialize data into one of the member types of an `anyOf` schema. + * + * When the schema declares a discriminator, its value selects the member type directly. + * Otherwise each member type is tried in turn and the first one that yields a valid model + * (or a non-null primitive) is returned. + * + * @param mixed $data the decoded data to resolve to a member type + * @param string $class a class name implementing AnyOfInterface + * @param string[]|null $httpHeaders HTTP headers + * + * @return mixed an instance of one of the `anyOf` member types + */ + private static function deserializeAnyOf(mixed $data, string $class, ?array $httpHeaders): mixed + { + // $data is already decoded (see deserialize()). Read the discriminator from an object view + // of it, but deserialize each member from the original $data so array and scalar members + // keep their own type handling instead of being coerced to an object here. + $probe = is_array($data) ? (object) $data : $data; + + $discriminator = $class::getAnyOfDiscriminator(); + if ($discriminator !== null && is_object($probe) && isset($probe->{$discriminator}) && is_string($probe->{$discriminator})) { + $mappings = $class::getAnyOfDiscriminatorMappings(); + if (isset($mappings[$probe->{$discriminator}])) { + return self::deserialize($data, $mappings[$probe->{$discriminator}], $httpHeaders); + } + } + + foreach ($class::getAnyOfTypes() as $type) { + try { + $instance = self::deserialize($data, $type, $httpHeaders); + } catch (\Throwable $e) { + // Any failure means the data does not match this member type; try the next one. + // The broad catch is intentional: a real failure is not hidden, since the loop + // throws below when no member type matches. + continue; + } + + if ($instance instanceof ModelInterface) { + if ($instance->valid()) { + return $instance; + } + } elseif ($instance !== null) { + return $instance; + } + } + + throw new \InvalidArgumentException(sprintf('No matching schema in anyOf %s for the given data', $class)); + } + /** * Build a query string from an array of key value pairs. * diff --git a/samples/client/petstore/php-nextgen/OpenAPIClient-php/tests/Model/CreatureTest.php b/samples/client/petstore/php-nextgen/OpenAPIClient-php/tests/Model/CreatureTest.php new file mode 100644 index 000000000000..f50c81721569 --- /dev/null +++ b/samples/client/petstore/php-nextgen/OpenAPIClient-php/tests/Model/CreatureTest.php @@ -0,0 +1,79 @@ +