@@ -138,7 +138,7 @@ protected function buildAnnotationForMethod(array $rules, string $pluginName, \R
138138 );
139139
140140 $ params = $ this ->determineParameters ($ rules , $ pluginName , $ methodName , $ reflectionMethod );
141- $ responses = $ this ->determineResponses ($ rules , $ pluginName , $ methodName );
141+ $ responses = $ this ->determineResponses ($ rules , $ pluginName , $ methodName, $ reflectionMethod );
142142
143143 $ isPost = !empty ($ rules ['plugins ' ][$ pluginName ]['methodsRequiringPost ' ])
144144 && in_array ($ methodName , $ rules ['plugins ' ][$ pluginName ]['methodsRequiringPost ' ]);
@@ -159,24 +159,57 @@ protected function getParamInfoFromDocBlock(string $docBlock): array
159159 $ name = ltrim ($ param ->parameterName , '$ ' );
160160 $ params [$ name ] = [
161161 'type ' => (string ) $ param ->type ,
162- 'desc ' => $ param ->description ,
162+ // Normalise the description. E.g. remove linebreaks and indentation
163+ 'desc ' => trim (preg_replace (['/^\h+/m ' , '/\R+/u ' ,], ['' , ' ' ], $ param ->description )),
163164 'byRef ' => $ param ->isReference ,
164165 'variadic ' => $ param ->isVariadic ,
165166 ];
166167 }
167168 return $ params ;
168169 }
169170
171+ protected function getResponseInfoFromDocBlock (string $ docBlock ): array
172+ {
173+ $ lexer = new Lexer ();
174+ $ tokens = $ lexer ->tokenize ($ docBlock );
175+ $ expressionParser = new ConstExprParser ();
176+ $ parser = new PhpDocParser (new TypeParser ($ expressionParser ), $ expressionParser );
177+ $ node = $ parser ->parse (new TokenIterator ($ tokens ));
178+
179+ $ responseInfo = ['type ' => null ];
180+ $ returnTags = $ node ->getReturnTagValues ();
181+ if (empty ($ returnTags )) {
182+ return $ responseInfo ;
183+ }
184+
185+ $ returnTag = $ returnTags [0 ];
186+ $ tagValue = strval ($ returnTag ->type );
187+ $ responseInfo ['type ' ] = $ this ->getOpenApiTypeFromPhpType ($ tagValue );
188+ if ($ responseInfo ['type ' ] === 'string ' && !empty ($ tagValue ) && strtolower ($ tagValue ) !== 'string ' ) {
189+ $ responseInfo ['type ' ] = '' ;
190+ $ responseInfo ['description ' ] = 'Response of unknown type ' ;
191+ }
192+ if (!empty ($ returnTag ->description )) {
193+ $ responseInfo ['description ' ] = $ returnTag ->description ;
194+ }
195+
196+ return $ responseInfo ;
197+ }
198+
170199 protected function buildVirtualPath (string $ virtualPathTemplate , string $ plugin , string $ method ): string
171200 {
172- return str_replace ([ '{plugin} ' , '{method} ' ], [ $ plugin , $ method ], $ virtualPathTemplate );
201+ return str_replace (['{plugin} ' , '{method} ' ], [$ plugin , $ method ], $ virtualPathTemplate );
173202 }
174203
175204 protected function buildParameterAnnotation (string $ paramName , array $ paramMetadata , array $ paramDocInfo ): array
176205 {
177206 $ docType = strtolower (trim ($ paramDocInfo ['type ' ] ?? '' ));
178207 $ metaType = strtolower (trim ($ paramMetadata ['type ' ] ?? $ docType ));
179208 $ type = $ metaType === 'string ' && $ docType !== 'string ' ? $ docType : $ metaType ;
209+ // If the signature type is array, but the type hinting provides more, use that instead
210+ if ($ type === 'array ' && strpos ($ docType , '[] ' ) !== false && strpos ($ docType , '| ' ) === false ) {
211+ $ type = $ docType ;
212+ }
180213 $ typesMap = [];
181214 // Check for pipes and try to list possible types
182215 foreach (explode ('| ' , $ type ) as $ typePart ) {
@@ -197,7 +230,7 @@ protected function buildParameterAnnotation(string $paramName, array $paramMetad
197230 'types ' => $ typesMap ,
198231 'description ' => $ paramDocInfo ['desc ' ] ?? '' ,
199232 'required ' => $ isRequired ? 'true ' : 'false ' ,
200- 'default ' => !$ isRequired ? json_encode ($ paramMetadata ['default ' ]) : '' ,
233+ 'default ' => !$ isRequired ? json_encode ($ paramMetadata ['default ' ]) : NoDefaultValue::class ,
201234 ];
202235 }
203236
@@ -300,8 +333,8 @@ protected function getApplicableDemoExampleUrls(string $pluginName, string $meth
300333
301334 protected function getExampleIfAvailable (string $ url ): array
302335 {
303- // Simply return the URL for TSV
304- if (stripos ($ url , 'format=tsv ' ) ! == false ) {
336+ // Simply return the URL for anything other than JSON until we figure out how to better format those examples
337+ if (stripos ($ url , 'format=json ' ) = == false ) {
305338 return ['externalValue ' => $ url ];
306339 }
307340
@@ -319,16 +352,10 @@ protected function getExampleIfAvailable(string $url): array
319352 curl_close ($ ch );
320353
321354 // If the example didn't load or is too big, simply include the URL instead of the string value
322- if ($ body === false || $ status !== 200 || strlen ($ body ) > 1000 || strpos ($ body , 'Error: ' ) === 0 ) {
355+ if ($ body === false || $ status !== 200 || strlen ($ body ) > 2000 || strpos ($ body , 'Error: ' ) === 0 ) {
323356 return ['externalValue ' => $ url ];
324357 }
325358
326- // Clean up XML formatting a bit
327- $ body = trim ($ body );
328- if (stripos ($ url , 'format=xml ' ) !== false ) {
329- $ body = str_replace (['<?xml version="1.0" encoding="utf-8" ?> ' , "\n" , "\t" , '" ' ], ['' , '' , '' , '\" ' ], $ body );
330- }
331-
332359 // The annotation expects an objects and not arrays
333360 if (stripos ($ url , 'format=json ' ) !== false && stripos ($ body , '[ ' ) === 0 ) {
334361 $ body = str_replace (['[ ' , '] ' ], ['{ ' , '} ' ], $ body );
@@ -337,21 +364,61 @@ protected function getExampleIfAvailable(string $url): array
337364 return ['value ' => $ body ];
338365 }
339366
340- protected function determineResponses (array $ rules , string $ plugin , string $ method ): array
367+ protected function determineResponses (array $ rules , string $ plugin , string $ method, \ ReflectionMethod $ reflectionMethod ): array
341368 {
342369 $ responses = [];
343370
344- // TODO - Try to determine the success response using the return type and/or doc-block return type
371+ // Try to determine the success response using the return type and/or doc-block return type
372+ $ returnType = $ reflectionMethod ->getReturnType ();
373+ $ responseInfo = $ this ->getResponseInfoFromDocBlock ($ reflectionMethod ->getDocComment ());
374+ if (!empty ($ returnType ) && $ returnType ->isBuiltin ()) {
375+ $ responseInfo ['type ' ] = $ this ->getOpenApiTypeFromPhpType (strval ($ returnType ));
376+ }
345377
346378 $ successRef = null ;
347379 $ successArray = ['code ' => 200 ];
348380 if (isset ($ rules ['plugins ' ][$ plugin ]['successResponseByMethod ' ][$ method ])) {
349381 $ successRef = $ rules ['plugins ' ][$ plugin ]['successResponseByMethod ' ][$ method ];
350382 }
383+ // TODO - See if there's a way to auto-handle custom objects, especially common stuff like DataTable\DataTableInterface
351384 if ($ successRef ) {
352385 $ successArray ['ref ' ] = $ successRef ;
353386 }
354387
388+ // If the return type is void, use the generic response type
389+ if (empty ($ successArray ['ref ' ]) && !empty ($ returnType ) && strval ($ returnType ) === 'void ' ) {
390+ $ successArray ['ref ' ] = '#/components/responses/GenericSuccessNoBody ' ;
391+ }
392+
393+ // If it's a generic type and there's no custom description, use one of the global generic responses
394+ if (empty ($ successArray ['ref ' ]) && !empty ($ responseInfo ['type ' ]) && empty ($ responseInfo ['description ' ])) {
395+ $ ref = '' ;
396+ switch ($ responseInfo ['type ' ]) {
397+ case 'array ' :
398+ $ ref = '#/components/responses/GenericArray ' ;
399+ break ;
400+ case 'integer ' :
401+ $ ref = '#/components/responses/GenericInteger ' ;
402+ break ;
403+ case 'boolean ' :
404+ $ ref = '#/components/responses/GenericBoolean ' ;
405+ break ;
406+ case 'string ' :
407+ $ ref = '#/components/responses/GenericString ' ;
408+ break ;
409+ }
410+
411+ if (!empty ($ ref )) {
412+ $ successArray ['ref ' ] = $ ref ;
413+ }
414+ }
415+
416+ if (!empty ($ responseInfo ['description ' ])) {
417+ $ successArray ['desc ' ] = $ responseInfo ['description ' ];
418+ }
419+
420+ $ responseSchema = !empty ($ responseInfo ['type ' ]) ? $ this ->buildSchemaObjectArray ($ responseInfo ['type ' ]) : [];
421+
355422 $ mediaTypes = [];
356423 // This simply reuses the example URLs used by the current documentation, but some endpoints don't work because authentication is required
357424 // TODO - Come up with a way to demo examples for endpoints which require authentication. E.g. hit a live endpoint server-side and replace any potentially sensitive data...
@@ -370,13 +437,21 @@ protected function determineResponses(array $rules, string $plugin, string $meth
370437 $ value = substr ($ value , 1 , -1 );
371438 }
372439 $ exampleProperties [] = $ valueKey . '= ' . $ value ;
373- $ mediaTypes [] = [
440+ $ mediaType = [
374441 'mediaType=" ' . $ contentType . '" ' ,
375442 '@OA\Examples ' => $ exampleProperties ,
376443 ];
444+ // If a type was found, add it as a schema to the media type
445+ if (!empty ($ responseSchema )) {
446+ $ mediaType = array_merge ($ mediaType , $ responseSchema );
447+ }
448+ $ mediaTypes [] = $ mediaType ;
377449 }
378450 if (!empty ($ mediaTypes )) {
379451 $ successArray ['mediaTypes ' ] = $ mediaTypes ;
452+ } else {
453+ // Make sure the schema is included in there are no examples
454+ $ successArray ['schema ' ] = $ responseSchema ;
380455 }
381456
382457 $ responses [] = $ successArray ;
@@ -434,7 +509,7 @@ protected function buildLinesForAnnotationObject(string $objectName, array $obje
434509 return array_merge ([$ indentString . $ objectName . $ openingCharacter ], $ lines , [$ indentString . $ closingCharacter . ', ' ]);
435510 }
436511
437- protected function buildSchemaObjectArray (string $ type , string $ subType = '' , string $ default = '' ): array
512+ protected function buildSchemaObjectArray (string $ type , string $ subType = '' , string $ default = NoDefaultValue::class ): array
438513 {
439514 $ schemaMap = ['type=" ' . $ type . '" ' ];
440515 $ subTypeString = '' ;
@@ -448,14 +523,32 @@ protected function buildSchemaObjectArray(string $type, string $subType = '', st
448523 }
449524 }
450525
451- if ($ default !== '' ) {
452- // TODO - Add some logic to only add default if it matches the type. E.g. false isn't a good default for string
453- $ schemaMap [] = 'default=" ' . $ default . '" ' ;
526+ if ($ this ->shouldIncludeDefault ($ type , $ default )) {
527+ $ doubleQuote = '" ' ;
528+ // Don't wrap with quotes for certain values
529+ if (in_array ($ default , ['{} ' , 'false ' , 'true ' , "{$ doubleQuote }{$ doubleQuote }" ])) {
530+ $ doubleQuote = '' ;
531+ }
532+ $ schemaMap [] = "default= {$ doubleQuote }{$ default }{$ doubleQuote }" ;
454533 }
455534
456535 return ['@OA\Schema ' => $ schemaMap ];
457536 }
458537
538+ protected function shouldIncludeDefault (string $ type , string $ default = NoDefaultValue::class): bool
539+ {
540+ if ($ default === NoDefaultValue::class) {
541+ return false ;
542+ }
543+
544+ // Don't use true or false for default if it's not a boolean type
545+ if ($ type !== 'boolean ' && in_array (strtolower ($ default ), ['false ' , 'true ' ])) {
546+ return false ;
547+ }
548+
549+ return true ;
550+ }
551+
459552 protected function buildSchemaObjectArrays (array $ typesMap , string $ default = '' ): array
460553 {
461554 $ schemas = [];
@@ -493,7 +586,8 @@ protected function compileOperationLines(string $path, string $opId, string $plu
493586 $ operationValuesMap [] = ['@OA\Parameter ' => $ paramMap ];
494587 }
495588 foreach ($ responses as $ response ) {
496- if (isset ($ response ['ref ' ])) {
589+ // Don't use the reference if there are media type examples
590+ if (isset ($ response ['ref ' ]) && empty ($ response ['mediaTypes ' ])) {
497591 $ code = $ response ['code ' ];
498592 $ codeFormatted = is_numeric ($ code ) ? (string )$ code : '" ' . $ code . '" ' ;
499593 $ operationValuesMap [] = '@OA\Response(response= ' . $ codeFormatted . ', ref=" ' . $ response ['ref ' ] . '") ' ;
@@ -502,6 +596,9 @@ protected function compileOperationLines(string $path, string $opId, string $plu
502596 'response=200 ' ,
503597 'description=" ' . ($ response ['desc ' ] ?? 'OK ' ) . '" ' ,
504598 ];
599+ if (!empty ($ response ['schema ' ])) {
600+ $ responsePropertyArray = array_merge ($ responsePropertyArray , $ response ['schema ' ]);
601+ }
505602 if (isset ($ response ['mediaTypes ' ]) && is_array ($ response ['mediaTypes ' ])) {
506603 foreach ($ response ['mediaTypes ' ] as $ mediaType ) {
507604 $ responsePropertyArray [] = ['@OA\MediaType ' => $ mediaType ];
@@ -510,7 +607,8 @@ protected function compileOperationLines(string $path, string $opId, string $plu
510607 $ operationValuesMap [] = ['@OA\Response ' => $ responsePropertyArray ];
511608 }
512609 }
513- $ operationValuesMap [] = 'x={"runtime"={"entry":"index.php","query":{"module":"API","method":" ' . $ plugin . '. ' . $ method . '"}}} ' ;
610+ // TODO - Remove this if it's determined that we won't ever use it
611+ //$operationValuesMap[] = 'x={"runtime"={"entry":"index.php","query":{"module":"API","method":"' . $plugin . '.' . $method . '"}}}';
514612
515613 $ lines = $ this ->buildLinesForAnnotationObject ('@OA \\' . ($ isPost ? 'Post ' : 'Get ' ), $ operationValuesMap );
516614
0 commit comments