@@ -389,6 +389,11 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
389389 // Loop through all input parameters to determine, whether we have to import something to
390390 // make the input type available.
391391 for (CodegenParameter param : op .allParams ) {
392+ // Enum-by-reference query params (e.g. OAS 3.1 dotted keys): normalize dataType and
393+ // sync this operation's imports (normalizeEnumRefParameterDataType →
394+ // syncEnumRefOperationImports). refreshAggregatedImportsForOperations runs once after
395+ // this inner loop to rebuild bundle-level Api imports for Mustache.
396+ normalizeEnumRefParameterDataType (op , param );
392397 // Determine if the parameter type is supported as a type hint and make it available
393398 // to the templating engine
394399 String typeHint = getTypeHint (param .dataType , false );
@@ -405,6 +410,16 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
405410 if (param .isContainer ) {
406411 param .vendorExtensions .put ("x-comment-type" , prependSlashToDataTypeOnlyIfNecessary (param .dataType ) + "[]" );
407412 }
413+
414+ // Enum $ref parameters: dataType is the short PHP model class name only; getTypeHint(dataType) is empty.
415+ // Build FQCN body so getTypeHint matches isModelClass and yields the same short name as file-level imports.
416+ if (param .isEnumRef && StringUtils .isNotEmpty (param .dataType )) {
417+ String fqcnBody = modelPackage () + "\\ " + param .dataType ;
418+ String enumTypeHint = getTypeHint (fqcnBody , false );
419+ if (StringUtils .isNotEmpty (enumTypeHint )) {
420+ param .vendorExtensions .put ("x-parameter-type" , enumTypeHint );
421+ }
422+ }
408423 }
409424
410425 // Create a variable to display the correct return type in comments for interfaces
@@ -441,9 +456,142 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
441456
442457 operations .put ("authMethods" , authMethods );
443458
459+ refreshAggregatedImportsForOperations (objs );
460+
444461 return objs ;
445462 }
446463
464+ /**
465+ * Normalizes {@link CodegenParameter#getDataType()} for enum-by-reference parameters only
466+ * ({@code param.isEnumRef}).
467+ * <p><b>Why:</b> Templates concatenate {@code modelPackage} + {@code dataType} for FQCN strings (e.g. validation /
468+ * deserialization). The API interface uses short names plus {@code use} imports, so {@code dataType} must be the
469+ * <em>short</em> PHP class name. Upstream parsing (OpenAPI 3.1 dotted keys, {@code components.parameters} {@code $ref},
470+ * or flattening) can leave {@code dataType} as a bogus single token (no {@code \}), a full FQCN, or a leading
471+ * {@code \}.
472+ * <p><b>Execution (this method):</b>
473+ * <ol>
474+ * <li>Exit if not enum ref or empty {@code dataType}.</li>
475+ * <li>Strip a leading {@code \} from the working copy.</li>
476+ * <li>If the copy contains {@code \}: if it starts with {@code modelPackage + "\\"}, keep the suffix as short name;
477+ * else if it looks like a model FQCN under another path, take {@link #extractSimpleName(String)}.</li>
478+ * <li>If there is no {@code \}: optionally strip a bogus {@code modelPackage} with all separators removed
479+ * (flat prefix) from the start, then {@link #toModelName(String)} so dotted logical names become the real
480+ * generated model class name.</li>
481+ * <li>Always finish with {@link #syncEnumRefOperationImports(CodegenOperation, CodegenParameter, String)} so
482+ * {@link CodegenOperation#imports} matches the normalized short name.</li>
483+ * </ol>
484+ * <p><b>Downstream in {@link #postProcessOperationsWithModels}:</b> after this call, {@link #getTypeHint(String, Boolean)}
485+ * and {@code x-parameter-type} use {@code modelPackage + "\\" + dataType} for enum refs so the hint matches imports.
486+ * <p><b>Aggregated imports:</b> {@link OperationsMap#setImports} is built before this hook; callers must invoke
487+ * {@link #refreshAggregatedImportsForOperations(OperationsMap)} once all operations/parameters are processed.
488+ */
489+ private void normalizeEnumRefParameterDataType (CodegenOperation op , CodegenParameter param ) {
490+ if (!param .isEnumRef || StringUtils .isEmpty (param .dataType )) {
491+ return ;
492+ }
493+ String dt = param .dataType ;
494+ if (dt .startsWith ("\\ " )) {
495+ dt = dt .substring (1 );
496+ }
497+ final String mp = modelPackage ();
498+ if (dt .contains ("\\ " )) {
499+ if (dt .startsWith (mp + "\\ " )) {
500+ param .dataType = dt .substring (mp .length () + 1 );
501+ } else if (isModelClass (dt )) {
502+ param .dataType = extractSimpleName (dt );
503+ }
504+ } else {
505+ // No backslashes: flattened invoker+model+class token, dotted logical name, or already-short class name
506+ String flatPrefix = mp .replace ("\\ " , "" );
507+ if (StringUtils .isNotEmpty (flatPrefix )
508+ && dt .startsWith (flatPrefix )
509+ && dt .length () > flatPrefix .length ()) {
510+ dt = dt .substring (flatPrefix .length ());
511+ }
512+ param .dataType = toModelName (dt );
513+ }
514+ syncEnumRefOperationImports (op , param , mp );
515+ }
516+
517+ /**
518+ * Repairs {@link CodegenOperation#imports} for one enum-ref parameter after {@link #normalizeEnumRefParameterDataType}.
519+ * <p><b>Step 1 — remove bogus entries:</b> {@code DefaultGenerator} may add a single token that is the
520+ * {@code modelPackage} string with all {@code \} removed, prefixed to the class name (still without {@code \}).
521+ * Such values are not valid PHP imports and break {@code api.mustache} {@code use} lines; drop any import string
522+ * that has no backslash, starts with that flat prefix, and is longer than the prefix alone.
523+ * <p><b>Step 2 — add the short model name:</b> if {@code param.dataType} is non-empty and {@link #needToImport(String)}
524+ * is true, add it so the operation contributes the correct short classname to the union used for template imports.
525+ */
526+ private void syncEnumRefOperationImports (CodegenOperation op , CodegenParameter param , String modelPackage ) {
527+ if (op == null || op .imports == null || StringUtils .isEmpty (modelPackage )) {
528+ return ;
529+ }
530+ String flatPrefix = modelPackage .replace ("\\ " , "" );
531+ if (StringUtils .isEmpty (flatPrefix )) {
532+ return ;
533+ }
534+ op .imports .removeIf (s ->
535+ s != null
536+ && !s .contains ("\\ " )
537+ && s .startsWith (flatPrefix )
538+ && s .length () > flatPrefix .length ());
539+ if (StringUtils .isNotEmpty (param .dataType ) && needToImport (param .dataType )) {
540+ op .imports .add (param .dataType );
541+ }
542+ }
543+
544+ /**
545+ * Recomputes the bundle-level import list exposed to Mustache ({@link OperationsMap#setImports},
546+ * {@code hasImport}) from the per-operation {@link CodegenOperation#imports} sets.
547+ * <p><b>When:</b> call once at the end of {@link #postProcessOperationsWithModels}, after every
548+ * {@link CodegenOperation} has had its parameters processed (including {@link #normalizeEnumRefParameterDataType} /
549+ * {@link #syncEnumRefOperationImports}).
550+ * <p><b>Execution:</b>
551+ * <ol>
552+ * <li>Union all strings in {@code op.imports} across operations into a sorted {@link TreeSet} (stable, de-duplicated).</li>
553+ * <li>For each symbol, resolve {@link #importMapping()} or {@link #toModelImportMap(String)} into
554+ * {@code fullQualifiedImport → shortClassName} pairs (same shape as {@code DefaultGenerator#processOperations}).</li>
555+ * <li>Build {@code {import, classname}} rows sorted by {@code classname} for the template partial that emits
556+ * {@code use Full\\Qualified;}</li>
557+ * <li>Replace {@code operationsMap} imports and set {@code hasImport}.</li>
558+ * </ol>
559+ */
560+ private void refreshAggregatedImportsForOperations (OperationsMap operationsMap ) {
561+ OperationMap operationMap = operationsMap .getOperations ();
562+ if (operationMap == null ) {
563+ return ;
564+ }
565+ List <CodegenOperation > operationList = operationMap .getOperation ();
566+ if (operationList == null ) {
567+ return ;
568+ }
569+ Set <String > allImports = new TreeSet <>();
570+ for (CodegenOperation op : operationList ) {
571+ if (op .imports != null ) {
572+ allImports .addAll (op .imports );
573+ }
574+ }
575+ Map <String , String > mapped = new LinkedHashMap <>();
576+ for (String nextImport : allImports ) {
577+ String mapping = importMapping ().get (nextImport );
578+ if (mapping != null ) {
579+ mapped .put (mapping , nextImport );
580+ } else {
581+ mapped .putAll (toModelImportMap (nextImport ));
582+ }
583+ }
584+ Set <Map <String , String >> importObjects = new TreeSet <>(Comparator .comparing (o -> o .get ("classname" )));
585+ for (Map .Entry <String , String > e : mapped .entrySet ()) {
586+ Map <String , String > row = new LinkedHashMap <>();
587+ row .put ("import" , e .getKey ());
588+ row .put ("classname" , e .getValue ());
589+ importObjects .add (row );
590+ }
591+ operationsMap .setImports (new ArrayList <>(importObjects ));
592+ operationsMap .put ("hasImport" , !importObjects .isEmpty ());
593+ }
594+
447595 private boolean isApplicationJsonOrApplicationXml (CodegenOperation op ) {
448596 if (op .produces != null ) {
449597 for (Map <String , String > produce : op .produces ) {
0 commit comments