451 lines
15 KiB
PHP
451 lines
15 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\File;
|
|
use Illuminate\Support\Str;
|
|
|
|
class GenerateEndpointsDocs extends Command
|
|
{
|
|
/**
|
|
* The name and signature of the console command.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $signature = 'docs:endpoints
|
|
{--append : Append to existing OpenAPI spec}
|
|
{--output= : Output file path}';
|
|
|
|
/**
|
|
* The console command description.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $description = 'Generate API documentation for admin-ajax endpoints and append to Scribe OpenAPI spec';
|
|
|
|
/**
|
|
* Execute the console command.
|
|
*/
|
|
public function handle()
|
|
{
|
|
$this->info('Scanning admin-ajax endpoints...');
|
|
|
|
$endpoints = $this->scanEndpoints();
|
|
$this->info("Found " . count($endpoints) . " endpoints");
|
|
|
|
if ($this->option('append')) {
|
|
$this->appendToOpenApiSpec($endpoints);
|
|
} else {
|
|
$this->generateStandaloneSpec($endpoints);
|
|
}
|
|
|
|
$this->info('✅ Endpoints documentation generated successfully!');
|
|
|
|
return Command::SUCCESS;
|
|
}
|
|
|
|
/**
|
|
* Scan admin-ajax directory for blade files
|
|
*/
|
|
protected function scanEndpoints(): array
|
|
{
|
|
$endpoints = [];
|
|
$path = resource_path('views/admin-ajax');
|
|
|
|
if (!File::isDirectory($path)) {
|
|
$this->error('admin-ajax directory not found');
|
|
return $endpoints;
|
|
}
|
|
|
|
// Scan files recursively
|
|
$this->scanDirectory($path, '', $endpoints);
|
|
|
|
// Sort alphabetically
|
|
usort($endpoints, fn($a, $b) => strcmp($a['name'], $b['name']));
|
|
|
|
return $endpoints;
|
|
}
|
|
|
|
/**
|
|
* Recursively scan directory for blade files
|
|
* SECURITY: Only includes endpoints marked with @api-readonly annotation
|
|
*/
|
|
protected function scanDirectory(string $basePath, string $prefix, array &$endpoints): void
|
|
{
|
|
$items = File::files($basePath);
|
|
|
|
foreach ($items as $file) {
|
|
$filename = $file->getFilename();
|
|
|
|
// Only process .blade.php files
|
|
if (!Str::endsWith($filename, '.blade.php')) {
|
|
continue;
|
|
}
|
|
|
|
// Extract endpoint name
|
|
$name = str_replace('.blade.php', '', $filename);
|
|
$fullName = $prefix ? "{$prefix}/{$name}" : $name;
|
|
|
|
// Analyze file for response type hints
|
|
$content = File::get($file->getPathname());
|
|
|
|
// SECURITY: Only include endpoints marked as @api-readonly
|
|
if (!Str::contains($content, '@api-readonly')) {
|
|
continue;
|
|
}
|
|
|
|
$responseType = $this->detectResponseType($content);
|
|
$description = $this->extractDescription($content, $name);
|
|
$category = $this->categorizeEndpoint($fullName);
|
|
|
|
$endpoints[] = [
|
|
'name' => $fullName,
|
|
'path' => str_replace(base_path(), '', $file->getPathname()),
|
|
'response_type' => $responseType,
|
|
'description' => $description,
|
|
'category' => $category,
|
|
];
|
|
}
|
|
|
|
// Scan subdirectories
|
|
$directories = File::directories($basePath);
|
|
foreach ($directories as $dir) {
|
|
$dirName = basename($dir);
|
|
$newPrefix = $prefix ? "{$prefix}/{$dirName}" : $dirName;
|
|
$this->scanDirectory($dir, $newPrefix, $endpoints);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Detect response type from blade content
|
|
*/
|
|
protected function detectResponseType(string $content): string
|
|
{
|
|
// Check for JSON indicators
|
|
if (
|
|
Str::contains($content, 'json_encode') ||
|
|
Str::contains($content, 'json_encode_tr') ||
|
|
Str::contains($content, 'response()->json') ||
|
|
Str::contains($content, "'status' =>")
|
|
) {
|
|
return 'json';
|
|
}
|
|
|
|
// Check for HTML indicators
|
|
if (
|
|
Str::contains($content, '<html') ||
|
|
Str::contains($content, '<div') ||
|
|
Str::contains($content, '@extends') ||
|
|
Str::contains($content, '@include')
|
|
) {
|
|
return 'html';
|
|
}
|
|
|
|
return 'auto';
|
|
}
|
|
|
|
/**
|
|
* Extract description from blade file comments
|
|
*/
|
|
protected function extractDescription(string $content, string $name): string
|
|
{
|
|
// Try to find PHPDoc style comment at the start
|
|
if (preg_match('/^<\?php\s*\/\*\*?\s*\n?\s*\*?\s*(.+?)(?:\n|\*\/)/s', $content, $matches)) {
|
|
return trim($matches[1]);
|
|
}
|
|
|
|
// Try to find HTML comment
|
|
if (preg_match('/<!--\s*(.+?)\s*-->/s', $content, $matches)) {
|
|
return trim($matches[1]);
|
|
}
|
|
|
|
// Generate description from name
|
|
return 'Execute ' . Str::title(str_replace(['-', '_'], ' ', $name)) . ' endpoint';
|
|
}
|
|
|
|
/**
|
|
* Categorize endpoint based on name
|
|
*/
|
|
protected function categorizeEndpoint(string $name): string
|
|
{
|
|
$prefixes = [
|
|
'ndt-' => 'NDT Operations',
|
|
'welder-' => 'Welder Management',
|
|
'weld' => 'Welding',
|
|
'register-' => 'Register Creator',
|
|
'test-package' => 'Test Packages',
|
|
'tp-' => 'Test Packages',
|
|
'document-' => 'Document Management',
|
|
'report-' => 'Reports',
|
|
'summary' => 'Summaries',
|
|
'excel-' => 'Excel Operations',
|
|
'pdf' => 'PDF Operations',
|
|
'user' => 'User Management',
|
|
'permission' => 'Permissions',
|
|
'content-' => 'Content Management',
|
|
'spool-' => 'Spool Operations',
|
|
'material-' => 'Materials',
|
|
'paint-' => 'Paint Operations',
|
|
'repair-' => 'Repair Operations',
|
|
'rfi-' => 'RFI Operations',
|
|
'punch-' => 'Punch Lists',
|
|
'mto' => 'MTO Operations',
|
|
'ai-' => 'AI Features',
|
|
'cron-' => 'Cron Jobs',
|
|
'si-' => 'System Info',
|
|
];
|
|
|
|
$baseName = Str::contains($name, '/') ? Str::afterLast($name, '/') : $name;
|
|
|
|
foreach ($prefixes as $prefix => $category) {
|
|
if (Str::startsWith($baseName, $prefix)) {
|
|
return $category;
|
|
}
|
|
}
|
|
|
|
return 'General';
|
|
}
|
|
|
|
/**
|
|
* Append endpoints to existing OpenAPI spec
|
|
*/
|
|
protected function appendToOpenApiSpec(array $endpoints): void
|
|
{
|
|
$specPath = storage_path('app/scribe/openapi.yaml');
|
|
|
|
if (!File::exists($specPath)) {
|
|
$this->warn('OpenAPI spec not found at ' . $specPath);
|
|
$this->info('Run "php artisan scribe:generate" first, then run this command with --append');
|
|
return;
|
|
}
|
|
|
|
// Load existing spec
|
|
$spec = yaml_parse_file($specPath);
|
|
|
|
if (!$spec) {
|
|
$this->error('Failed to parse OpenAPI spec');
|
|
return;
|
|
}
|
|
|
|
// Add endpoint paths
|
|
foreach ($endpoints as $endpoint) {
|
|
$pathKey = '/api/endpoints/' . $endpoint['name'];
|
|
|
|
$spec['paths'][$pathKey] = $this->generatePathSpec($endpoint);
|
|
}
|
|
|
|
// Add Endpoints tag if not exists
|
|
$hasEndpointsTag = false;
|
|
foreach ($spec['tags'] ?? [] as $tag) {
|
|
if ($tag['name'] === 'Endpoints') {
|
|
$hasEndpointsTag = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$hasEndpointsTag) {
|
|
$spec['tags'][] = [
|
|
'name' => 'Endpoints',
|
|
'description' => 'Admin-ajax endpoints served via API'
|
|
];
|
|
}
|
|
|
|
// Save updated spec
|
|
$yaml = yaml_emit($spec, YAML_UTF8_ENCODING);
|
|
File::put($specPath, $yaml);
|
|
|
|
$this->info('Updated OpenAPI spec at ' . $specPath);
|
|
}
|
|
|
|
/**
|
|
* Generate standalone endpoints spec file
|
|
*/
|
|
protected function generateStandaloneSpec(array $endpoints): void
|
|
{
|
|
$outputPath = $this->option('output') ?: storage_path('app/scribe/endpoints.json');
|
|
|
|
// Group endpoints by category
|
|
$grouped = [];
|
|
foreach ($endpoints as $endpoint) {
|
|
$category = $endpoint['category'];
|
|
if (!isset($grouped[$category])) {
|
|
$grouped[$category] = [];
|
|
}
|
|
$grouped[$category][] = $endpoint;
|
|
}
|
|
|
|
$output = [
|
|
'generated_at' => now()->toIso8601String(),
|
|
'total_endpoints' => count($endpoints),
|
|
'categories' => array_keys($grouped),
|
|
'endpoints' => $endpoints,
|
|
'grouped' => $grouped,
|
|
];
|
|
|
|
File::ensureDirectoryExists(dirname($outputPath));
|
|
File::put($outputPath, json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
|
|
|
$this->info('Generated endpoints spec at ' . $outputPath);
|
|
|
|
// Generate Scribe response file for dynamic documentation
|
|
$this->generateScribeResponseFile($endpoints);
|
|
|
|
// Also generate markdown documentation
|
|
$this->generateMarkdownDocs($grouped);
|
|
}
|
|
|
|
/**
|
|
* Generate response file for Scribe documentation
|
|
*/
|
|
protected function generateScribeResponseFile(array $endpoints): void
|
|
{
|
|
$responsePath = storage_path('app/scribe/endpoints-response.json');
|
|
|
|
// Format endpoints with url and methods for API response
|
|
$formattedEndpoints = array_map(function ($endpoint) {
|
|
return [
|
|
'name' => $endpoint['name'],
|
|
'url' => '/api/endpoints/' . $endpoint['name'],
|
|
'methods' => ['GET', 'POST'],
|
|
'response_type' => $endpoint['response_type'],
|
|
'description' => $endpoint['description'],
|
|
'category' => $endpoint['category'],
|
|
];
|
|
}, $endpoints);
|
|
|
|
$response = [
|
|
'status' => 'success',
|
|
'data' => [
|
|
'endpoints' => $formattedEndpoints,
|
|
'total' => count($formattedEndpoints)
|
|
]
|
|
];
|
|
|
|
File::ensureDirectoryExists(dirname($responsePath));
|
|
File::put($responsePath, json_encode($response, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
|
|
|
$this->info('Generated Scribe response file at ' . $responsePath);
|
|
}
|
|
|
|
/**
|
|
* Generate OpenAPI path specification for an endpoint
|
|
*/
|
|
protected function generatePathSpec(array $endpoint): array
|
|
{
|
|
$responseSchema = $endpoint['response_type'] === 'json'
|
|
? ['type' => 'object', 'properties' => ['status' => ['type' => 'string'], 'data' => ['type' => 'object']]]
|
|
: ['type' => 'object', 'properties' => ['status' => ['type' => 'string'], 'html' => ['type' => 'string']]];
|
|
|
|
return [
|
|
'get' => [
|
|
'tags' => ['Endpoints'],
|
|
'summary' => $endpoint['description'],
|
|
'description' => "Endpoint: {$endpoint['name']}\nResponse Type: {$endpoint['response_type']}\nCategory: {$endpoint['category']}",
|
|
'operationId' => 'endpoint_' . Str::slug($endpoint['name'], '_'),
|
|
'security' => [['bearerAuth' => []]],
|
|
'responses' => [
|
|
'200' => [
|
|
'description' => 'Successful response',
|
|
'content' => [
|
|
'application/json' => [
|
|
'schema' => $responseSchema
|
|
]
|
|
]
|
|
],
|
|
'401' => [
|
|
'description' => 'Unauthorized'
|
|
],
|
|
'404' => [
|
|
'description' => 'Endpoint not found'
|
|
]
|
|
]
|
|
],
|
|
'post' => [
|
|
'tags' => ['Endpoints'],
|
|
'summary' => $endpoint['description'],
|
|
'description' => "Endpoint: {$endpoint['name']}\nResponse Type: {$endpoint['response_type']}\nCategory: {$endpoint['category']}",
|
|
'operationId' => 'endpoint_' . Str::slug($endpoint['name'], '_') . '_post',
|
|
'security' => [['bearerAuth' => []]],
|
|
'requestBody' => [
|
|
'content' => [
|
|
'application/json' => [
|
|
'schema' => [
|
|
'type' => 'object',
|
|
'additionalProperties' => true
|
|
]
|
|
]
|
|
]
|
|
],
|
|
'responses' => [
|
|
'200' => [
|
|
'description' => 'Successful response',
|
|
'content' => [
|
|
'application/json' => [
|
|
'schema' => $responseSchema
|
|
]
|
|
]
|
|
],
|
|
'401' => [
|
|
'description' => 'Unauthorized'
|
|
],
|
|
'404' => [
|
|
'description' => 'Endpoint not found'
|
|
]
|
|
]
|
|
]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Generate markdown documentation
|
|
*/
|
|
protected function generateMarkdownDocs(array $grouped): void
|
|
{
|
|
$markdown = "# Available Endpoints\n\n";
|
|
$markdown .= "This document lists all available admin-ajax endpoints accessible via the API.\n\n";
|
|
$markdown .= "**Base URL:** `/api/endpoints/{endpoint}`\n\n";
|
|
$markdown .= "**Authentication:** Bearer Token required\n\n";
|
|
$markdown .= "---\n\n";
|
|
|
|
foreach ($grouped as $category => $endpoints) {
|
|
$markdown .= "## {$category}\n\n";
|
|
$markdown .= "| Endpoint | Description | Response Type |\n";
|
|
$markdown .= "|----------|-------------|---------------|\n";
|
|
|
|
foreach ($endpoints as $endpoint) {
|
|
$markdown .= "| `{$endpoint['name']}` | {$endpoint['description']} | {$endpoint['response_type']} |\n";
|
|
}
|
|
|
|
$markdown .= "\n";
|
|
}
|
|
|
|
$markdown .= "---\n\n";
|
|
$markdown .= "## Usage Examples\n\n";
|
|
$markdown .= "### cURL\n\n";
|
|
$markdown .= "```bash\n";
|
|
$markdown .= "curl -X GET \\\n";
|
|
$markdown .= " '{base_url}/api/endpoints/users' \\\n";
|
|
$markdown .= " -H 'Authorization: Bearer {your_token}' \\\n";
|
|
$markdown .= " -H 'Accept: application/json'\n";
|
|
$markdown .= "```\n\n";
|
|
$markdown .= "### JavaScript\n\n";
|
|
$markdown .= "```javascript\n";
|
|
$markdown .= "const response = await fetch('/api/endpoints/users', {\n";
|
|
$markdown .= " headers: {\n";
|
|
$markdown .= " 'Authorization': 'Bearer ' + token,\n";
|
|
$markdown .= " 'Accept': 'application/json'\n";
|
|
$markdown .= " }\n";
|
|
$markdown .= "});\n";
|
|
$markdown .= "const data = await response.json();\n";
|
|
$markdown .= "```\n";
|
|
|
|
$docsPath = resource_path('views/guide/api-endpoints.md');
|
|
File::ensureDirectoryExists(dirname($docsPath));
|
|
File::put($docsPath, $markdown);
|
|
|
|
$this->info('Generated markdown docs at ' . $docsPath);
|
|
}
|
|
}
|
|
|