Skip to content
Merged
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 @@ -389,6 +389,11 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
// 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
String typeHint = getTypeHint(param.dataType, false);
Expand All @@ -405,6 +410,16 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
if (param.isContainer) {
param.vendorExtensions.put("x-comment-type", prependSlashToDataTypeOnlyIfNecessary(param.dataType) + "[]");
}

// Enum $ref parameters: dataType is the short PHP model class name only; getTypeHint(dataType) is empty.
// Build FQCN body so getTypeHint matches isModelClass and yields the same short name as file-level imports.
if (param.isEnumRef && StringUtils.isNotEmpty(param.dataType)) {
String fqcnBody = modelPackage() + "\\" + param.dataType;
String enumTypeHint = getTypeHint(fqcnBody, false);
if (StringUtils.isNotEmpty(enumTypeHint)) {
param.vendorExtensions.put("x-parameter-type", enumTypeHint);
}
}
}

// Create a variable to display the correct return type in comments for interfaces
Expand Down Expand Up @@ -441,9 +456,142 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo

operations.put("authMethods", authMethods);

refreshAggregatedImportsForOperations(objs);

return objs;
}

/**
* Normalizes {@link CodegenParameter#getDataType()} for enum-by-reference parameters only
* ({@code param.isEnumRef}).
* <p><b>Why:</b> 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
* <em>short</em> 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 \}.
* <p><b>Execution (this method):</b>
* <ol>
* <li>Exit if not enum ref or empty {@code dataType}.</li>
* <li>Strip a leading {@code \} from the working copy.</li>
* <li>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)}.</li>
* <li>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.</li>
* <li>Always finish with {@link #syncEnumRefOperationImports(CodegenOperation, CodegenParameter, String)} so
* {@link CodegenOperation#imports} matches the normalized short name.</li>
* </ol>
* <p><b>Downstream in {@link #postProcessOperationsWithModels}:</b> after this call, {@link #getTypeHint(String, Boolean)}
* and {@code x-parameter-type} use {@code modelPackage + "\\" + dataType} for enum refs so the hint matches imports.
* <p><b>Aggregated imports:</b> {@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}.
* <p><b>Step 1 — remove bogus entries:</b> {@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.
* <p><b>Step 2 — add the short model name:</b> 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.
* <p><b>When:</b> call once at the end of {@link #postProcessOperationsWithModels}, after every
* {@link CodegenOperation} has had its parameters processed (including {@link #normalizeEnumRefParameterDataType} /
* {@link #syncEnumRefOperationImports}).
* <p><b>Execution:</b>
* <ol>
* <li>Union all strings in {@code op.imports} across operations into a sorted {@link TreeSet} (stable, de-duplicated).</li>
* <li>For each symbol, resolve {@link #importMapping()} or {@link #toModelImportMap(String)} into
* {@code fullQualifiedImport → shortClassName} pairs (same shape as {@code DefaultGenerator#processOperations}).</li>
* <li>Build {@code {import, classname}} rows sorted by {@code classname} for the template partial that emits
* {@code use Full\\Qualified;}</li>
* <li>Replace {@code operationsMap} imports and set {@code hasImport}.</li>
* </ol>
*/
private void refreshAggregatedImportsForOperations(OperationsMap operationsMap) {
OperationMap operationMap = operationsMap.getOperations();
if (operationMap == null) {
return;
}
List<CodegenOperation> operationList = operationMap.getOperation();
if (operationList == null) {
return;
}
Set<String> allImports = new TreeSet<>();
for (CodegenOperation op : operationList) {
if (op.imports != null) {
allImports.addAll(op.imports);
}
}
Map<String, String> 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<Map<String, String>> importObjects = new TreeSet<>(Comparator.comparing(o -> o.get("classname")));
for (Map.Entry<String, String> e : mapped.entrySet()) {
Map<String, String> 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<String, String> produce : op.produces) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 ()
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}}');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
* <p>
* 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<String, Object> 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<File> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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
Loading