Lines
84.56%
126 / 149
Methods
33.33%
3 / 9
Classes
0.00%
0 / 1
| Name | Lines | Methods | CRAP | ||||
|---|---|---|---|---|---|---|---|
| generateApi | 95.00% | 19 / 20 | 0.00% | 0 / 1 | 2 | ||
| scanControllers | 80.00% | 8 / 10 | 0.00% | 0 / 1 | 4.13 | ||
| scanPhpFile | 68.42% | 13 / 19 | 0.00% | 0 / 1 | 7.13 | ||
| toNamespace | 100.00% | 9 / 9 | 100.00% | 1 / 1 | 2 | ||
| processEndpoint | 93.33% | 28 / 30 | 0.00% | 0 / 1 | 8.02 | ||
| generateEndpointsHtml | 100.00% | 4 / 4 | 100.00% | 1 / 1 | 2 | ||
| generateEndpointHtml | 83.33% | 40 / 48 | 0.00% | 0 / 1 | 7.23 | ||
| formatResponseData | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| getResponseType | 50.00% | 4 / 8 | 0.00% | 0 / 1 | 26.12 | ||
| 17 | class GenerateDocumentationCommand | |
| 18 | { | |
| 19 | ||
| 20 | public static string $command = "generate api-docs"; | |
| 21 | ||
| 22 | public function generateApi(): string | |
| 23 | { | |
| 24 | $documentation = $this->scanControllers(); | |
| 25 | ||
| 26 | // Load our template | |
| 27 | $template = file_get_contents(LUCENT . 'Templates' . DIRECTORY_SEPARATOR . 'api-docs.php'); | |
| 28 | ||
| 29 | // Replace our template variables | |
| 30 | $template = str_replace( | |
| 31 | [ | |
| 32 | '{{endpoints}}', | |
| 33 | '{{date}}', | |
| 34 | '{{version}}' | |
| 35 | ], | |
| 36 | [ | |
| 37 | $this->generateEndpointsHtml($documentation), | |
| 38 | date('F j, Y'), | |
| 39 | App::getLucentVersion() | |
| 40 | ], | |
| 41 | $template | |
| 42 | ); | |
| 43 | ||
| 44 | // Save to file | |
| 45 | $outputPath = FileSystem::rootPath() . DIRECTORY_SEPARATOR . "storage" . DIRECTORY_SEPARATOR . 'documentation' . DIRECTORY_SEPARATOR; | |
| 46 | if (!file_exists($outputPath)) { | |
| 47 | mkdir($outputPath, 0755, true); | |
| 48 | } | |
| 49 | ||
| 50 | file_put_contents($outputPath . 'api.html', $template); | |
| 51 | ||
| 52 | return "API documentation generated successfully at " . $outputPath . "api.html"; | |
| 53 | } | |
| 54 | public function scanControllers(): array | |
| 55 | { | |
| 56 | $documentation = []; | |
| 57 | ||
| 58 | $app = new Folder("/App"); | |
| 59 | ||
| 60 | if (!$app->exists()) { | |
| 61 | Log::channel("lucent.commandline")->error("Fatal error app folder doesnt exist..."); | |
| 62 | return []; | |
| 63 | } | |
| 64 | ||
| 65 | foreach ($app->getFiles(true) as $file) { | |
| 66 | ||
| 67 | if ($file->getExtension() == ".php") { | |
| 68 | $this->scanPhpFile($file, $documentation); | |
| 69 | } | |
| 70 | } | |
| 71 | ||
| 72 | Log::channel("lucent.commandline")->info("Scan complete. Found " . count($documentation) . " endpoints"); | |
| 73 | return $documentation; | |
| 74 | } | |
| 75 | ||
| 76 | private function scanPhpFile(File $file, &$documentation): void | |
| 77 | { | |
| 78 | try { | |
| 79 | $className = $this->toNamespace($file->path); | |
| 80 | ||
| 81 | if (!class_exists($className)) { | |
| 82 | Log::channel("lucent.commandline")->debug("Class not found, requiring file: " . $file->path); | |
| 83 | require_once $file->path; | |
| 84 | } | |
| 85 | ||
| 86 | $reflection = new ReflectionClass($className); | |
| 87 | ||
| 88 | foreach ($reflection->getMethods() as $method) { | |
| 89 | ||
| 90 | $endpointAttributes = $method->getAttributes(ApiEndpoint::class); | |
| 91 | ||
| 92 | //skip the endpoint if it has no data | |
| 93 | if (empty($endpointAttributes)) { | |
| 94 | continue; | |
| 95 | } | |
| 96 | ||
| 97 | //get the endpoint and responses | |
| 98 | $endpoint = $endpointAttributes[0]->newInstance(); | |
| 99 | $responses = []; | |
| 100 | ||
| 101 | $responseAttributes = $method->getAttributes(ApiResponse::class); | |
| 102 | ||
| 103 | foreach ($responseAttributes as $attribute) { | |
| 104 | $responses[] = $attribute->newInstance(); | |
| 105 | } | |
| 106 | ||
| 107 | $documentation[] = $this->processEndpoint($endpoint, $responses); | |
| 108 | } | |
| 109 | } catch (\ReflectionException $e) { | |
| 110 | Log::channel("lucent.commandline")->critical( | |
| 111 | implode(" | ", ExceptionChain::messages($e)) | |
| 112 | ); | |
| 113 | } | |
| 114 | } | |
| 115 | ||
| 116 | private function toNamespace(string $filePath): string | |
| 117 | { | |
| 118 | $rootPath = FileSystem::rootPath(); | |
| 119 | ||
| 120 | // remove FileSystem::rootPath() if present | |
| 121 | if (str_starts_with($filePath, $rootPath)) { | |
| 122 | $filePath = substr($filePath, strlen($rootPath)); | |
| 123 | } | |
| 124 | ||
| 125 | // remove leading directory separator | |
| 126 | $filePath = ltrim($filePath, DIRECTORY_SEPARATOR); | |
| 127 | ||
| 128 | // remove .php extension and convert directory separators to namespace separators | |
| 129 | return str_replace( | |
| 130 | [DIRECTORY_SEPARATOR, '.php'], | |
| 131 | ['\\', ''], | |
| 132 | $filePath | |
| 133 | ); | |
| 134 | } | |
| 135 | ||
| 136 | private function processEndpoint(ApiEndpoint $endpoint, array $responses): array | |
| 137 | { | |
| 138 | $examples = []; | |
| 139 | $validationRules = null; | |
| 140 | ||
| 141 | // Process API responses | |
| 142 | foreach ($responses as $response) { | |
| 143 | $body = [ | |
| 144 | 'message' => $response->message, | |
| 145 | 'outcome' => $response->outcome, | |
| 146 | 'status' => $response->status, | |
| 147 | 'content' => $response->content ?? [], | |
| 148 | 'errors' => [], | |
| 149 | ]; | |
| 150 | ||
| 151 | // Convert sequential arrays to associative if they appear to be key-value pairs | |
| 152 | if (!empty($response->content) && is_array($response->content) && count($response->content) % 2 === 0) { | |
| 153 | $pairs = array_chunk($response->content, 2); | |
| 154 | $content = []; | |
| 155 | foreach ($pairs as $pair) { | |
| 156 | if (is_string($pair[0])) { | |
| 157 | $content[$pair[0]] = $pair[1]; | |
| 158 | continue; | |
| 159 | } | |
| 160 | $content[] = $pair; | |
| 161 | } | |
| 162 | $body['content'] = $content; | |
| 163 | } | |
| 164 | ||
| 165 | if (!empty($response->errors)) { | |
| 166 | $body['errors'] = $response->errors; | |
| 167 | } | |
| 168 | ||
| 169 | $examples[$response->status] = $body; | |
| 170 | } | |
| 171 | ||
| 172 | return [ | |
| 173 | 'path' => $endpoint->path, | |
| 174 | 'method' => $endpoint->method, | |
| 175 | 'description' => $endpoint->description, | |
| 176 | 'parameters' => $endpoint->pathParams, | |
| 177 | 'validationRules' => $validationRules, | |
| 178 | 'examples' => $examples | |
| 179 | ]; | |
| 180 | } | |
| 181 | private function generateEndpointsHtml(array $documentation): string | |
| 182 | { | |
| 183 | $html = ''; | |
| 184 | foreach ($documentation as $endpoint) { | |
| 185 | $html .= $this->generateEndpointHtml($endpoint); | |
| 186 | } | |
| 187 | return $html; | |
| 188 | } | |
| 189 | ||
| 190 | private function generateEndpointHtml(array $endpoint): string | |
| 191 | { | |
| 192 | $urlParams = ''; | |
| 193 | if (!empty($endpoint['parameters'])) { | |
| 194 | $urlParams = '<div class="parameters"> | |
| 195 | <h3>URL Parameters</h3>'; | |
| 196 | ||
| 197 | foreach ($endpoint['parameters'] as $name => $description) { | |
| 198 | $urlParams .= '<div class="parameter"> | |
| 199 | <span class="parameter-name">' . htmlspecialchars($name) . '</span> | |
| 200 | <span class="parameter-description">' . htmlspecialchars($description) . '</span> | |
| 201 | </div>'; | |
| 202 | } | |
| 203 | ||
| 204 | $urlParams .= '</div>'; | |
| 205 | } | |
| 206 | ||
| 207 | $validationRules = ''; | |
| 208 | if (!empty($endpoint['validationRules'])) { | |
| 209 | $validationRules = '<div class="validation-rules"> | |
| 210 | <h3>Validation Rules</h3> | |
| 211 | <ul class="rules-list">'; | |
| 212 | ||
| 213 | foreach ($endpoint['validationRules'] as $field => $rules) { | |
| 214 | $validationRules .= '<li> | |
| 215 | <span class="rule-name">' . htmlspecialchars($field) . '</span> | |
| 216 | <span>' . htmlspecialchars(implode(', ', (array)$rules)) . '</span> | |
| 217 | </li>'; | |
| 218 | } | |
| 219 | ||
| 220 | $validationRules .= '</ul></div>'; | |
| 221 | } | |
| 222 | ||
| 223 | $examples = ''; | |
| 224 | if (!empty($endpoint['examples'])) { | |
| 225 | $examples = '<div class="response-section"> | |
| 226 | <h3>Response Examples</h3>'; | |
| 227 | ||
| 228 | // Sort examples by status code | |
| 229 | ksort($endpoint['examples']); | |
| 230 | ||
| 231 | foreach ($endpoint['examples'] as $status => $response) { | |
| 232 | $responseType = $this->getResponseType($status); | |
| 233 | $responseData = $response; | |
| 234 | ||
| 235 | // Format the response data | |
| 236 | $formattedResponse = $this->formatResponseData($responseData); | |
| 237 | ||
| 238 | $examples .= '<div class="response"> | |
| 239 | <div class="response-header">' . $responseType . ' (' . $status . ')</div> | |
| 240 | <div class="response-body"> | |
| 241 | <pre>' . $formattedResponse . '</pre> | |
| 242 | </div> | |
| 243 | </div>'; | |
| 244 | } | |
| 245 | ||
| 246 | $examples .= '</div>'; | |
| 247 | } | |
| 248 | ||
| 249 | // Format the path to highlight parameters | |
| 250 | $path = preg_replace( | |
| 251 | '/\{([^}]+)\}/', | |
| 252 | '<span class="parameter">{$1}</span>', | |
| 253 | htmlspecialchars($endpoint['path']) | |
| 254 | ); | |
| 255 | ||
| 256 | return <<<HTML | |
| 257 | <div class="endpoint"> | |
| 258 | <div class="endpoint-header"> | |
| 259 | <span class="method {$endpoint['method']}">{$endpoint['method']}</span> | |
| 260 | <span class="endpoint-path">{$path}</span> | |
| 261 | </div> | |
| 262 | <div class="endpoint-content"> | |
| 263 | <p>{$endpoint['description']}</p> | |
| 264 | {$urlParams} | |
| 265 | {$validationRules} | |
| 266 | {$examples} | |
| 267 | </div> | |
| 268 | </div> | |
| 269 | HTML; | |
| 270 | } | |
| 271 | ||
| 272 | private function formatResponseData(array $data): string | |
| 273 | { | |
| 274 | return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); | |
| 275 | } | |
| 276 | ||
| 277 | private function getResponseType(int $status): string | |
| 278 | { | |
| 279 | return match (true) { | |
| 280 | $status >= 200 && $status < 300 => 'Success', | |
| 281 | $status === 400 => 'Validation Error', | |
| 282 | $status === 401 => 'Unauthorized', | |
| 283 | $status === 403 => 'Forbidden', | |
| 284 | $status === 404 => 'Not Found', | |
| 285 | $status >= 400 && $status < 500 => 'Client Error', | |
| 286 | $status >= 500 => 'Server Error', | |
| 287 | default => 'Unknown' | |
| 288 | }; | |
| 289 | } | |
| 290 | } |