From e3a2ff01262f7af0d6d5fd294b9db3d6414d321b Mon Sep 17 00:00:00 2001 From: Jerry Date: Tue, 14 Apr 2026 22:03:34 +0800 Subject: [PATCH 1/2] fix(php): honor OpenAPI default beside $ref in toDefaultValue Parameter/model schemas that combine $ref with a sibling default were not handled by the boolean/number/string branches, so CodegenParameter.defaultValue stayed empty and php-symfony emitted query->get without the second argument. Add a fallback: when getDefault() is set, emit a PHP literal (quoted strings via escapeTextInSingleQuotes, other types via toString()). Update PhpSymfonyServerCodegenTest: assert optional enum-ref query defaults, and relax petstore interface assertions when a default is present (non-nullable signature per api.mustache). --- .../codegen/languages/AbstractPhpCodegen.java | 11 +++ .../php/PhpSymfonyServerCodegenTest.java | 75 ++++++++++++++++++- .../optional-enum-query-ref-default.yaml | 42 +++++++++++ 3 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 modules/openapi-generator/src/test/resources/3_1/php-symfony/optional-enum-query-ref-default.yaml 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 68520b123d99..ec6ff0038375 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 @@ -636,6 +636,17 @@ public String toDefaultValue(Schema p) { } } + // OAS 3.x: `default` may appear alongside `$ref` on the same schema (e.g. optional query param whose schema + // references an enum model). That wrapper is often not classified as string/number here, but still carries + // the default OpenAPI value — needed so Mustache can emit `query->get(..., )` for php-symfony. + if (p.getDefault() != null) { + Object def = p.getDefault(); + if (def instanceof String) { + return "'" + escapeTextInSingleQuotes((String) def) + "'"; + } + return def.toString(); + } + return null; } 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 54298c614486..f65a1bb843f0 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 @@ -216,12 +216,14 @@ public void testPetstoreDottedEnumRefQueryParameterUsesShortClassInApiInterface( Assert.assertTrue( apiContent.contains("use Org\\OpenAPITools\\Petstore\\Model\\PetModelPetStatus;"), "Expected enum model import"); + // Optional enum ref may carry an OpenAPI default: php-symfony api.mustache omits leading "?" / "|null" when + // defaultValue is set (handler always receives the enum after the controller applies the default). Assert.assertTrue( - apiContent.contains("?PetModelPetStatus $status"), + Pattern.compile("public function listPets\\(\\s*\\??PetModelPetStatus\\s+\\$status,").matcher(apiContent).find(), "Expected enum ref query param to use short class in type hint"); Assert.assertTrue( - Pattern.compile("@param\\s+PetModelPetStatus\\|null\\s+\\$status\\b").matcher(apiContent).find(), - "PHPDoc @param should use short PetModelPetStatus|null (consistent with use import)"); + Pattern.compile("@param\\s+PetModelPetStatus(\\|null)?\\s+\\$status\\b").matcher(apiContent).find(), + "PHPDoc @param should use short PetModelPetStatus (optional |null when no default in spec)"); Assert.assertFalse( apiContent.contains("?\\Org\\OpenAPITools\\Petstore\\Model\\PetModelPetStatus $status"), "Signature must not use leading-backslash FQCN when a matching use import exists"); @@ -234,6 +236,73 @@ public void testPetstoreDottedEnumRefQueryParameterUsesShortClassInApiInterface( output.deleteOnExit(); } + /** + * Optional {@code in: query} parameter: {@code required: false}, schema is an enum {@code $ref} with a valid + * {@code default} (see OpenAPI 3.x). Omitting the query key must be equivalent to sending that default; the + * generated controller must not reject the request in validation solely because the value was absent. + *

+ * Spec: {@code src/test/resources/3_1/php-symfony/optional-enum-query-ref-default.yaml}. Product doc: + * {@code php-symfony.md} section "可选 query:带默认值的非必填枚举 {@code $ref} 缺省却仍被拒绝". + *

+ * Expected generated behavior (any one is acceptable): + *

+ * Also asserts the integer optional {@code limit} parameter still receives {@code get('limit', 10)} as a control. + *

+ * Note: This test fails on the generator until optional enum-ref query parameters expose + * {@link org.openapitools.codegen.CodegenParameter#defaultValue} (or equivalent) so templates apply the OpenAPI + * default and/or skip strict {@code Assert\\Type} on {@code null}. It is intended to lock the fix described in the + * php-symfony troubleshooting doc. + */ + @Test + public void testOptionalEnumRefQueryParameterWithDefaultAppliesOpenApiSemantics() throws Exception { + Map properties = new HashMap<>(); + properties.put("invokerPackage", "Org\\OpenAPITools\\FeedHints"); + properties.put(AbstractPhpCodegen.SRC_BASE_PATH, "src"); + + File output = Files.createTempDirectory("test").toFile(); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("php-symfony") + .setAdditionalProperties(properties) + .setInputSpec("src/test/resources/3_1/php-symfony/optional-enum-query-ref-default.yaml") + .setOutputDir(output.getAbsolutePath().replace("\\", "/")); + + final ClientOptInput clientOptInput = configurator.toClientOptInput(); + DefaultGenerator generator = new DefaultGenerator(); + List files = generator.opts(clientOptInput).generate(); + + File controllerFile = files.stream() + .filter(f -> "DefaultController.php".equals(f.getName()) && f.getPath().contains("Controller" + File.separator)) + .findFirst() + .orElseThrow(() -> new AssertionError("DefaultController.php not generated")); + + String controller = Files.readString(controllerFile.toPath(), StandardCharsets.UTF_8); + + Assert.assertTrue( + controller.contains("$request->query->get('limit', 10)"), + "Integer optional query with default should pass default as second argument to query->get (control case)"); + + boolean defaultInGet = Pattern.compile("\\$request->query->get\\('tone',\\s*").matcher(controller).find(); + boolean elvisDefault = Pattern.compile("\\$tone\\s*=\\s*\\$tone\\?:").matcher(controller).find(); + boolean optionalEnumTypeAssert = + controller.contains("new Assert\\Optional(") + && controller.contains("PetAnnouncementTone"); + + Assert.assertTrue( + defaultInGet || elvisDefault || optionalEnumTypeAssert, + "Omitted optional enum-ref query with OpenAPI default must apply default (get/Elvis) and/or use " + + "Assert\\Optional around enum Type so null is valid before default is applied; " + + "see optional-enum-query-ref-default.yaml and php-symfony troubleshooting doc"); + + assertGeneratedPhpSyntaxValid(controllerFile); + + output.deleteOnExit(); + } + /** * 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 new file mode 100644 index 000000000000..a1b1c0d55ea0 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_1/php-symfony/optional-enum-query-ref-default.yaml @@ -0,0 +1,42 @@ +# 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. +openapi: 3.1.0 +info: + title: Optional enum query ref with default (php-symfony repro) + version: '1.0' +paths: + /pets/feed-hints: + get: + operationId: listFeedHints + parameters: + - $ref: '#/components/parameters/ToneQuery' + - name: limit + in: query + required: false + description: Integer optional query with default (often generated correctly). + schema: + type: integer + format: int32 + default: 10 + minimum: 1 + maximum: 50 + responses: + '200': + description: OK +components: + parameters: + ToneQuery: + name: tone + in: query + required: false + description: Optional filter; default applies when the query key is omitted. + schema: + $ref: '#/components/schemas/PetAnnouncementTone' + default: friendly + schemas: + PetAnnouncementTone: + type: string + enum: + - friendly + - formal From 1fa1b8ce8fa695e598dc29fc13fca121c5b8ec6c Mon Sep 17 00:00:00 2001 From: Jerry Date: Tue, 14 Apr 2026 23:07:09 +0800 Subject: [PATCH 2/2] fix(php): safe OpenAPI default literals for $ref schemas; tighten php-symfony tests AbstractPhpCodegen maps getDefault() to valid PHP via defaultValueToPhpLiteral instead of blind toString(). PhpSymfonyServerCodegenTest asserts non-nullable handler params when an enum $ref has an OpenAPI default (petstore + optional spec). --- .../codegen/languages/AbstractPhpCodegen.java | 42 ++++++++++++++++--- .../php/PhpSymfonyServerCodegenTest.java | 37 +++++++++++++--- 2 files changed, 68 insertions(+), 11 deletions(-) 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 ec6ff0038375..0a1e1de1a194 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 @@ -33,6 +33,7 @@ import org.slf4j.LoggerFactory; import java.io.File; +import java.math.BigDecimal; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -640,16 +641,47 @@ public String toDefaultValue(Schema p) { // references an enum model). That wrapper is often not classified as string/number here, but still carries // the default OpenAPI value — needed so Mustache can emit `query->get(..., )` for php-symfony. if (p.getDefault() != null) { - Object def = p.getDefault(); - if (def instanceof String) { - return "'" + escapeTextInSingleQuotes((String) def) + "'"; - } - return def.toString(); + return defaultValueToPhpLiteral(p.getDefault()); } return null; } + /** + * Converts a JSON Schema {@code default} value to a PHP expression suitable for templates (e.g. second argument to + * {@code query->get}). Only safe scalar literals are supported; unknown types log a warning and yield {@code null} + * so we do not emit broken PHP from {@code Object#toString()}. + */ + private String defaultValueToPhpLiteral(Object def) { + if (def == null) { + return null; + } + if (def instanceof String) { + return "'" + escapeTextInSingleQuotes((String) def) + "'"; + } + if (def instanceof Boolean) { + return Boolean.TRUE.equals(def) ? "true" : "false"; + } + if (def instanceof BigDecimal) { + return ((BigDecimal) def).toPlainString(); + } + if (def instanceof Number) { + String s = def.toString(); + if (s.contains("Infinity") || s.contains("NaN")) { + LOGGER.warn("Unsupported numeric default for PHP literal: {}", def); + return null; + } + return s; + } + if (def instanceof Character) { + return "'" + escapeTextInSingleQuotes(String.valueOf((Character) def)) + "'"; + } + LOGGER.warn( + "Cannot convert OpenAPI default of type {} to a PHP literal; omitting defaultValue", + def.getClass().getName()); + return null; + } + @Override public void setParameterExampleValue(CodegenParameter p) { String example; 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 f65a1bb843f0..291d25da5ca5 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 @@ -216,14 +216,20 @@ public void testPetstoreDottedEnumRefQueryParameterUsesShortClassInApiInterface( Assert.assertTrue( apiContent.contains("use Org\\OpenAPITools\\Petstore\\Model\\PetModelPetStatus;"), "Expected enum model import"); - // Optional enum ref may carry an OpenAPI default: php-symfony api.mustache omits leading "?" / "|null" when - // defaultValue is set (handler always receives the enum after the controller applies the default). + // This spec sets default: available on the enum $ref; the handler must be non-nullable (no leading "?" / + // "|null") because the controller always supplies a value after applying the OpenAPI default. Assert.assertTrue( - Pattern.compile("public function listPets\\(\\s*\\??PetModelPetStatus\\s+\\$status,").matcher(apiContent).find(), - "Expected enum ref query param to use short class in type hint"); + Pattern.compile("public function listPets\\(\\s*PetModelPetStatus\\s+\\$status,").matcher(apiContent).find(), + "Expected defaulted enum-ref query param to use short non-nullable class in type hint"); + Assert.assertFalse( + Pattern.compile("public function listPets\\(\\s*\\?PetModelPetStatus\\s+\\$status").matcher(apiContent).find(), + "Defaulted enum-ref query param must not use nullable type hint (?PetModelPetStatus)"); Assert.assertTrue( - Pattern.compile("@param\\s+PetModelPetStatus(\\|null)?\\s+\\$status\\b").matcher(apiContent).find(), - "PHPDoc @param should use short PetModelPetStatus (optional |null when no default in spec)"); + Pattern.compile("@param\\s+PetModelPetStatus\\s+\\$status\\b").matcher(apiContent).find(), + "PHPDoc @param should use short PetModelPetStatus without |null when OpenAPI default is set"); + Assert.assertFalse( + Pattern.compile("@param\\s+PetModelPetStatus\\|null\\s+\\$status\\b").matcher(apiContent).find(), + "PHPDoc must not document |null for enum ref when OpenAPI default is set"); Assert.assertFalse( apiContent.contains("?\\Org\\OpenAPITools\\Petstore\\Model\\PetModelPetStatus $status"), "Signature must not use leading-backslash FQCN when a matching use import exists"); @@ -275,6 +281,25 @@ public void testOptionalEnumRefQueryParameterWithDefaultAppliesOpenApiSemantics( DefaultGenerator generator = new DefaultGenerator(); List files = generator.opts(clientOptInput).generate(); + File apiInterfaceFile = files.stream() + .filter(f -> "DefaultApiInterface.php".equals(f.getName()) && f.getPath().contains("Api" + File.separator)) + .findFirst() + .orElseThrow(() -> new AssertionError("DefaultApiInterface.php not generated")); + String apiContent = Files.readString(apiInterfaceFile.toPath(), StandardCharsets.UTF_8); + Assert.assertTrue( + Pattern.compile("public function listFeedHints\\(\\s*PetAnnouncementTone\\s+\\$tone,").matcher(apiContent).find(), + "Expected defaulted enum-ref query param to use short non-nullable class in API interface type hint"); + Assert.assertFalse( + Pattern.compile("public function listFeedHints\\(\\s*\\?PetAnnouncementTone\\s+\\$tone").matcher(apiContent).find(), + "Defaulted enum-ref query param must not use nullable type hint (?PetAnnouncementTone)"); + Assert.assertTrue( + Pattern.compile("@param\\s+PetAnnouncementTone\\s+\\$tone\\b").matcher(apiContent).find(), + "PHPDoc @param should use PetAnnouncementTone without |null when OpenAPI default is set"); + Assert.assertFalse( + Pattern.compile("@param\\s+PetAnnouncementTone\\|null\\s+\\$tone\\b").matcher(apiContent).find(), + "PHPDoc must not document |null for enum ref when OpenAPI default is set"); + assertGeneratedPhpSyntaxValid(apiInterfaceFile); + File controllerFile = files.stream() .filter(f -> "DefaultController.php".equals(f.getName()) && f.getPath().contains("Controller" + File.separator)) .findFirst()