Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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}).
* <p>
* The legacy {@code self::}{@code <datatype>_<CASE>} 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}.
* <p>
* 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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}.
* <p>
* 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).
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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:
Expand Down
Loading