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
17class 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}