From 345d0f3b4077295728d0ab89e3b77ca158605c8e Mon Sep 17 00:00:00 2001 From: Jerry Date: Mon, 13 Apr 2026 19:33:46 +0800 Subject: [PATCH 1/2] fix(php-symfony): enum-ref query params use short types with correct imports * Normalize dotted / flattened enum-ref dataType and resync operation imports; rebuild OperationsMap imports after postProcess for api.mustache use lines * Unify api.mustache @param and signatures on vendorExtensions.x-parameter-type (short name + optional|null) for isEnumRef * Add OAS 3.1 petstore fixture under 3_1/php-symfony and extend PhpSymfonyServerCodegenTest (PHPDoc + php -l) * Document normalizeEnumRefParameterDataType / sync / refresh import pipeline --- .../languages/PhpSymfonyServerCodegen.java | 149 +++++++++++++++++- .../main/resources/php-symfony/api.mustache | 10 -- .../php-symfony/api_controller.mustache | 4 +- .../php-symfony/api_input_validation.mustache | 2 +- .../php/PhpSymfonyServerCodegenTest.java | 77 +++++++++ ...dotted-enum-ref-query-param-component.yaml | 30 ++++ 6 files changed, 257 insertions(+), 15 deletions(-) create mode 100644 modules/openapi-generator/src/test/resources/3_1/php-symfony/petstore-dotted-enum-ref-query-param-component.yaml diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpSymfonyServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpSymfonyServerCodegen.java index db559d3c6685..cef32972bd9b 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpSymfonyServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpSymfonyServerCodegen.java @@ -386,9 +386,11 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List operationList = operations.getOperation(); for (CodegenOperation op : operationList) { - // Loop through all input parameters to determine, whether we have to import something to - // make the input type available. + // Per-parameter: enum $ref fixes (normalizeEnumRefParameterDataType → syncEnumRefOperationImports), + // then x-parameter-type / x-comment-type. Aggregated Api imports are rebuilt once afterward + // (refreshAggregatedImportsForOperations). for (CodegenParameter param : op.allParams) { + normalizeEnumRefParameterDataType(op, param); // Determine if the parameter type is supported as a type hint and make it available // to the templating engine String typeHint = getTypeHint(param.dataType, false); @@ -405,6 +407,16 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, ListWhy: Templates concatenate {@code modelPackage} + {@code dataType} for FQCN strings (e.g. validation / + * deserialization). The API interface uses short names plus {@code use} imports, so {@code dataType} must be the + * short PHP class name. Upstream parsing (OpenAPI 3.1 dotted keys, {@code components.parameters} {@code $ref}, + * or flattening) can leave {@code dataType} as a bogus single token (no {@code \}), a full FQCN, or a leading + * {@code \}. + *

Execution (this method): + *

    + *
  1. Exit if not enum ref or empty {@code dataType}.
  2. + *
  3. Strip a leading {@code \} from the working copy.
  4. + *
  5. If the copy contains {@code \}: if it starts with {@code modelPackage + "\\"}, keep the suffix as short name; + * else if it looks like a model FQCN under another path, take {@link #extractSimpleName(String)}.
  6. + *
  7. If there is no {@code \}: optionally strip a bogus {@code modelPackage} with all separators removed + * (flat prefix) from the start, then {@link #toModelName(String)} so dotted logical names become the real + * generated model class name.
  8. + *
  9. Always finish with {@link #syncEnumRefOperationImports(CodegenOperation, CodegenParameter, String)} so + * {@link CodegenOperation#imports} matches the normalized short name.
  10. + *
+ *

Downstream in {@link #postProcessOperationsWithModels}: after this call, {@link #getTypeHint(String, Boolean)} + * and {@code x-parameter-type} use {@code modelPackage + "\\" + dataType} for enum refs so the hint matches imports. + *

Aggregated imports: {@link OperationsMap#setImports} is built before this hook; callers must invoke + * {@link #refreshAggregatedImportsForOperations(OperationsMap)} once all operations/parameters are processed. + */ + private void normalizeEnumRefParameterDataType(CodegenOperation op, CodegenParameter param) { + if (!param.isEnumRef || StringUtils.isEmpty(param.dataType)) { + return; + } + String dt = param.dataType; + if (dt.startsWith("\\")) { + dt = dt.substring(1); + } + final String mp = modelPackage(); + if (dt.contains("\\")) { + if (dt.startsWith(mp + "\\")) { + param.dataType = dt.substring(mp.length() + 1); + } else if (isModelClass(dt)) { + param.dataType = extractSimpleName(dt); + } + } else { + // No backslashes: flattened invoker+model+class token, dotted logical name, or already-short class name + String flatPrefix = mp.replace("\\", ""); + if (StringUtils.isNotEmpty(flatPrefix) + && dt.startsWith(flatPrefix) + && dt.length() > flatPrefix.length()) { + dt = dt.substring(flatPrefix.length()); + } + param.dataType = toModelName(dt); + } + syncEnumRefOperationImports(op, param, mp); + } + + /** + * Repairs {@link CodegenOperation#imports} for one enum-ref parameter after {@link #normalizeEnumRefParameterDataType}. + *

Step 1 — remove bogus entries: {@code DefaultGenerator} may add a single token that is the + * {@code modelPackage} string with all {@code \} removed, prefixed to the class name (still without {@code \}). + * Such values are not valid PHP imports and break {@code api.mustache} {@code use} lines; drop any import string + * that has no backslash, starts with that flat prefix, and is longer than the prefix alone. + *

Step 2 — add the short model name: if {@code param.dataType} is non-empty and {@link #needToImport(String)} + * is true, add it so the operation contributes the correct short classname to the union used for template imports. + */ + private void syncEnumRefOperationImports(CodegenOperation op, CodegenParameter param, String modelPackage) { + if (op == null || op.imports == null || StringUtils.isEmpty(modelPackage)) { + return; + } + String flatPrefix = modelPackage.replace("\\", ""); + if (StringUtils.isEmpty(flatPrefix)) { + return; + } + op.imports.removeIf(s -> + s != null + && !s.contains("\\") + && s.startsWith(flatPrefix) + && s.length() > flatPrefix.length()); + if (StringUtils.isNotEmpty(param.dataType) && needToImport(param.dataType)) { + op.imports.add(param.dataType); + } + } + + /** + * Recomputes the bundle-level import list exposed to Mustache ({@link OperationsMap#setImports}, + * {@code hasImport}) from the per-operation {@link CodegenOperation#imports} sets. + *

When: call once at the end of {@link #postProcessOperationsWithModels}, after every + * {@link CodegenOperation} has had its parameters processed (including {@link #normalizeEnumRefParameterDataType} / + * {@link #syncEnumRefOperationImports}). + *

Execution: + *

    + *
  1. Union all strings in {@code op.imports} across operations into a sorted {@link TreeSet} (stable, de-duplicated).
  2. + *
  3. For each symbol, resolve {@link #importMapping()} or {@link #toModelImportMap(String)} into + * {@code fullQualifiedImport → shortClassName} pairs (same shape as {@code DefaultGenerator#processOperations}).
  4. + *
  5. Build {@code {import, classname}} rows sorted by {@code classname} for the template partial that emits + * {@code use Full\\Qualified;}
  6. + *
  7. Replace {@code operationsMap} imports and set {@code hasImport}.
  8. + *
+ */ + private void refreshAggregatedImportsForOperations(OperationsMap operationsMap) { + OperationMap operationMap = operationsMap.getOperations(); + if (operationMap == null) { + return; + } + List operationList = operationMap.getOperation(); + if (operationList == null) { + return; + } + Set allImports = new TreeSet<>(); + for (CodegenOperation op : operationList) { + if (op.imports != null) { + allImports.addAll(op.imports); + } + } + Map mapped = new LinkedHashMap<>(); + for (String nextImport : allImports) { + String mapping = importMapping().get(nextImport); + if (mapping != null) { + mapped.put(mapping, nextImport); + } else { + mapped.putAll(toModelImportMap(nextImport)); + } + } + Set> importObjects = new TreeSet<>(Comparator.comparing(o -> o.get("classname"))); + for (Map.Entry e : mapped.entrySet()) { + Map row = new LinkedHashMap<>(); + row.put("import", e.getKey()); + row.put("classname", e.getValue()); + importObjects.add(row); + } + operationsMap.setImports(new ArrayList<>(importObjects)); + operationsMap.put("hasImport", !importObjects.isEmpty()); + } + private boolean isApplicationJsonOrApplicationXml(CodegenOperation op) { if (op.produces != null) { for(Map produce : op.produces) { diff --git a/modules/openapi-generator/src/main/resources/php-symfony/api.mustache b/modules/openapi-generator/src/main/resources/php-symfony/api.mustache index 6064af923fa7..2c13c0f91060 100644 --- a/modules/openapi-generator/src/main/resources/php-symfony/api.mustache +++ b/modules/openapi-generator/src/main/resources/php-symfony/api.mustache @@ -59,12 +59,7 @@ interface {{classname}} * {{/description}} {{#allParams}} - {{#isEnumRef}} - * @param \{{modelPackage}}\{{dataType}}{{^required}}{{^defaultValue}}|null{{/defaultValue}}{{/required}} ${{paramName}} {{description}} {{#required}}(required){{/required}}{{^required}}(optional{{#defaultValue}}, default to {{{.}}}{{/defaultValue}}){{/required}}{{#isDeprecated}} (deprecated){{/isDeprecated}} - {{/isEnumRef}} - {{^isEnumRef}} * @param {{vendorExtensions.x-parameter-type}}{{^required}}{{^defaultValue}}|null{{/defaultValue}}{{/required}} ${{paramName}} {{description}} {{#required}}(required){{/required}}{{^required}}(optional{{#defaultValue}}, default to {{{.}}}{{/defaultValue}}){{/required}}{{#isDeprecated}} (deprecated){{/isDeprecated}} - {{/isEnumRef}} {{/allParams}} * @param int &$responseCode The HTTP Response Code * @param array $responseHeaders Additional HTTP headers to return with the response () @@ -76,12 +71,7 @@ interface {{classname}} */ public function {{operationId}}( {{#allParams}} - {{#isEnumRef}} - {{^required}}{{^defaultValue}}?{{/defaultValue}}{{/required}}\{{modelPackage}}\{{dataType}} ${{paramName}}, - {{/isEnumRef}} - {{^isEnumRef}} {{^required}}{{^defaultValue}}?{{/defaultValue}}{{/required}}{{#vendorExtensions.x-parameter-type}}{{vendorExtensions.x-parameter-type}} {{/vendorExtensions.x-parameter-type}}${{paramName}}, - {{/isEnumRef}} {{/allParams}} int &$responseCode, array &$responseHeaders diff --git a/modules/openapi-generator/src/main/resources/php-symfony/api_controller.mustache b/modules/openapi-generator/src/main/resources/php-symfony/api_controller.mustache index 83a3cf00e8d4..c7c305a0e9de 100644 --- a/modules/openapi-generator/src/main/resources/php-symfony/api_controller.mustache +++ b/modules/openapi-generator/src/main/resources/php-symfony/api_controller.mustache @@ -154,10 +154,10 @@ class {{controllerName}} extends Controller {{^isFile}} {{#isBodyParam}} $inputFormat = $request->getMimeType($request->getContentTypeFormat()); - ${{paramName}} = $this->deserialize(${{paramName}}, '{{#isContainer}}{{#items}}array<{{dataType}}>{{/items}}{{/isContainer}}{{^isContainer}}{{#isEnumRef}}\{{modelPackage}}\{{dataType}}{{/isEnumRef}}{{^isEnumRef}}{{dataType}}{{/isEnumRef}}{{/isContainer}}', $inputFormat); + ${{paramName}} = $this->deserialize(${{paramName}}, '{{#isContainer}}{{#items}}array<{{dataType}}>{{/items}}{{/isContainer}}{{^isContainer}}{{#isEnumRef}}\{{{modelPackage}}}\{{{dataType}}}{{/isEnumRef}}{{^isEnumRef}}{{dataType}}{{/isEnumRef}}{{/isContainer}}', $inputFormat); {{/isBodyParam}} {{^isBodyParam}} - ${{paramName}} = $this->deserialize(${{paramName}}, '{{#isContainer}}array<{{collectionFormat}}{{^collectionFormat}}csv{{/collectionFormat}},{{dataType}}>{{/isContainer}}{{^isContainer}}{{#isEnumRef}}\{{modelPackage}}\{{dataType}}{{/isEnumRef}}{{^isEnumRef}}{{dataType}}{{/isEnumRef}}{{/isContainer}}', 'string'); + ${{paramName}} = $this->deserialize(${{paramName}}, '{{#isContainer}}array<{{collectionFormat}}{{^collectionFormat}}csv{{/collectionFormat}},{{dataType}}>{{/isContainer}}{{^isContainer}}{{#isEnumRef}}\{{{modelPackage}}}\{{{dataType}}}{{/isEnumRef}}{{^isEnumRef}}{{dataType}}{{/isEnumRef}}{{/isContainer}}', 'string'); {{/isBodyParam}} {{/isFile}} {{/allParams}} diff --git a/modules/openapi-generator/src/main/resources/php-symfony/api_input_validation.mustache b/modules/openapi-generator/src/main/resources/php-symfony/api_input_validation.mustache index 330f113e976f..98c6e5b5eec1 100644 --- a/modules/openapi-generator/src/main/resources/php-symfony/api_input_validation.mustache +++ b/modules/openapi-generator/src/main/resources/php-symfony/api_input_validation.mustache @@ -39,7 +39,7 @@ {{/isFile}} {{^isFile}} {{#isEnumRef}} - $asserts[] = new Assert\Type("\{{modelPackage}}\{{dataType}}"); + $asserts[] = new Assert\Type('\{{{modelPackage}}}\{{{dataType}}}'); {{/isEnumRef}} {{^isEnumRef}} $asserts[] = new Assert\Type('{{dataType}}'); 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 dbbb89171c70..54298c614486 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 @@ -25,13 +25,18 @@ import org.openapitools.codegen.languages.AbstractPhpCodegen; import org.openapitools.codegen.languages.PhpSymfonyServerCodegen; import org.testng.Assert; +import org.testng.SkipException; import org.testng.annotations.Test; import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; public class PhpSymfonyServerCodegenTest { @@ -173,4 +178,76 @@ public void testGeneratePingWithDifferentSourceDirectory() throws Exception { output.deleteOnExit(); } + + /** + * OpenAPI 3.1 + dotted schema keys + {@code components.parameters} {@code $ref}: enum-ref query + * parameters must use a short model class name in {@code DefaultApiInterface} (aligned with + * {@code use} imports), not a bogus flattened namespace token. + *

+ * Also verifies PHPDoc {@code @param} uses the same short name (not {@code \\FQCN}) and, when + * {@code php} is on {@code PATH}, that {@code php -l} accepts the generated file (valid syntax). + */ + @Test + public void testPetstoreDottedEnumRefQueryParameterUsesShortClassInApiInterface() throws Exception { + Map properties = new HashMap<>(); + properties.put("invokerPackage", "Org\\OpenAPITools\\Petstore"); + + File output = Files.createTempDirectory("test").toFile(); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("php-symfony") + .setAdditionalProperties(properties) + .setInputSpec("src/test/resources/3_1/php-symfony/petstore-dotted-enum-ref-query-param-component.yaml") + .setOutputDir(output.getAbsolutePath().replace("\\", "/")); + + final ClientOptInput clientOptInput = configurator.toClientOptInput(); + 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.assertFalse( + apiContent.contains("OrgOpenAPIToolsPetstoreModel"), + "Must not emit flattened invoker+model token in interface"); + Assert.assertTrue( + apiContent.contains("use Org\\OpenAPITools\\Petstore\\Model\\PetModelPetStatus;"), + "Expected enum model import"); + Assert.assertTrue( + apiContent.contains("?PetModelPetStatus $status"), + "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)"); + Assert.assertFalse( + apiContent.contains("?\\Org\\OpenAPITools\\Petstore\\Model\\PetModelPetStatus $status"), + "Signature must not use leading-backslash FQCN when a matching use import exists"); + Assert.assertFalse( + apiContent.contains("@param \\Org\\"), + "PHPDoc @param must not use leading-backslash FQCN for enum ref"); + + assertGeneratedPhpSyntaxValid(apiInterfaceFile); + + output.deleteOnExit(); + } + + /** + * Runs {@code php -l} on the file. Skips if {@code php} is not available (optional toolchain). + */ + private static void assertGeneratedPhpSyntaxValid(File phpFile) throws Exception { + ProcessBuilder pb = new ProcessBuilder("php", "-l", phpFile.getAbsolutePath()); + pb.redirectErrorStream(true); + final Process p; + try { + p = pb.start(); + } catch (IOException e) { + throw new SkipException("php not available on PATH, skipping syntax check: " + e.getMessage()); + } + Assert.assertTrue(p.waitFor(30, TimeUnit.SECONDS), "php -l timed out"); + String out = new String(p.getInputStream().readAllBytes(), StandardCharsets.UTF_8).trim(); + Assert.assertEquals(p.exitValue(), 0, "php -l must accept generated interface: " + out); + } } diff --git a/modules/openapi-generator/src/test/resources/3_1/php-symfony/petstore-dotted-enum-ref-query-param-component.yaml b/modules/openapi-generator/src/test/resources/3_1/php-symfony/petstore-dotted-enum-ref-query-param-component.yaml new file mode 100644 index 000000000000..8b27f9a648cb --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_1/php-symfony/petstore-dotted-enum-ref-query-param-component.yaml @@ -0,0 +1,30 @@ +openapi: 3.1.0 +info: + title: php-symfony petstore dotted enum ref via components.parameters + version: '1.0' +paths: + /pets: + get: + operationId: listPets + parameters: + - $ref: '#/components/parameters/Pet.HTTP.ListPetsRequest.status' + responses: + '200': + description: OK +components: + parameters: + Pet.HTTP.ListPetsRequest.status: + name: status + in: query + required: false + schema: + $ref: '#/components/schemas/Pet.Model.PetStatus' + default: available + explode: false + schemas: + Pet.Model.PetStatus: + type: string + enum: + - available + - pending + - sold From 8def2cb5de31bc1d8e3ce5cfab9b3d8ffd5fb13b Mon Sep 17 00:00:00 2001 From: Jerry Date: Mon, 13 Apr 2026 19:42:28 +0800 Subject: [PATCH 2/2] update comments --- .../codegen/languages/PhpSymfonyServerCodegen.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpSymfonyServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpSymfonyServerCodegen.java index cef32972bd9b..98eb526a9c62 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpSymfonyServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpSymfonyServerCodegen.java @@ -386,10 +386,13 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List operationList = operations.getOperation(); for (CodegenOperation op : operationList) { - // Per-parameter: enum $ref fixes (normalizeEnumRefParameterDataType → syncEnumRefOperationImports), - // then x-parameter-type / x-comment-type. Aggregated Api imports are rebuilt once afterward - // (refreshAggregatedImportsForOperations). + // Loop through all input parameters to determine, whether we have to import something to + // make the input type available. for (CodegenParameter param : op.allParams) { + // Enum-by-reference query params (e.g. OAS 3.1 dotted keys): normalize dataType and + // sync this operation's imports (normalizeEnumRefParameterDataType → + // syncEnumRefOperationImports). refreshAggregatedImportsForOperations runs once after + // this inner loop to rebuild bundle-level Api imports for Mustache. normalizeEnumRefParameterDataType(op, param); // Determine if the parameter type is supported as a type hint and make it available // to the templating engine