feat: add OpenAPI auto-generate dari routes - Tambah OpenAPIGenerator class untuk scan routes dan generate spec - Tambah CLI command bin/generate-openapi.php - Support auto-generate on request via OPENAPI_AUTO_GENERATE env - Update public/index.php untuk auto-generate saat request /docs/openapi.json - Tambah dokumentasi OPENAPI_AUTO_GENERATE.md
This commit is contained in:
65
bin/generate-openapi.php
Normal file
65
bin/generate-openapi.php
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CLI script to generate OpenAPI spec from routes
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* php bin/generate-openapi.php
|
||||||
|
* php bin/generate-openapi.php --output public/docs/openapi.json
|
||||||
|
*
|
||||||
|
* Note: This script requires database connection to register routes.
|
||||||
|
* For auto-generation without DB, use OPENAPI_AUTO_GENERATE=true in .env
|
||||||
|
*/
|
||||||
|
|
||||||
|
require __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
use App\Bootstrap\AppBootstrap;
|
||||||
|
use App\Config\AppConfig;
|
||||||
|
use App\Modules\Auth\AuthRoutes;
|
||||||
|
use App\Modules\Health\HealthRoutes;
|
||||||
|
use App\Modules\Retribusi\Dashboard\DashboardRoutes;
|
||||||
|
use App\Modules\Retribusi\Realtime\RealtimeRoutes;
|
||||||
|
use App\Modules\Retribusi\RetribusiRoutes;
|
||||||
|
use App\Modules\Retribusi\Summary\SummaryRoutes;
|
||||||
|
use App\Support\OpenAPIGenerator;
|
||||||
|
|
||||||
|
// Load environment variables
|
||||||
|
AppConfig::loadEnv(__DIR__ . '/..');
|
||||||
|
|
||||||
|
// Parse command line arguments
|
||||||
|
$outputFile = $argv[1] ?? __DIR__ . '/../public/docs/openapi.json';
|
||||||
|
if (isset($argv[1]) && $argv[1] === '--output' && isset($argv[2])) {
|
||||||
|
$outputFile = $argv[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Bootstrap application
|
||||||
|
$app = AppBootstrap::create();
|
||||||
|
|
||||||
|
// Register all routes (same as public/index.php)
|
||||||
|
// Note: This requires database connection
|
||||||
|
HealthRoutes::register($app);
|
||||||
|
AuthRoutes::register($app);
|
||||||
|
RetribusiRoutes::register($app);
|
||||||
|
SummaryRoutes::register($app);
|
||||||
|
DashboardRoutes::register($app);
|
||||||
|
RealtimeRoutes::register($app);
|
||||||
|
|
||||||
|
// Generate OpenAPI spec
|
||||||
|
$generator = new OpenAPIGenerator($app);
|
||||||
|
$spec = $generator->generate();
|
||||||
|
|
||||||
|
// Write to file
|
||||||
|
$json = json_encode($spec, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||||
|
file_put_contents($outputFile, $json);
|
||||||
|
|
||||||
|
echo "✅ OpenAPI spec generated successfully!\n";
|
||||||
|
echo "📄 Output: {$outputFile}\n";
|
||||||
|
echo "📊 Total paths: " . count($spec['paths']) . "\n";
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
echo "❌ Error generating OpenAPI spec: " . $e->getMessage() . "\n";
|
||||||
|
echo "💡 Tip: Make sure database is configured in .env file\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
197
docs/OPENAPI_AUTO_GENERATE.md
Normal file
197
docs/OPENAPI_AUTO_GENERATE.md
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
# OpenAPI Auto-Generate
|
||||||
|
|
||||||
|
Sistem auto-generate OpenAPI spec dari routes yang terdaftar di Slim Framework.
|
||||||
|
|
||||||
|
## 🎯 Fitur
|
||||||
|
|
||||||
|
- **Auto-generate dari routes** - Scan semua routes yang terdaftar dan generate OpenAPI spec
|
||||||
|
- **CLI command** - Generate via command line
|
||||||
|
- **On-demand generation** - Auto-generate saat request `/docs/openapi.json` (optional)
|
||||||
|
- **No manual update** - Tidak perlu update manual `openapi.json` setiap kali tambah endpoint
|
||||||
|
|
||||||
|
## 📋 Cara Menggunakan
|
||||||
|
|
||||||
|
### Opsi 1: CLI Command (Recommended)
|
||||||
|
|
||||||
|
Generate OpenAPI spec via command line:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php bin/generate-openapi.php
|
||||||
|
```
|
||||||
|
|
||||||
|
Output default: `public/docs/openapi.json`
|
||||||
|
|
||||||
|
Custom output file:
|
||||||
|
```bash
|
||||||
|
php bin/generate-openapi.php --output custom/path/openapi.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** CLI command memerlukan database connection karena routes perlu di-register.
|
||||||
|
|
||||||
|
### Opsi 2: Auto-Generate on Request
|
||||||
|
|
||||||
|
Enable auto-generate saat request `/docs/openapi.json`:
|
||||||
|
|
||||||
|
1. Tambahkan di `.env`:
|
||||||
|
```env
|
||||||
|
OPENAPI_AUTO_GENERATE=true
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Setiap request ke `/docs/openapi.json` akan:
|
||||||
|
- Generate OpenAPI spec dari routes yang terdaftar
|
||||||
|
- Save ke file `public/docs/openapi.json`
|
||||||
|
- Return JSON response
|
||||||
|
|
||||||
|
**Keuntungan:**
|
||||||
|
- ✅ Selalu up-to-date dengan routes terbaru
|
||||||
|
- ✅ Tidak perlu manual update
|
||||||
|
- ✅ Swagger UI otomatis menampilkan endpoint baru
|
||||||
|
|
||||||
|
**Kekurangan:**
|
||||||
|
- ⚠️ Perlu database connection (routes butuh DB untuk register)
|
||||||
|
- ⚠️ Sedikit overhead saat request (generate setiap kali)
|
||||||
|
|
||||||
|
### Opsi 3: Manual Update (Default)
|
||||||
|
|
||||||
|
Default behavior: load dari file `public/docs/openapi.json`.
|
||||||
|
|
||||||
|
Jika `OPENAPI_AUTO_GENERATE=false` atau tidak di-set, akan load dari file.
|
||||||
|
|
||||||
|
## 🔧 Konfigurasi
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Enable auto-generate on request
|
||||||
|
OPENAPI_AUTO_GENERATE=true
|
||||||
|
|
||||||
|
# Default: false (load from file)
|
||||||
|
```
|
||||||
|
|
||||||
|
### File Locations
|
||||||
|
|
||||||
|
- **Generator class:** `src/Support/OpenAPIGenerator.php`
|
||||||
|
- **CLI script:** `bin/generate-openapi.php`
|
||||||
|
- **Output file:** `public/docs/openapi.json`
|
||||||
|
- **Swagger UI:** `/docs`
|
||||||
|
|
||||||
|
## 📝 Cara Kerja
|
||||||
|
|
||||||
|
1. **Scan Routes** - Generator scan semua routes yang terdaftar di Slim App
|
||||||
|
2. **Extract Metadata** - Extract method, path, parameters, security dari routes
|
||||||
|
3. **Generate Schema** - Generate request/response schema berdasarkan path patterns
|
||||||
|
4. **Build OpenAPI Spec** - Build complete OpenAPI 3.0 spec
|
||||||
|
5. **Save/Return** - Save ke file atau return sebagai JSON
|
||||||
|
|
||||||
|
## 🎨 Customization
|
||||||
|
|
||||||
|
### Custom Request Body Schema
|
||||||
|
|
||||||
|
Edit method `getRequestBodySchema()` di `src/Support/OpenAPIGenerator.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
private function getRequestBodySchema(string $pattern, $callable): ?array
|
||||||
|
{
|
||||||
|
// Add custom schema based on path pattern
|
||||||
|
if (strpos($pattern, '/custom-endpoint') !== false) {
|
||||||
|
return [
|
||||||
|
'required' => true,
|
||||||
|
'content' => [
|
||||||
|
'application/json' => [
|
||||||
|
'schema' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'field1' => ['type' => 'string'],
|
||||||
|
'field2' => ['type' => 'integer']
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Tags
|
||||||
|
|
||||||
|
Edit method `getTagFromPath()` untuk custom tag assignment:
|
||||||
|
|
||||||
|
```php
|
||||||
|
private function getTagFromPath(string $path): string
|
||||||
|
{
|
||||||
|
if (strpos($path, '/custom') !== false) {
|
||||||
|
return 'Custom';
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Parameters
|
||||||
|
|
||||||
|
Edit method `extractParameters()` untuk custom query parameters:
|
||||||
|
|
||||||
|
```php
|
||||||
|
private function extractParameters(string $pattern): array
|
||||||
|
{
|
||||||
|
$parameters = [];
|
||||||
|
|
||||||
|
// Add custom query params
|
||||||
|
if (strpos($pattern, '/custom') !== false) {
|
||||||
|
$parameters[] = [
|
||||||
|
'name' => 'custom_param',
|
||||||
|
'in' => 'query',
|
||||||
|
'schema' => ['type' => 'string']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $parameters;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Workflow
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
1. Tambah endpoint baru di routes
|
||||||
|
2. Run `php bin/generate-openapi.php`
|
||||||
|
3. Commit `openapi.json` yang ter-update
|
||||||
|
4. Swagger UI otomatis menampilkan endpoint baru
|
||||||
|
|
||||||
|
### Production (with auto-generate)
|
||||||
|
|
||||||
|
1. Set `OPENAPI_AUTO_GENERATE=true` di `.env`
|
||||||
|
2. Tambah endpoint baru di routes
|
||||||
|
3. Deploy
|
||||||
|
4. Swagger UI otomatis menampilkan endpoint baru (no commit needed)
|
||||||
|
|
||||||
|
### Production (without auto-generate)
|
||||||
|
|
||||||
|
1. Tambah endpoint baru di routes
|
||||||
|
2. Run `php bin/generate-openapi.php` di local/staging
|
||||||
|
3. Commit `openapi.json` yang ter-update
|
||||||
|
4. Deploy (include updated `openapi.json`)
|
||||||
|
|
||||||
|
## ⚠️ Limitations
|
||||||
|
|
||||||
|
1. **Database Required** - Routes perlu database connection untuk register
|
||||||
|
2. **Schema Inference** - Request body schema di-infer dari path pattern (bisa kurang akurat)
|
||||||
|
3. **No Reflection** - Tidak scan controller methods untuk extract detailed schema
|
||||||
|
4. **Manual Customization** - Complex schemas perlu manual edit di generator
|
||||||
|
|
||||||
|
## 🔮 Future Improvements
|
||||||
|
|
||||||
|
- [ ] Support PHP annotations untuk detailed schema
|
||||||
|
- [ ] Reflection-based schema extraction dari controller methods
|
||||||
|
- [ ] Support untuk custom OpenAPI extensions
|
||||||
|
- [ ] Cache generated spec untuk performance
|
||||||
|
- [ ] Support untuk multiple OpenAPI versions
|
||||||
|
|
||||||
|
## 📚 Related Files
|
||||||
|
|
||||||
|
- `src/Support/OpenAPIGenerator.php` - Generator class
|
||||||
|
- `bin/generate-openapi.php` - CLI script
|
||||||
|
- `public/index.php` - Auto-generate on request handler
|
||||||
|
- `public/docs/openapi.json` - Generated OpenAPI spec
|
||||||
|
- `public/docs/index.html` - Swagger UI
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ use App\Modules\Retribusi\Dashboard\DashboardRoutes;
|
|||||||
use App\Modules\Retribusi\Realtime\RealtimeRoutes;
|
use App\Modules\Retribusi\Realtime\RealtimeRoutes;
|
||||||
use App\Modules\Retribusi\RetribusiRoutes;
|
use App\Modules\Retribusi\RetribusiRoutes;
|
||||||
use App\Modules\Retribusi\Summary\SummaryRoutes;
|
use App\Modules\Retribusi\Summary\SummaryRoutes;
|
||||||
|
use App\Support\OpenAPIGenerator;
|
||||||
|
|
||||||
// Load environment variables
|
// Load environment variables
|
||||||
AppConfig::loadEnv(__DIR__ . '/..');
|
AppConfig::loadEnv(__DIR__ . '/..');
|
||||||
@@ -55,9 +56,29 @@ $app->get('/docs', function ($request, $response) {
|
|||||||
|
|
||||||
// Serve OpenAPI JSON
|
// Serve OpenAPI JSON
|
||||||
// NOTE: Saat ini PUBLIC. Jika perlu protect, tambahkan middleware
|
// NOTE: Saat ini PUBLIC. Jika perlu protect, tambahkan middleware
|
||||||
$app->get('/docs/openapi.json', function ($request, $response) {
|
$app->get('/docs/openapi.json', function ($request, $response) use ($app) {
|
||||||
$openApiPath = __DIR__ . '/docs/openapi.json';
|
$openApiPath = __DIR__ . '/docs/openapi.json';
|
||||||
|
$autoGenerate = AppConfig::get('OPENAPI_AUTO_GENERATE', 'false') === 'true';
|
||||||
|
|
||||||
|
// Auto-generate if enabled
|
||||||
|
if ($autoGenerate) {
|
||||||
|
try {
|
||||||
|
$generator = new OpenAPIGenerator($app);
|
||||||
|
$spec = $generator->generate();
|
||||||
|
$json = json_encode($spec, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
|
// Optionally save to file
|
||||||
|
file_put_contents($openApiPath, $json);
|
||||||
|
|
||||||
|
$response->getBody()->write($json);
|
||||||
|
return $response->withHeader('Content-Type', 'application/json');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Fallback to file if generation fails
|
||||||
|
error_log('OpenAPI generation failed: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load from file (default behavior)
|
||||||
if (!file_exists($openApiPath)) {
|
if (!file_exists($openApiPath)) {
|
||||||
$response->getBody()->write(json_encode(['error' => 'OpenAPI spec not found']));
|
$response->getBody()->write(json_encode(['error' => 'OpenAPI spec not found']));
|
||||||
return $response
|
return $response
|
||||||
|
|||||||
480
src/Support/OpenAPIGenerator.php
Normal file
480
src/Support/OpenAPIGenerator.php
Normal file
@@ -0,0 +1,480 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Support;
|
||||||
|
|
||||||
|
use ReflectionClass;
|
||||||
|
use ReflectionMethod;
|
||||||
|
use Slim\App;
|
||||||
|
use Slim\Routing\RouteCollector;
|
||||||
|
|
||||||
|
class OpenAPIGenerator
|
||||||
|
{
|
||||||
|
private App $app;
|
||||||
|
private array $baseSpec;
|
||||||
|
|
||||||
|
public function __construct(App $app)
|
||||||
|
{
|
||||||
|
$this->app = $app;
|
||||||
|
$this->baseSpec = $this->getBaseSpec();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate OpenAPI spec from registered routes
|
||||||
|
*/
|
||||||
|
public function generate(): array
|
||||||
|
{
|
||||||
|
$spec = $this->baseSpec;
|
||||||
|
$spec['paths'] = $this->scanRoutes();
|
||||||
|
|
||||||
|
return $spec;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get base OpenAPI spec structure
|
||||||
|
*/
|
||||||
|
private function getBaseSpec(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'openapi' => '3.0.0',
|
||||||
|
'info' => [
|
||||||
|
'title' => 'API Retribusi',
|
||||||
|
'description' => 'Sistem API Retribusi berbasis Slim Framework 4 untuk infrastruktur pemerintah',
|
||||||
|
'version' => '1.0.0',
|
||||||
|
'contact' => [
|
||||||
|
'name' => 'BTekno Development Team'
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'servers' => [
|
||||||
|
[
|
||||||
|
'url' => 'https://api.btekno.cloud',
|
||||||
|
'description' => 'Production Server'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'url' => 'http://localhost',
|
||||||
|
'description' => 'Local Development'
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'tags' => [
|
||||||
|
['name' => 'Health', 'description' => 'Health check endpoint'],
|
||||||
|
['name' => 'Authentication', 'description' => 'JWT authentication'],
|
||||||
|
['name' => 'Ingest', 'description' => 'Event ingestion (mesin YOLO)'],
|
||||||
|
['name' => 'Frontend', 'description' => 'Frontend CRUD operations'],
|
||||||
|
['name' => 'Summary', 'description' => 'Data summary & aggregation'],
|
||||||
|
['name' => 'Dashboard', 'description' => 'Dashboard visualization data'],
|
||||||
|
['name' => 'Realtime', 'description' => 'Real-time events (SSE)']
|
||||||
|
],
|
||||||
|
'components' => [
|
||||||
|
'securitySchemes' => [
|
||||||
|
'BearerAuth' => [
|
||||||
|
'type' => 'http',
|
||||||
|
'scheme' => 'bearer',
|
||||||
|
'bearerFormat' => 'JWT',
|
||||||
|
'description' => 'JWT token untuk frontend API'
|
||||||
|
],
|
||||||
|
'ApiKeyAuth' => [
|
||||||
|
'type' => 'apiKey',
|
||||||
|
'in' => 'header',
|
||||||
|
'name' => 'X-API-KEY',
|
||||||
|
'description' => 'API Key untuk ingest endpoint'
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'schemas' => [
|
||||||
|
'Error' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'error' => ['type' => 'string', 'description' => 'Error code'],
|
||||||
|
'message' => ['type' => 'string', 'description' => 'Error message'],
|
||||||
|
'fields' => ['type' => 'object', 'description' => 'Validation errors (optional)']
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'Success' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'success' => ['type' => 'boolean', 'example' => true],
|
||||||
|
'data' => ['type' => 'object'],
|
||||||
|
'timestamp' => ['type' => 'integer', 'description' => 'Unix timestamp']
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'paths' => []
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scan all registered routes
|
||||||
|
*/
|
||||||
|
private function scanRoutes(): array
|
||||||
|
{
|
||||||
|
$paths = [];
|
||||||
|
$routeCollector = $this->app->getRouteCollector();
|
||||||
|
$routes = $routeCollector->getRoutes();
|
||||||
|
|
||||||
|
foreach ($routes as $route) {
|
||||||
|
$methods = $route->getMethods();
|
||||||
|
$pattern = $route->getPattern();
|
||||||
|
$callable = $route->getCallable();
|
||||||
|
|
||||||
|
// Skip internal routes
|
||||||
|
if ($this->shouldSkipRoute($pattern)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert Slim route pattern to OpenAPI path
|
||||||
|
$openApiPath = $this->convertToOpenApiPath($pattern);
|
||||||
|
|
||||||
|
// Get route metadata
|
||||||
|
$tag = $this->getTagFromPath($pattern);
|
||||||
|
$summary = $this->getSummaryFromRoute($pattern, $methods[0] ?? 'GET');
|
||||||
|
$description = $this->getDescriptionFromRoute($pattern, $methods[0] ?? 'GET');
|
||||||
|
|
||||||
|
// Get security requirements
|
||||||
|
$security = $this->getSecurityFromRoute($route, $pattern);
|
||||||
|
|
||||||
|
// Get parameters from path
|
||||||
|
$parameters = $this->extractParameters($pattern);
|
||||||
|
|
||||||
|
// Get request body schema (for POST/PUT)
|
||||||
|
$requestBody = null;
|
||||||
|
if (in_array($methods[0] ?? '', ['POST', 'PUT', 'PATCH'])) {
|
||||||
|
$requestBody = $this->getRequestBodySchema($pattern, $callable);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process each HTTP method
|
||||||
|
foreach ($methods as $method) {
|
||||||
|
$methodLower = strtolower($method);
|
||||||
|
|
||||||
|
if (!isset($paths[$openApiPath])) {
|
||||||
|
$paths[$openApiPath] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$paths[$openApiPath][$methodLower] = [
|
||||||
|
'tags' => [$tag],
|
||||||
|
'summary' => $summary,
|
||||||
|
'description' => $description,
|
||||||
|
'security' => $security,
|
||||||
|
'parameters' => $parameters,
|
||||||
|
'responses' => $this->getDefaultResponses($pattern, $method)
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($requestBody !== null) {
|
||||||
|
$paths[$openApiPath][$methodLower]['requestBody'] = $requestBody;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if route should be skipped
|
||||||
|
*/
|
||||||
|
private function shouldSkipRoute(string $pattern): bool
|
||||||
|
{
|
||||||
|
$skipPatterns = [
|
||||||
|
'/',
|
||||||
|
'/docs',
|
||||||
|
'/docs/openapi.json',
|
||||||
|
'/{routes:.+}' // OPTIONS route
|
||||||
|
];
|
||||||
|
|
||||||
|
return in_array($pattern, $skipPatterns);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert Slim route pattern to OpenAPI path format
|
||||||
|
*/
|
||||||
|
private function convertToOpenApiPath(string $pattern): string
|
||||||
|
{
|
||||||
|
// Convert {param} to {param} (already OpenAPI format)
|
||||||
|
// Remove regex patterns like {routes:.+}
|
||||||
|
$path = preg_replace('/\{([^:}]+):[^}]+\}/', '{$1}', $pattern);
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get tag from path
|
||||||
|
*/
|
||||||
|
private function getTagFromPath(string $path): string
|
||||||
|
{
|
||||||
|
if (strpos($path, '/health') === 0) {
|
||||||
|
return 'Health';
|
||||||
|
}
|
||||||
|
if (strpos($path, '/auth') === 0) {
|
||||||
|
return 'Authentication';
|
||||||
|
}
|
||||||
|
if (strpos($path, '/ingest') !== false) {
|
||||||
|
return 'Ingest';
|
||||||
|
}
|
||||||
|
if (strpos($path, '/frontend') !== false) {
|
||||||
|
return 'Frontend';
|
||||||
|
}
|
||||||
|
if (strpos($path, '/summary') !== false) {
|
||||||
|
return 'Summary';
|
||||||
|
}
|
||||||
|
if (strpos($path, '/dashboard') !== false) {
|
||||||
|
return 'Dashboard';
|
||||||
|
}
|
||||||
|
if (strpos($path, '/realtime') !== false) {
|
||||||
|
return 'Realtime';
|
||||||
|
}
|
||||||
|
return 'Frontend';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get summary from route
|
||||||
|
*/
|
||||||
|
private function getSummaryFromRoute(string $path, string $method): string
|
||||||
|
{
|
||||||
|
$pathParts = explode('/', trim($path, '/'));
|
||||||
|
$lastPart = end($pathParts);
|
||||||
|
|
||||||
|
// Remove path parameters
|
||||||
|
$lastPart = preg_replace('/\{[^}]+\}/', '', $lastPart);
|
||||||
|
$lastPart = trim($lastPart, '/');
|
||||||
|
|
||||||
|
if (empty($lastPart)) {
|
||||||
|
$lastPart = $pathParts[count($pathParts) - 2] ?? 'list';
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = match ($method) {
|
||||||
|
'GET' => 'Get',
|
||||||
|
'POST' => 'Create',
|
||||||
|
'PUT' => 'Update',
|
||||||
|
'PATCH' => 'Update',
|
||||||
|
'DELETE' => 'Delete',
|
||||||
|
default => 'Handle'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert snake_case/kebab-case to Title Case
|
||||||
|
$lastPart = str_replace(['-', '_'], ' ', $lastPart);
|
||||||
|
$lastPart = ucwords($lastPart);
|
||||||
|
|
||||||
|
return $action . ' ' . $lastPart;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get description from route
|
||||||
|
*/
|
||||||
|
private function getDescriptionFromRoute(string $path, string $method): string
|
||||||
|
{
|
||||||
|
$summary = $this->getSummaryFromRoute($path, $method);
|
||||||
|
return $summary . ' endpoint';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get security requirements from route
|
||||||
|
*/
|
||||||
|
private function getSecurityFromRoute($route, string $path): array
|
||||||
|
{
|
||||||
|
$security = [];
|
||||||
|
|
||||||
|
// Check if route has JWT middleware (based on path patterns)
|
||||||
|
if (
|
||||||
|
strpos($path, '/frontend') !== false ||
|
||||||
|
strpos($path, '/summary') !== false ||
|
||||||
|
strpos($path, '/dashboard') !== false ||
|
||||||
|
strpos($path, '/realtime') !== false
|
||||||
|
) {
|
||||||
|
$security[] = ['BearerAuth' => []];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if route has API key middleware
|
||||||
|
if (strpos($path, '/ingest') !== false) {
|
||||||
|
$security[] = ['ApiKeyAuth' => []];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $security;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract parameters from path pattern
|
||||||
|
*/
|
||||||
|
private function extractParameters(string $pattern): array
|
||||||
|
{
|
||||||
|
$parameters = [];
|
||||||
|
preg_match_all('/\{([^}:]+)(?::[^}]+)?\}/', $pattern, $matches);
|
||||||
|
|
||||||
|
foreach ($matches[1] as $paramName) {
|
||||||
|
// Skip special parameters
|
||||||
|
if ($paramName === 'routes') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parameters[] = [
|
||||||
|
'name' => $paramName,
|
||||||
|
'in' => 'path',
|
||||||
|
'required' => true,
|
||||||
|
'schema' => ['type' => 'string'],
|
||||||
|
'description' => ucfirst(str_replace('_', ' ', $paramName))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add common query parameters for list endpoints
|
||||||
|
if (preg_match('/\/(locations|gates|tariffs|audit-logs|entry-events|events)(\/|$)/', $pattern)) {
|
||||||
|
$parameters[] = [
|
||||||
|
'name' => 'page',
|
||||||
|
'in' => 'query',
|
||||||
|
'schema' => ['type' => 'integer', 'default' => 1, 'minimum' => 1],
|
||||||
|
'description' => 'Page number'
|
||||||
|
];
|
||||||
|
$parameters[] = [
|
||||||
|
'name' => 'limit',
|
||||||
|
'in' => 'query',
|
||||||
|
'schema' => ['type' => 'integer', 'default' => 20, 'minimum' => 1, 'maximum' => 100],
|
||||||
|
'description' => 'Items per page'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add date parameters for summary/dashboard endpoints
|
||||||
|
if (strpos($pattern, '/summary') !== false || strpos($pattern, '/dashboard') !== false) {
|
||||||
|
$parameters[] = [
|
||||||
|
'name' => 'date',
|
||||||
|
'in' => 'query',
|
||||||
|
'schema' => ['type' => 'string', 'format' => 'date'],
|
||||||
|
'description' => 'Date (Y-m-d format)'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get request body schema
|
||||||
|
*/
|
||||||
|
private function getRequestBodySchema(string $pattern, $callable): ?array
|
||||||
|
{
|
||||||
|
// Try to infer schema from path and method
|
||||||
|
$schema = ['type' => 'object', 'properties' => []];
|
||||||
|
|
||||||
|
if (strpos($pattern, '/locations') !== false) {
|
||||||
|
$schema['required'] = ['code', 'name', 'type', 'is_active'];
|
||||||
|
$schema['properties'] = [
|
||||||
|
'code' => ['type' => 'string', 'example' => 'kerkof_01'],
|
||||||
|
'name' => ['type' => 'string', 'example' => 'Kerkof Garut'],
|
||||||
|
'type' => ['type' => 'string', 'example' => 'parkir'],
|
||||||
|
'is_active' => ['type' => 'integer', 'enum' => [0, 1], 'example' => 1]
|
||||||
|
];
|
||||||
|
} elseif (strpos($pattern, '/gates') !== false) {
|
||||||
|
$schema['required'] = ['location_code', 'gate_code', 'name', 'direction', 'is_active'];
|
||||||
|
$schema['properties'] = [
|
||||||
|
'location_code' => ['type' => 'string', 'example' => 'kerkof_01'],
|
||||||
|
'gate_code' => ['type' => 'string', 'example' => 'gate01'],
|
||||||
|
'name' => ['type' => 'string', 'example' => 'Gate 01'],
|
||||||
|
'direction' => ['type' => 'string', 'enum' => ['in', 'out'], 'example' => 'in'],
|
||||||
|
'camera' => ['type' => 'string', 'description' => 'Camera URL (HLS, RTSP, HTTP) atau camera ID', 'example' => 'https://example.com/stream.m3u8', 'maxLength' => 500],
|
||||||
|
'is_active' => ['type' => 'integer', 'enum' => [0, 1], 'example' => 1]
|
||||||
|
];
|
||||||
|
} elseif (strpos($pattern, '/tariffs') !== false) {
|
||||||
|
$schema['required'] = ['location_code', 'gate_code', 'category', 'price'];
|
||||||
|
$schema['properties'] = [
|
||||||
|
'location_code' => ['type' => 'string'],
|
||||||
|
'gate_code' => ['type' => 'string'],
|
||||||
|
'category' => ['type' => 'string', 'enum' => ['person_walk', 'motor', 'car']],
|
||||||
|
'price' => ['type' => 'integer', 'minimum' => 0]
|
||||||
|
];
|
||||||
|
} elseif (strpos($pattern, '/ingest') !== false) {
|
||||||
|
$schema['required'] = ['timestamp', 'location_code', 'gate_code', 'category'];
|
||||||
|
$schema['properties'] = [
|
||||||
|
'timestamp' => ['type' => 'integer', 'description' => 'Unix timestamp', 'example' => 1735123456],
|
||||||
|
'location_code' => ['type' => 'string', 'example' => 'kerkof_01'],
|
||||||
|
'gate_code' => ['type' => 'string', 'example' => 'gate01'],
|
||||||
|
'category' => ['type' => 'string', 'example' => 'motor']
|
||||||
|
];
|
||||||
|
} elseif (strpos($pattern, '/auth') !== false && strpos($pattern, '/login') !== false) {
|
||||||
|
$schema['required'] = ['username', 'password'];
|
||||||
|
$schema['properties'] = [
|
||||||
|
'username' => ['type' => 'string', 'example' => 'admin'],
|
||||||
|
'password' => ['type' => 'string', 'format' => 'password', 'example' => 'password123']
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
// Generic schema
|
||||||
|
$schema['properties'] = ['data' => ['type' => 'object']];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'required' => true,
|
||||||
|
'content' => [
|
||||||
|
'application/json' => [
|
||||||
|
'schema' => $schema
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get default responses
|
||||||
|
*/
|
||||||
|
private function getDefaultResponses(string $pattern, string $method): array
|
||||||
|
{
|
||||||
|
$responses = [
|
||||||
|
'200' => [
|
||||||
|
'description' => 'Success',
|
||||||
|
'content' => [
|
||||||
|
'application/json' => [
|
||||||
|
'schema' => ['$ref' => '#/components/schemas/Success']
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add method-specific responses
|
||||||
|
if ($method === 'POST') {
|
||||||
|
$responses['201'] = [
|
||||||
|
'description' => 'Created',
|
||||||
|
'content' => [
|
||||||
|
'application/json' => [
|
||||||
|
'schema' => ['$ref' => '#/components/schemas/Success']
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add common error responses
|
||||||
|
$responses['401'] = [
|
||||||
|
'description' => 'Unauthorized',
|
||||||
|
'content' => [
|
||||||
|
'application/json' => [
|
||||||
|
'schema' => ['$ref' => '#/components/schemas/Error']
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
if (strpos($pattern, '/frontend') !== false) {
|
||||||
|
$responses['403'] = [
|
||||||
|
'description' => 'Forbidden',
|
||||||
|
'content' => [
|
||||||
|
'application/json' => [
|
||||||
|
'schema' => ['$ref' => '#/components/schemas/Error']
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($method === 'POST' || $method === 'PUT') {
|
||||||
|
$responses['422'] = [
|
||||||
|
'description' => 'Validation error',
|
||||||
|
'content' => [
|
||||||
|
'application/json' => [
|
||||||
|
'schema' => ['$ref' => '#/components/schemas/Error']
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($method === 'GET' || $method === 'PUT' || $method === 'DELETE') {
|
||||||
|
$responses['404'] = [
|
||||||
|
'description' => 'Not found',
|
||||||
|
'content' => [
|
||||||
|
'application/json' => [
|
||||||
|
'schema' => ['$ref' => '#/components/schemas/Error']
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $responses;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user