diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPhpCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPhpCodegen.java index 0a1e1de1a194..4315d09c8098 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPhpCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPhpCodegen.java @@ -781,9 +781,46 @@ public String toEnumValue(String value, String datatype) { } } + /** + * Builds the PHP expression for a backed enum case default (PHP 8.1+ {@code enum}). + *

+ * The legacy {@code self::}{@code _} form came from class-constant style enums (#10273) and is + * invalid when {@code datatype} is a namespaced class: {@code self::} only resolves constants on the current + * class. Native enums must use {@code EnumType::CASE}. + *

+ * Execution: {@code datatype} is produced upstream (e.g. {@link DefaultCodegen#updateCodegenPropertyEnum}) via + * {@link #getTypeDeclaration(Schema)} for the referenced enum schema; {@code value} is the sanitized case name + * from {@link #toEnumVarName}. When the enum class sits under {@link #modelPackage}, we emit only the short class + * name plus {@code ::} so it matches sibling model references in generated files ({@code namespace} is + * {@code modelPackage}; unqualified names resolve correctly). A fully qualified body without a leading + * {@code \} would be resolved relative to the file namespace and is invalid PHP for defaults. + * + * @param value enum case name (e.g. {@code AVAILABLE}) + * @param datatype enum class as produced by {@link #getTypeDeclaration(Schema)} (may include {@code modelPackage}) + * @return PHP default expression for that case (e.g. {@code PetStatus::AVAILABLE}) + */ @Override public String toEnumDefaultValue(String value, String datatype) { - return "self::" + datatype + "_" + value; + return unqualifiedEnumClassForModelDefault(datatype) + "::" + value; + } + + /** + * Strips {@link #modelPackage} from a declared enum class name so defaults use the same unqualified form as + * property type hints in model templates. + * + * @param datatype enum class string from codegen (optional leading {@code \}) + * @return short class name if under {@code modelPackage}, otherwise the original {@code datatype} + */ + private String unqualifiedEnumClassForModelDefault(String datatype) { + if (StringUtils.isBlank(datatype) || StringUtils.isBlank(modelPackage)) { + return datatype; + } + String normalized = datatype.charAt(0) == '\\' ? datatype.substring(1) : datatype; + String prefix = modelPackage + "\\"; + if (normalized.startsWith(prefix)) { + return normalized.substring(prefix.length()); + } + return datatype; } @Override diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/php/PhpSymfonyServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/php/PhpSymfonyServerCodegenTest.java index 291d25da5ca5..f2ca5c84da98 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/php/PhpSymfonyServerCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/php/PhpSymfonyServerCodegenTest.java @@ -17,8 +17,11 @@ package org.openapitools.codegen.php; +import io.swagger.v3.oas.models.media.Schema; import org.openapitools.codegen.ClientOptInput; import org.openapitools.codegen.CodegenConstants; +import org.openapitools.codegen.CodegenModel; +import org.openapitools.codegen.CodegenProperty; import org.openapitools.codegen.DefaultGenerator; import org.openapitools.codegen.TestUtils; import org.openapitools.codegen.config.CodegenConfigurator; @@ -328,6 +331,44 @@ public void testOptionalEnumRefQueryParameterWithDefaultAppliesOpenApiSemantics( output.deleteOnExit(); } + /** + * Model property: string enum {@code $ref} with sibling {@code default} must produce a valid PHP 8.1 backed-enum + * default expression ({@code Type::CASE}), not {@code self::} + FQCN + {@code _CASE} from + * {@link AbstractPhpCodegen#toEnumDefaultValue}. + *

+ * Spec: {@code src/test/resources/3_1/php-symfony/optional-enum-query-ref-default.yaml} ({@code Pet.HTTP.CreatePetRequest.status}). + */ + @Test + public void testModelPropertyEnumRefWithDefaultUsesNativeEnumCaseInCodegen() { + final var openAPI = TestUtils.parseFlattenSpec( + "src/test/resources/3_1/php-symfony/optional-enum-query-ref-default.yaml"); + final PhpSymfonyServerCodegen codegen = new PhpSymfonyServerCodegen(); + codegen.setOpenAPI(openAPI); + codegen.processOpts(); + + Schema createPet = openAPI.getComponents().getSchemas().get("Pet.HTTP.CreatePetRequest"); + Assert.assertNotNull(createPet, "Fixture must define Pet.HTTP.CreatePetRequest"); + + CodegenModel cm = codegen.fromModel("Pet.HTTP.CreatePetRequest", createPet); + codegen.postProcessModels(TestUtils.createCodegenModelWrapper(cm)); + + CodegenProperty status = cm.getVars().stream() + .filter(v -> "status".equals(v.getName())) + .findFirst() + .orElseThrow(() -> new AssertionError("Expected status property on CreatePetRequest model")); + + Assert.assertNotNull( + status.getDefaultValue(), + "Enum ref + OpenAPI default should set CodegenProperty.defaultValue (see AbstractPhpCodegen.toDefaultValue" + + " ref+default handling and updateCodegenPropertyEnum)"); + Assert.assertFalse( + status.getDefaultValue().startsWith("self::"), + "Invalid PHP: backed enum default must not use self:: prefix, got: " + status.getDefaultValue()); + Assert.assertTrue( + status.getDefaultValue().contains("::AVAILABLE"), + "Expected PetModelPetStatus::AVAILABLE (or FQCN::AVAILABLE), got: " + status.getDefaultValue()); + } + /** * Runs {@code php -l} on the file. Skips if {@code php} is not available (optional toolchain). */ diff --git a/modules/openapi-generator/src/test/resources/3_1/php-symfony/optional-enum-query-ref-default.yaml b/modules/openapi-generator/src/test/resources/3_1/php-symfony/optional-enum-query-ref-default.yaml index a1b1c0d55ea0..2fb394f57a3b 100644 --- a/modules/openapi-generator/src/test/resources/3_1/php-symfony/optional-enum-query-ref-default.yaml +++ b/modules/openapi-generator/src/test/resources/3_1/php-symfony/optional-enum-query-ref-default.yaml @@ -1,13 +1,32 @@ -# Minimal OpenAPI 3.1 spec: optional query with enum $ref + default (components.parameters). -# Repro pattern: omitting the query key should behave like the declared default; php-symfony -# may still validate null as enum type before business logic. See project troubleshooting doc. +# Minimal OpenAPI 3.1 spec (pet store style): +# - Optional query: enum $ref + default (components.parameters) — regression for query/controller path. +# - Model property: enum $ref + sibling default on an object schema — invalid PHP may be emitted for the +# property initializer (e.g. self::..._CASE) when default flows through AbstractPhpCodegen.toEnumDefaultValue. openapi: 3.1.0 info: title: Optional enum query ref with default (php-symfony repro) version: '1.0' +servers: + - url: https://petstore.example.com/api +# Explicit empty security for minimal test fixtures (no global auth). +security: [] paths: + /pets: + post: + summary: Create a pet (request body uses enum ref + default on a property) + operationId: createPet + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Pet.HTTP.CreatePetRequest' + responses: + '201': + description: Created /pets/feed-hints: get: + summary: List feed hints (optional enum query ref + default) operationId: listFeedHints parameters: - $ref: '#/components/parameters/ToneQuery' @@ -35,6 +54,19 @@ components: $ref: '#/components/schemas/PetAnnouncementTone' default: friendly schemas: + Pet.Model.PetStatus: + type: string + enum: + - available + - pending + - sold + Pet.HTTP.CreatePetRequest: + type: object + properties: + # OAS 3.1: sibling keys beside $ref are allowed. + status: + $ref: '#/components/schemas/Pet.Model.PetStatus' + default: available PetAnnouncementTone: type: string enum: