@@ -99,6 +99,11 @@ class AnnotationGenerator
9999 */
100100 protected $ allowLocalRequests ;
101101
102+ /**
103+ * @var array<string, mixed>|null
104+ */
105+ protected $ parameterExamples ;
106+
102107 public function __construct (
103108 DocumentationGenerator $ generator ,
104109 ?PathResolver $ pathResolver = null ,
@@ -110,6 +115,7 @@ public function __construct(
110115 $ this ->artifactWriter = $ artifactWriter ?? new ArtifactWriter ();
111116 $ this ->missingImportantDataWarnings = [];
112117 $ this ->allowLocalRequests = $ allowLocalRequests ;
118+ $ this ->parameterExamples = null ;
113119 $ this ->currentPluginDir = Manager::getInstance ()::getPluginDirectory ('OpenApiDocs ' );
114120 }
115121
@@ -455,7 +461,7 @@ public function buildParameterAnnotationData(string $methodName, string $paramNa
455461 // Sometimes, doc-block can wrap type hinting with parenthesis. Remove them.
456462 $ type = trim ($ type , '() ' );
457463 // If the signature type is array, but the type hinting provides more, use that instead
458- if ($ type === 'array ' && strpos ($ docType, ' [] ' ) !== false && strpos ($ docType , '| ' ) === false ) {
464+ if ($ type === 'array ' && $ this -> hasSpecificArrayShape ($ docType) && strpos ($ docType , '| ' ) === false ) {
459465 $ type = $ docType ;
460466 }
461467 $ typesMap = [];
@@ -468,6 +474,7 @@ public function buildParameterAnnotationData(string $methodName, string $paramNa
468474 $ typeHints = array_diff ($ typeHints , ['bool ' ]);
469475 }
470476
477+ $ isRequired = !key_exists ('default ' , $ paramMetadata ) || $ paramMetadata ['default ' ] instanceof NoDefaultValue;
471478 $ allTypeHintsAreStringLiterals = $ this ->areAllTypeHintsStringLiterals ($ typeHints );
472479 $ enumValues = [];
473480 if ($ allTypeHintsAreStringLiterals ) {
@@ -478,17 +485,13 @@ public function buildParameterAnnotationData(string $methodName, string $paramNa
478485 } else {
479486 foreach ($ typeHints as $ typePart ) {
480487 $ typePart = trim ($ typePart , ' () ' );
481- $ normalisedType = $ this ->getOpenApiTypeFromPhpType ($ typePart );
488+ $ normalisedType = $ this ->hasSpecificArrayShape ( $ typePart ) ? ' array ' : $ this -> getOpenApiTypeFromPhpType ($ typePart );
482489 // If the type is array, check if there's a subType
483- $ subType = null ;
484- if ($ normalisedType === 'array ' && $ typePart !== 'array ' && strpos ($ typePart , '[] ' ) !== false ) {
485- $ subType = substr ($ typePart , 0 , strpos ($ typePart , '[] ' ));
486- }
490+ $ subType = $ this ->getArraySubTypeFromPhpType ($ typePart , $ normalisedType );
487491 $ typesMap [$ normalisedType ] = $ subType !== null ? $ this ->getOpenApiTypeFromPhpType ($ subType ) : $ subType ;
488492 }
489493 }
490494
491- $ isRequired = !key_exists ('default ' , $ paramMetadata ) || $ paramMetadata ['default ' ] instanceof NoDefaultValue;
492495 $ description = $ paramDocInfo ['description ' ] ?? '' ;
493496 if (empty ($ description )) {
494497 $ this ->addMissingImportantDataWarning ($ methodName , $ paramName , 'Description is not specified in comment block. ' );
@@ -506,11 +509,20 @@ public function buildParameterAnnotationData(string $methodName, string $paramNa
506509 $ example = trim ($ example , '" ' );
507510 }
508511
512+ if ($ isRequired && $ example === '' ) {
513+ $ configExample = $ this ->getParameterExampleFromConfig ($ paramName , $ type , $ typesMap );
514+ if ($ configExample !== null ) {
515+ $ example = $ configExample ;
516+ }
517+ }
518+
509519 // Clean up the descriptions a little more like removing linebreaks and escaping double-quotes
510520 $ description = $ this ->normaliseDescriptionText ($ description );
511521
512522 $ default = $ paramMetadata ['default ' ] ?? null ;
513- if (!is_string ($ default )) {
523+ if ($ default === null ) {
524+ $ default = NoDefaultValue::class;
525+ } elseif (!is_string ($ default )) {
514526 $ default = json_encode ($ default );
515527 }
516528
@@ -555,6 +567,138 @@ protected function areAllTypeHintsStringLiterals(array $typeHints): bool
555567 return true ;
556568 }
557569
570+ protected function hasSpecificArrayShape (string $ type ): bool
571+ {
572+ return strpos ($ type , '[] ' ) !== false || preg_match ('/^(array|list)<.+>$/ ' , trim ($ type )) === 1 ;
573+ }
574+
575+ protected function getArraySubTypeFromPhpType (string $ typePart , string $ normalisedType ): ?string
576+ {
577+ if ($ normalisedType !== 'array ' || $ typePart === 'array ' ) {
578+ return null ;
579+ }
580+
581+ if (strpos ($ typePart , '[] ' ) !== false ) {
582+ return substr ($ typePart , 0 , strpos ($ typePart , '[] ' ));
583+ }
584+
585+ if (preg_match ('/^(array|list)<(.+)>$/ ' , trim ($ typePart ), $ matches ) !== 1 ) {
586+ return null ;
587+ }
588+
589+ $ genericParts = array_map ('trim ' , explode (', ' , $ matches [2 ], 2 ));
590+ if (count ($ genericParts ) === 1 ) {
591+ return $ genericParts [0 ];
592+ }
593+
594+ return $ genericParts [1 ];
595+ }
596+
597+ /**
598+ * Load and return the configured parameter examples.
599+ *
600+ * @return array<string, mixed>
601+ */
602+ protected function getParameterExamplesConfig (): array
603+ {
604+ if ($ this ->parameterExamples !== null ) {
605+ return $ this ->parameterExamples ;
606+ }
607+
608+ $ configPath = $ this ->currentPluginDir . '/config/ParameterExamples.php ' ;
609+ if (!is_file ($ configPath )) {
610+ $ this ->parameterExamples = [];
611+ return $ this ->parameterExamples ;
612+ }
613+
614+ $ config = require $ configPath ;
615+ $ this ->parameterExamples = is_array ($ config ) ? $ config : [];
616+
617+ return $ this ->parameterExamples ;
618+ }
619+
620+ /**
621+ * Return a config-backed example string if the configured example is intentionally simple enough to support.
622+ */
623+ protected function getParameterExampleFromConfig (string $ paramName , string $ type , array $ typesMap ): ?string
624+ {
625+ $ config = $ this ->getParameterExamplesConfig ();
626+ $ keysToTry = [
627+ $ paramName . ': ' . $ type ,
628+ $ paramName . ': ' . preg_replace ('/\s+/ ' , '' , $ type ),
629+ ];
630+
631+ $ configValue = null ;
632+ $ foundConfigValue = false ;
633+ foreach (array_unique ($ keysToTry ) as $ key ) {
634+ if (!array_key_exists ($ key , $ config )) {
635+ continue ;
636+ }
637+
638+ $ configValue = $ config [$ key ];
639+ $ foundConfigValue = true ;
640+ break ;
641+ }
642+
643+ if (!$ foundConfigValue ) {
644+ return null ;
645+ }
646+
647+ return $ this ->normaliseConfiguredParameterExample ($ configValue , $ typesMap );
648+ }
649+
650+ /**
651+ * Convert supported scalar/basic-array config values into the string form used by schema generation.
652+ */
653+ protected function normaliseConfiguredParameterExample ($ example , array $ typesMap = []): ?string
654+ {
655+ if (is_bool ($ example )) {
656+ return $ example ? 'true ' : 'false ' ;
657+ }
658+
659+ if (is_int ($ example ) || is_float ($ example ) || is_string ($ example )) {
660+ return strval ($ example );
661+ }
662+
663+ if (
664+ !is_array ($ example )
665+ || !$ this ->isBasicExampleArray ($ example )
666+ || !$ this ->supportsBasicArrayExample ($ typesMap )
667+ ) {
668+ return null ;
669+ }
670+
671+ $ encoded = json_encode (array_values ($ example ));
672+
673+ return is_string ($ encoded ) ? $ encoded : null ;
674+ }
675+
676+ /**
677+ * Only use array config examples when the emitted schema includes an array shape.
678+ */
679+ protected function supportsBasicArrayExample (array $ typesMap ): bool
680+ {
681+ return array_key_exists ('array ' , $ typesMap );
682+ }
683+
684+ /**
685+ * Only support flat indexed arrays of scalar values for now.
686+ */
687+ protected function isBasicExampleArray (array $ example ): bool
688+ {
689+ if (array_values ($ example ) !== $ example ) {
690+ return false ;
691+ }
692+
693+ foreach ($ example as $ item ) {
694+ if (!is_bool ($ item ) && !is_int ($ item ) && !is_float ($ item ) && !is_string ($ item )) {
695+ return false ;
696+ }
697+ }
698+
699+ return true ;
700+ }
701+
558702 /**
559703 * Take description text and normalise it. This includes trimming surrounding whitespace, removing newlines and
560704 * escaping double-quote characters.
@@ -1863,7 +2007,7 @@ public function buildSchemaObjectArray(string $type, string $subType = '', strin
18632007 */
18642008 public function wrapStringWithQuotes (string $ string , string $ type , string $ quoteCharacter = '" ' ): string
18652009 {
1866- if (in_array ($ type , ['integer ' , 'boolean ' , 'array ' ])) {
2010+ if (in_array ($ type , ['integer ' , 'number ' , ' boolean ' , 'array ' ])) {
18672011 return $ string ;
18682012 }
18692013
@@ -1972,12 +2116,17 @@ public function compileOperationLines(string $path, string $opId, string $plugin
19722116 $ paramMap [] = 'description=" ' . $ param ['description ' ] . '" ' ;
19732117 }
19742118 $ exampleString = $ param ['example ' ];
1975- if (in_array ('array ' , array_keys ($ param ['types ' ]))) {
2119+ $ useParameterLevelExample = $ this ->shouldUseParameterLevelExample ($ param ['types ' ], $ exampleString );
2120+ if (in_array ('array ' , array_keys ($ param ['types ' ])) && !$ useParameterLevelExample ) {
19762121 // The annotation expects example objects and not arrays, so replace [] with {}
19772122 $ exampleString = str_replace (['[ ' , '] ' ], ['{ ' , '} ' ], $ exampleString );
19782123 // Escape quotes differently for the annotation examples
19792124 $ exampleString = str_replace ('\" ' , '"" ' , $ exampleString );
19802125 }
2126+ if ($ useParameterLevelExample ) {
2127+ $ paramMap [] = 'example=" ' . $ this ->normaliseDescriptionText ($ exampleString ) . '" ' ;
2128+ $ exampleString = '' ;
2129+ }
19812130 $ paramMap [] = $ this ->buildSchemaObjectArrays (
19822131 $ param ['types ' ],
19832132 strval ($ param ['default ' ]),
@@ -2019,4 +2168,18 @@ public function compileOperationLines(string $path, string $opId, string $plugin
20192168 $ this ->removeTrailingCommaFromLastLine ($ lines );
20202169 return $ lines ;
20212170 }
2171+
2172+ /**
2173+ * Use a parameter-level string example for scalar/array unions so Swagger UI can show a concrete query value.
2174+ *
2175+ * @param array<string, string|null> $typesMap
2176+ */
2177+ protected function shouldUseParameterLevelExample (array $ typesMap , string $ example ): bool
2178+ {
2179+ if (count ($ typesMap ) <= 1 || !array_key_exists ('array ' , $ typesMap ) || $ example === '' ) {
2180+ return false ;
2181+ }
2182+
2183+ return is_array (json_decode ($ example , true ));
2184+ }
20222185}
0 commit comments