diff --git a/Annotations/AnnotationGenerator.php b/Annotations/AnnotationGenerator.php
index 4e045e4..0bc6917 100644
--- a/Annotations/AnnotationGenerator.php
+++ b/Annotations/AnnotationGenerator.php
@@ -239,16 +239,8 @@ protected function writeAnnotationsToFile(array $annotations, string $filePath,
*/
protected function buildAnnotationForMethod(array $rules, string $pluginName, \ReflectionMethod $reflectionMethod): array
{
- $existing = $reflectionMethod->getDocComment();
// Skip methods which have been marked as internal or auto annotations disabled
- if (
- $existing !== false
- && (
- stripos($existing, '@internal') !== false
- || stripos($existing, '@hide') !== false
- || stripos($existing, '@deprecated') !== false
- )
- ) {
+ if (self::shouldApiMethodBeIgnored($reflectionMethod)) {
return [];
}
@@ -269,6 +261,33 @@ protected function buildAnnotationForMethod(array $rules, string $pluginName, \R
return $this->compileOperationLines($path, $opId, $pluginName, $params, $responses, $isPost);
}
+ /**
+ * Check whether the method should be included in public documentation, or it's been marked as internal or similar.
+ *
+ * @param \ReflectionMethod $reflectionMethod Reflection method used to check the comment block for annotations.
+ *
+ * @return bool Whether the API method should be ignored while generating documentation, like being marked as
+ * internal, hide, deprecated, etc.
+ */
+ public static function shouldApiMethodBeIgnored(\ReflectionMethod $reflectionMethod): bool
+ {
+ $existing = $reflectionMethod->getDocComment();
+ // Skip methods which have been marked as internal or hide
+ if (
+ $existing !== false
+ && (
+ stripos($existing, '@internal') !== false
+ || stripos($existing, '@hide') !== false
+ || stripos($existing, '@deprecated') !== false
+ || stripos($existing, '@ignore') !== false
+ )
+ ) {
+ return true;
+ }
+
+ return false;
+ }
+
/**
* Try to extract the list of parameters and key information about them from the method's doc block string.
*
diff --git a/Annotations/ApiMethodInfoExtractor.php b/Annotations/ApiMethodInfoExtractor.php
new file mode 100644
index 0000000..d735a5b
--- /dev/null
+++ b/Annotations/ApiMethodInfoExtractor.php
@@ -0,0 +1,188 @@
+checkIsPluginActivated($plugin);
+
+ $className = Request::getClassNameAPI($plugin);
+
+ $result = $this->buildEndpointInfoArrayForApiClass($className);
+ if (!empty($result)) {
+ $methodInfoArray = array_merge($methodInfoArray, $result);
+ }
+ }
+ $methodInfoString = json_encode($methodInfoArray);
+
+ $fileBaseName = 'matomo';
+ // If there's only one plugin, name the spec after the plugin
+ if (count($pluginNames) === 1) {
+ $fileBaseName = $pluginNames[0];
+ }
+
+ if ($writeToFile) {
+ $pluginSpecPath = $currentPluginDir . OpenApiDocs::GENERATED_ANNOTATIONS_PATH . $fileBaseName . '_api_method_info.json';
+ file_put_contents($pluginSpecPath, $methodInfoString);
+ }
+
+ return $methodInfoString;
+ }
+
+ /**
+ * Build the array of info about all the public API endpoints for a specific API class. It uses reflection and the
+ * metadata from the Proxy class used to build documentation.
+ *
+ * @param string $apiClassName Full name of the class which can be used to instantiate a ReflectionClass.
+ *
+ * @return array[] Collection of key details about each public API endpoint in the specified class.
+ */
+ public function buildEndpointInfoArrayForApiClass(string $apiClassName): array
+ {
+ try {
+ $reflectionClass = new \ReflectionClass($apiClassName);
+ } catch (\ReflectionException $e) {
+ throw new \RuntimeException('Unable to get reflection class for API class: ' . $apiClassName, 0, $e);
+ }
+
+ Proxy::getInstance()->registerClass($apiClassName);
+ $pluginMetadata = Proxy::getInstance()->getMetadata()[$apiClassName] ?? [];
+
+ // The proxy has already determined which methods should be ignored, so we just use the list it came up with.
+ $methodInfoArray = [];
+ foreach ($pluginMetadata as $methodName => $metadataMethod) {
+ if ($methodName === '__documentation') {
+ continue;
+ }
+ $result = $this->buildJsonInfoForApiMethod($reflectionClass, $methodName, $metadataMethod);
+ if (!empty($result)) {
+ $methodInfoArray[] = $result;
+ }
+ }
+
+ return $methodInfoArray;
+ }
+
+ /**
+ * Build the array of info about a specific API endpoint.
+ *
+ * @param \ReflectionClass $reflectionClass Reflection class used to get the method data defined in the signature.
+ * @param string $methodName Name of the method being processed.
+ * @param array $methodMetadata Metadata from the Proxy class which can be used to enhance the reflection data.
+ *
+ * @return array Collection of key details about the API endpoint.
+ * @throws \ReflectionException If the method cannot be found on the class.
+ */
+ public function buildJsonInfoForApiMethod(\ReflectionClass $reflectionClass, string $methodName, array $methodMetadata): array
+ {
+ $reflectionMethod = $reflectionClass->getMethod($methodName);
+ if (AnnotationGenerator::shouldApiMethodBeIgnored($reflectionMethod)) {
+ return [];
+ }
+
+ $doc = $reflectionMethod->getDocComment() ?: '';
+
+ $signatureString = 'public function ' . $methodName . '(';
+ $paramsString = '';
+ $signature = $this->buildMethodSignatureUsingReflection($reflectionMethod);
+ foreach ($signature['params'] as $param) {
+ $paramsString .= !empty($param['php_type']) ? $param['php_type'] . ' ' : '';
+ $paramsString .= $param['name'];
+ $paramsString .= $param['default'] !== null ? ' = ' . (!is_string($param['default']) ? json_encode($param['default']) : $param['default']) : '';
+ $paramsString .= ', ';
+ }
+ $paramsString = rtrim($paramsString, ', ');
+ $signatureString .= $paramsString . ')';
+ if (!empty($signature['return']) && !empty($signature['return']['php_type'])) {
+ $signatureString .= ': ' . ($signature['return']['nullable'] ? '?' : '') . $signature['return']['php_type'];
+ }
+
+ $fileName = $reflectionMethod->getFileName();
+ $fileName = str_replace(PIWIK_DOCUMENT_ROOT . '/', '', $fileName);
+
+ return [
+ 'fqcn' => $reflectionClass->getName(),
+ 'method' => $methodName,
+ 'source' => ['file' => $fileName, 'start' => $reflectionMethod->getStartLine(), 'end' => $reflectionMethod->getEndLine()],
+ 'header' => $signatureString,
+ 'signature' => $signature,
+ 'docblock_raw' => $doc,
+ ];
+ }
+
+ /**
+ * Use reflection to build up information about the method signature, such as parameter and return type details.
+ *
+ * @param \ReflectionMethod $reflectionMethod The reflection method which can provide all the necessary info.
+ *
+ * @return array[] Collection of parameter and return type details, such as php type and whether they're nullable.
+ */
+ public function buildMethodSignatureUsingReflection(\ReflectionMethod $reflectionMethod)
+ {
+ $params = [];
+ foreach ($reflectionMethod->getParameters() as $reflectionParameter) {
+ $defaultValue = null;
+ if ($reflectionParameter->isOptional() && $reflectionParameter->isDefaultValueAvailable()) {
+ $defaultValue = $reflectionParameter->getDefaultValue();
+ $defaultValue = $defaultValue !== null ? $defaultValue : json_encode($reflectionParameter->getDefaultValue());
+ }
+ $param = [
+ 'name' => '$' . $reflectionParameter->getName(),
+ 'php_type' => $reflectionParameter->hasType() ? strval($reflectionParameter->getType()) : null,
+ 'required' => !$reflectionParameter->isOptional(),
+ 'nullable' => $reflectionParameter->hasType() && $reflectionParameter->getType() !== null && $reflectionParameter->getType()->allowsNull(),
+ 'default' => $defaultValue,
+ 'byRef' => $reflectionParameter->isPassedByReference(),
+ 'variadic' => $reflectionParameter->isVariadic(),
+ ];
+
+ $params[] = $param;
+ }
+
+ $returnType = $reflectionMethod->getReturnType();
+ $returnTypeInfo = [
+ 'php_type' => $returnType ? strval($returnType) : null,
+ 'nullable' => $returnType !== null && $returnType->allowsNull(),
+ ];
+
+ return ['params' => $params, 'return' => $returnTypeInfo];
+ }
+}
diff --git a/Commands/ExtractReportingApiMethodInfo.php b/Commands/ExtractReportingApiMethodInfo.php
new file mode 100644
index 0000000..5ff52ba
--- /dev/null
+++ b/Commands/ExtractReportingApiMethodInfo.php
@@ -0,0 +1,98 @@
+setName('openapidocs:extract-api-method-info');
+ $this->setDescription('Extract the comment block and basic information about methods for the Matomo Reporting API.');
+ $this->addRequiredValueOption('plugin', 'p', 'Name of the plugin to inspect');
+ $this->addNoValueOption('not-dry-run', null, 'Flag to allow writing to file instead of outputting a dry run.');
+ }
+
+ /**
+ * Interact with the user.
+ *
+ * This method is executed before the InputDefinition is validated.
+ * This means that this is the only place where the command can
+ * interactively ask for values of missing required arguments.
+ */
+ protected function doInteract(): void
+ {
+ }
+
+ /**
+ * Initializes the command after the input has been bound and before the input
+ * is validated.
+ *
+ * This is mainly useful when a lot of commands extends one main command
+ * where some things need to be initialized based on the input arguments and options.
+ */
+ protected function doInitialize(): void
+ {
+ }
+
+ /**
+ * The actual task is defined in this method. Here you can access any option or argument that was defined on the
+ * command line via $this->getInput() and write anything to the console via $this->getOutput().
+ * In case anything went wrong during the execution you should throw an exception to make sure the user will get a
+ * useful error message and to make sure the command does not exit with the status code 0.
+ *
+ * Ideally, the actual command is quite short as it acts like a controller. It should only receive the input values,
+ * execute the task by calling a method of another class and output any useful information.
+ *
+ * Execute the command like: ./console openapidocs:extract-api-method-info --plugin=TagManager --not-dry-run
+ */
+ protected function doExecute(): int
+ {
+ $input = $this->getInput();
+ $output = $this->getOutput();
+
+ $plugin = $input->getOption('plugin');
+ if (empty($plugin)) {
+ throw new \RuntimeException('Please specify a plugin name.');
+ }
+ $notDryRun = $input->getOption('not-dry-run') ?: false;
+
+ $message = sprintf('Extracting API method info for: %s', $plugin);
+
+ $output->writeln($message);
+
+ $result = (new ApiMethodInfoExtractor())->extractMethodInfo($plugin, $notDryRun);
+
+ if ($notDryRun) {
+ $output->writeln('Results written to plugins/OpenApiDocs/tmp/annotations directory.');
+
+ return $result ? self::SUCCESS : self::FAILURE;
+ }
+
+ $output->writeln($result);
+
+ return $result ? self::SUCCESS : self::FAILURE;
+ }
+}