|
| 1 | +<?php |
| 2 | + |
| 3 | +/** |
| 4 | + * Matomo - free/libre analytics platform |
| 5 | + * |
| 6 | + * @link https://matomo.org |
| 7 | + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later |
| 8 | + */ |
| 9 | + |
| 10 | +declare(strict_types=1); |
| 11 | + |
| 12 | +namespace Piwik\Plugins\OpenApiDocs\Annotations; |
| 13 | + |
| 14 | +use Piwik\API\Proxy; |
| 15 | +use Piwik\API\Request; |
| 16 | +use Piwik\Plugin\Manager; |
| 17 | +use Piwik\Plugins\OpenApiDocs\OpenApiDocs; |
| 18 | +use Piwik\Validators\BaseValidator; |
| 19 | +use Piwik\Validators\NotEmpty; |
| 20 | + |
| 21 | +class ApiMethodInfoExtractor |
| 22 | +{ |
| 23 | + /** |
| 24 | + * Look up the Matomo Reporting API methods for the specified plugin(s) and output the basic information for each. |
| 25 | + * This includes the comment block, parameter information, and things like that. This can then be fed to a secure |
| 26 | + * AI model which can come up with suggestions on how to improve the descriptions, type hinting, etc. |
| 27 | + * |
| 28 | + * @param string $pluginName Name or comma-separated list of names of the plugins to extract API endpoint info for. |
| 29 | + * @param bool $writeToFile Whether to output the result to console or write it to tmp file. |
| 30 | + * |
| 31 | + * @return string String result of the extracted info. |
| 32 | + * @throws \Exception |
| 33 | + */ |
| 34 | + public function extractMethodInfo(string $pluginName, bool $writeToFile = false): string |
| 35 | + { |
| 36 | + BaseValidator::check('plugin', $pluginName, [new NotEmpty()]); |
| 37 | + $pluginNames = explode(',', $pluginName); |
| 38 | + |
| 39 | + BaseValidator::check('pluginNames', $pluginNames, [new NotEmpty()]); |
| 40 | + $currentPluginDir = Manager::getInstance()::getPluginDirectory('OpenApiDocs'); |
| 41 | + |
| 42 | + $methodInfoArray = []; |
| 43 | + foreach ($pluginNames as $plugin) { |
| 44 | + BaseValidator::check('pluginName', $plugin, [new NotEmpty()]); |
| 45 | + Manager::getInstance()->checkIsPluginActivated($plugin); |
| 46 | + |
| 47 | + $className = Request::getClassNameAPI($plugin); |
| 48 | + |
| 49 | + $result = $this->buildEndpointInfoArrayForApiClass($className); |
| 50 | + if (!empty($result)) { |
| 51 | + $methodInfoArray = array_merge($methodInfoArray, $result); |
| 52 | + } |
| 53 | + } |
| 54 | + $methodInfoString = json_encode($methodInfoArray); |
| 55 | + |
| 56 | + $fileBaseName = 'matomo'; |
| 57 | + // If there's only one plugin, name the spec after the plugin |
| 58 | + if (count($pluginNames) === 1) { |
| 59 | + $fileBaseName = $pluginNames[0]; |
| 60 | + } |
| 61 | + |
| 62 | + if ($writeToFile) { |
| 63 | + $pluginSpecPath = $currentPluginDir . OpenApiDocs::GENERATED_ANNOTATIONS_PATH . $fileBaseName . '_api_method_info.json'; |
| 64 | + file_put_contents($pluginSpecPath, $methodInfoString); |
| 65 | + } |
| 66 | + |
| 67 | + return $methodInfoString; |
| 68 | + } |
| 69 | + |
| 70 | + /** |
| 71 | + * Build the array of info about all the public API endpoints for a specific API class. It uses reflection and the |
| 72 | + * metadata from the Proxy class used to build documentation. |
| 73 | + * |
| 74 | + * @param string $apiClassName Full name of the class which can be used to instantiate a ReflectionClass. |
| 75 | + * |
| 76 | + * @return array[] Collection of key details about each public API endpoint in the specified class. |
| 77 | + */ |
| 78 | + public function buildEndpointInfoArrayForApiClass(string $apiClassName): array |
| 79 | + { |
| 80 | + try { |
| 81 | + $reflectionClass = new \ReflectionClass($apiClassName); |
| 82 | + } catch (\ReflectionException $e) { |
| 83 | + throw new \RuntimeException('Unable to get reflection class for API class: ' . $apiClassName, 0, $e); |
| 84 | + } |
| 85 | + |
| 86 | + Proxy::getInstance()->registerClass($apiClassName); |
| 87 | + $pluginMetadata = Proxy::getInstance()->getMetadata()[$apiClassName] ?? []; |
| 88 | + |
| 89 | + // The proxy has already determined which methods should be ignored, so we just use the list it came up with. |
| 90 | + $methodInfoArray = []; |
| 91 | + foreach ($pluginMetadata as $methodName => $metadataMethod) { |
| 92 | + if ($methodName === '__documentation') { |
| 93 | + continue; |
| 94 | + } |
| 95 | + $result = $this->buildJsonInfoForApiMethod($reflectionClass, $methodName, $metadataMethod); |
| 96 | + if (!empty($result)) { |
| 97 | + $methodInfoArray[] = $result; |
| 98 | + } |
| 99 | + } |
| 100 | + |
| 101 | + return $methodInfoArray; |
| 102 | + } |
| 103 | + |
| 104 | + /** |
| 105 | + * Build the array of info about a specific API endpoint. |
| 106 | + * |
| 107 | + * @param \ReflectionClass $reflectionClass Reflection class used to get the method data defined in the signature. |
| 108 | + * @param string $methodName Name of the method being processed. |
| 109 | + * @param array $methodMetadata Metadata from the Proxy class which can be used to enhance the reflection data. |
| 110 | + * |
| 111 | + * @return array Collection of key details about the API endpoint. |
| 112 | + * @throws \ReflectionException If the method cannot be found on the class. |
| 113 | + */ |
| 114 | + public function buildJsonInfoForApiMethod(\ReflectionClass $reflectionClass, string $methodName, array $methodMetadata): array |
| 115 | + { |
| 116 | + $reflectionMethod = $reflectionClass->getMethod($methodName); |
| 117 | + if (AnnotationGenerator::shouldApiMethodBeIgnored($reflectionMethod)) { |
| 118 | + return []; |
| 119 | + } |
| 120 | + |
| 121 | + $doc = $reflectionMethod->getDocComment() ?: ''; |
| 122 | + |
| 123 | + $signatureString = 'public function ' . $methodName . '('; |
| 124 | + $paramsString = ''; |
| 125 | + $signature = $this->buildMethodSignatureUsingReflection($reflectionMethod); |
| 126 | + foreach ($signature['params'] as $param) { |
| 127 | + $paramsString .= !empty($param['php_type']) ? $param['php_type'] . ' ' : ''; |
| 128 | + $paramsString .= $param['name']; |
| 129 | + $paramsString .= $param['default'] !== null ? ' = ' . (!is_string($param['default']) ? json_encode($param['default']) : $param['default']) : ''; |
| 130 | + $paramsString .= ', '; |
| 131 | + } |
| 132 | + $paramsString = rtrim($paramsString, ', '); |
| 133 | + $signatureString .= $paramsString . ')'; |
| 134 | + if (!empty($signature['return']) && !empty($signature['return']['php_type'])) { |
| 135 | + $signatureString .= ': ' . ($signature['return']['nullable'] ? '?' : '') . $signature['return']['php_type']; |
| 136 | + } |
| 137 | + |
| 138 | + $fileName = $reflectionMethod->getFileName(); |
| 139 | + $fileName = str_replace(PIWIK_DOCUMENT_ROOT . '/', '', $fileName); |
| 140 | + |
| 141 | + return [ |
| 142 | + 'fqcn' => $reflectionClass->getName(), |
| 143 | + 'method' => $methodName, |
| 144 | + 'source' => ['file' => $fileName, 'start' => $reflectionMethod->getStartLine(), 'end' => $reflectionMethod->getEndLine()], |
| 145 | + 'header' => $signatureString, |
| 146 | + 'signature' => $signature, |
| 147 | + 'docblock_raw' => $doc, |
| 148 | + ]; |
| 149 | + } |
| 150 | + |
| 151 | + /** |
| 152 | + * Use reflection to build up information about the method signature, such as parameter and return type details. |
| 153 | + * |
| 154 | + * @param \ReflectionMethod $reflectionMethod The reflection method which can provide all the necessary info. |
| 155 | + * |
| 156 | + * @return array[] Collection of parameter and return type details, such as php type and whether they're nullable. |
| 157 | + */ |
| 158 | + public function buildMethodSignatureUsingReflection(\ReflectionMethod $reflectionMethod) |
| 159 | + { |
| 160 | + $params = []; |
| 161 | + foreach ($reflectionMethod->getParameters() as $reflectionParameter) { |
| 162 | + $defaultValue = null; |
| 163 | + if ($reflectionParameter->isOptional() && $reflectionParameter->isDefaultValueAvailable()) { |
| 164 | + $defaultValue = $reflectionParameter->getDefaultValue(); |
| 165 | + $defaultValue = $defaultValue !== null ? $defaultValue : json_encode($reflectionParameter->getDefaultValue()); |
| 166 | + } |
| 167 | + $param = [ |
| 168 | + 'name' => '$' . $reflectionParameter->getName(), |
| 169 | + 'php_type' => $reflectionParameter->hasType() ? strval($reflectionParameter->getType()) : null, |
| 170 | + 'required' => !$reflectionParameter->isOptional(), |
| 171 | + 'nullable' => $reflectionParameter->hasType() && $reflectionParameter->getType() !== null && $reflectionParameter->getType()->allowsNull(), |
| 172 | + 'default' => $defaultValue, |
| 173 | + 'byRef' => $reflectionParameter->isPassedByReference(), |
| 174 | + 'variadic' => $reflectionParameter->isVariadic(), |
| 175 | + ]; |
| 176 | + |
| 177 | + $params[] = $param; |
| 178 | + } |
| 179 | + |
| 180 | + $returnType = $reflectionMethod->getReturnType(); |
| 181 | + $returnTypeInfo = [ |
| 182 | + 'php_type' => $returnType ? strval($returnType) : null, |
| 183 | + 'nullable' => $returnType !== null && $returnType->allowsNull(), |
| 184 | + ]; |
| 185 | + |
| 186 | + return ['params' => $params, 'return' => $returnTypeInfo]; |
| 187 | + } |
| 188 | +} |
0 commit comments