Skip to content

Commit e4436d1

Browse files
authored
Merge pull request #19 from matomo-org/PG-4883-update-swagger-generator
Update Swagger Generator to read php function & class docblocks, #PG-4883
2 parents e143f42 + c49a949 commit e4436d1

5 files changed

Lines changed: 82 additions & 18 deletions

File tree

Annotations/AnnotationGenerator.php

Lines changed: 74 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Piwik\Plugins\OpenApiDocs\Annotations;
1313

14+
use Matomo\Dependencies\OpenApiDocs\phpDocumentor\Reflection\DocBlock\Description;
1415
use Matomo\Dependencies\OpenApiDocs\phpDocumentor\Reflection\DocBlock\Tags\Param;
1516
use Matomo\Dependencies\OpenApiDocs\phpDocumentor\Reflection\DocBlock\Tags\TagWithType;
1617
use Matomo\Dependencies\OpenApiDocs\phpDocumentor\Reflection\DocBlockFactory;
@@ -121,15 +122,15 @@ public function generatePluginApiAnnotations(string $pluginName, bool $writeToFi
121122
$pluginMetadata = Proxy::getInstance()->getMetadata()[$className] ?? [];
122123

123124
$annotations = [[sprintf('@OA\Tag(name="%s")', $pluginName)]];
124-
// I decided to not include the description in the tag annotation so that it automatically pulls the API class comment as the description.
125-
// if (!empty($pluginMetadata['__documentation'])) {
126-
// $tagLines = $this->buildLinesForAnnotationObject('@OA\Tag', [
127-
// sprintf('name="%s"', $pluginName),
128-
// sprintf('description="%s"', $this->normaliseDescriptionText($pluginMetadata['__documentation'])),
129-
// ]);
130-
// $this->removeTrailingCommaFromLastLine($tagLines);
131-
// $annotations[] = $tagLines;
132-
// }
125+
126+
if (!empty($pluginMetadata['__documentation'])) {
127+
$tagLines = $this->buildLinesForAnnotationObject('@OA\Tag', [
128+
sprintf('name="%s"', $pluginName),
129+
sprintf('description="%s"', $this->normaliseDescriptionText($pluginMetadata['__documentation'])),
130+
]);
131+
$this->removeTrailingCommaFromLastLine($tagLines);
132+
$annotations[] = $tagLines;
133+
}
133134

134135
foreach (array_keys($pluginMetadata) as $metadataMethod) {
135136
if (!$reflectionClass->hasMethod($metadataMethod)) {
@@ -254,11 +255,12 @@ protected function buildAnnotationForMethod(array $rules, string $pluginName, \R
254255

255256
$params = $this->determineParameters($rules, $pluginName, $methodName, $reflectionMethod);
256257
$responses = $this->determineResponses($rules, $pluginName, $methodName, $reflectionMethod, $params);
258+
$description = $this->determineDescription($pluginName, $methodName, $reflectionMethod);
257259

258260
$isPost = !empty($rules['plugins'][$pluginName]['methodsRequiringPost'])
259261
&& in_array($methodName, $rules['plugins'][$pluginName]['methodsRequiringPost']);
260262

261-
return $this->compileOperationLines($path, $opId, $pluginName, $params, $responses, $isPost);
263+
return $this->compileOperationLines($path, $opId, $pluginName, $params, $responses, $description, $isPost);
262264
}
263265

264266
/**
@@ -354,6 +356,24 @@ public function getResponseInfoFromDocBlock(string $docBlock): array
354356
}
355357

356358
/**
359+
* Extract the description/summary from a given docblock
360+
*
361+
* @param string $docBlock The comment block from a method, which hopefully contains a description.
362+
*
363+
* @return string Description/summary extracted from the docblock
364+
*/
365+
public function getDescriptionFromDocBlock(string $docBlock): string
366+
{
367+
$factory = DocBlockFactory::createInstance();
368+
$docBlockObject = $factory->create($docBlock);
369+
370+
return $docBlockObject->getSummary();
371+
}
372+
373+
374+
375+
376+
/**
357377
* This is a helper method for building the path used for an operation annotation. It takes a path template, like
358378
* the one from the config array and populates it with the plugin name and API method name.
359379
*
@@ -449,12 +469,17 @@ public function buildParameterAnnotationData(string $methodName, string $paramNa
449469
// Clean up the descriptions a little more like removing linebreaks and escaping double-quotes
450470
$description = $this->normaliseDescriptionText($description);
451471

472+
$default = $paramMetadata['default'] ?? null;
473+
if (!is_string($default)) {
474+
$default = json_encode($default);
475+
}
476+
452477
return [
453478
'name' => $paramName,
454479
'types' => $typesMap,
455480
'description' => $description,
456481
'required' => $isRequired ? 'true' : 'false',
457-
'default' => !$isRequired ? json_encode($paramMetadata['default']) : NoDefaultValue::class,
482+
'default' => !$isRequired ? $default : NoDefaultValue::class,
458483
'example' => $example,
459484
];
460485
}
@@ -578,6 +603,30 @@ protected function determineParameters(array $rules, string $plugin, string $met
578603
];
579604
}
580605

606+
607+
608+
609+
/**
610+
* Get the description/summary of a given method
611+
*
612+
* @param string $plugin Name of the plugin. E.g. TagManager.
613+
* @param string $method The name of the method being annotated.
614+
* @param \ReflectionMethod $reflectionMethod The reflective representation of the method to provide metadata.
615+
*
616+
* @return string Description of the method
617+
*/
618+
protected function determineDescription(string $plugin, string $method, \ReflectionMethod $reflectionMethod): string
619+
{
620+
$description = '';
621+
$docBlock = $reflectionMethod->getDocComment();
622+
if (!empty($docBlock)) {
623+
$description = $this->getDescriptionFromDocBlock($docBlock);
624+
}
625+
626+
return $description;
627+
}
628+
629+
581630
/**
582631
* Map the PHP type to the OpenAPI type. The currently available types for v3.1.1 are the following: “null”,
583632
* “boolean”, “object”, “array”, “number”, “string”, or “integer”.
@@ -614,6 +663,9 @@ public function getOpenApiTypeFromPhpType(string $type): string
614663
case 'double':
615664
$type = 'number';
616665
break;
666+
case 'void':
667+
$type = 'null';
668+
break;
617669
default:
618670
$type = 'string';
619671
}
@@ -1037,8 +1089,13 @@ protected function determineResponses(array $rules, string $plugin, string $meth
10371089
$successArray['ref'] = '#/components/responses/GenericSuccess';
10381090
}
10391091

1092+
$description = $responseInfo['description'] ?? null;
1093+
if ($description instanceof Description) {
1094+
$description = $description->getBodyTemplate();
1095+
}
1096+
10401097
// If it's a generic type and there's no custom description, use one of the global generic responses
1041-
if (empty($successArray['ref']) && !empty($responseInfo['type']) && empty($responseInfo['description'])) {
1098+
if (empty($successArray['ref']) && !empty($responseInfo['type']) && empty($description)) {
10421099
$ref = '';
10431100
switch ($responseInfo['type']) {
10441101
case 'array':
@@ -1053,6 +1110,8 @@ protected function determineResponses(array $rules, string $plugin, string $meth
10531110
case 'string':
10541111
$ref = '#/components/responses/GenericString';
10551112
break;
1113+
case 'null':
1114+
$ref = '#/components/responses/GenericSuccess';
10561115
}
10571116

10581117
if (!empty($ref)) {
@@ -1713,16 +1772,18 @@ public function buildSchemaObjectArrays(array $typesMap, string $default = '', s
17131772
* @param string $plugin The name of the plugin. E.g. CustomReports
17141773
* @param array $params The compiled list of method parameters and key information about them, like type.
17151774
* @param array $responses compiled list of method expected responses and key information about them, like type.
1775+
* @param string $description The method level description
17161776
* @param bool $isPost Indicates whether the operation is a POST. The default is false, meaning it's GET.
17171777
*
17181778
* @return string[] The array of all the lines of the operation annotation object.
17191779
*/
1720-
public function compileOperationLines(string $path, string $opId, string $plugin, array $params, array $responses, bool $isPost = false): array
1780+
public function compileOperationLines(string $path, string $opId, string $plugin, array $params, array $responses, string $description, bool $isPost = false): array
17211781
{
17221782
$operationValuesMap = [
17231783
'path="' . $path . '"',
17241784
'operationId="' . $opId . '"',
17251785
'tags={"' . $plugin . '"}',
1786+
'description="' . $this->normaliseDescriptionText($description) . '"',
17261787
];
17271788
foreach ($params['refs'] ?? [] as $ref) {
17281789
$operationValuesMap[] = '@OA\Parameter(ref="' . $ref . '")';

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
## Changelog
22

3+
4+
5.0.1-b1
5+
- 16-02-2026 - Initial implementation of plugin and POC generating documentation from annotations
6+
37
5.0.0-b1
48
- Initial implementation of plugin and POC generating documentation from annotations

Specs/SpecGenerator.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,7 @@ public function generateSpec(array $pluginNames, string $format = 'json', string
7272
BaseValidator::check('pluginName', $pluginName, [new NotEmpty()]);
7373
Manager::getInstance()->checkIsPluginActivated($pluginName);
7474

75-
$pluginDir = Manager::getInstance()::getPluginDirectory($pluginName);
76-
$pluginAnnotationsSource = $pluginDir . '/API.php';
75+
$pluginAnnotationsSource = $currentPluginDir . '/tmp/annotations/' . $pluginName . 'GeneratedAnnotations.php';
7776
try {
7877
$openapi = (new Generator(StaticContainer::get(NullLogger::class)))->generate([
7978
$pluginAnnotationsSource,

plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "OpenApiDocs",
33
"description": "Generate API documentation for Matomo in the OpenAPI format.",
4-
"version": "5.0.0-b1",
4+
"version": "5.0.1-b1",
55
"theme": false,
66
"keywords": [
77
"API",

tests/Unit/AnnotationGeneratorTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,7 @@ public function getTestDataForBuildParameterAnnotationData(): iterable
435435
'types' => ['string' => null],
436436
'description' => '',
437437
'required' => 'false',
438-
'default' => '"SomeDefaultValue"',
438+
'default' => 'SomeDefaultValue',
439439
'example' => '',
440440
]];
441441
yield 'should not wrap metadata default value when boolean type' => ['someParam', [
@@ -467,7 +467,7 @@ public function getTestDataForBuildParameterAnnotationData(): iterable
467467
'types' => ['string' => null],
468468
'description' => '',
469469
'required' => 'false',
470-
'default' => '""',
470+
'default' => '',
471471
'example' => '',
472472
]];
473473
yield 'should not count the NoDefaultValue class as a default value' => ['someParam', [

0 commit comments

Comments
 (0)