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