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\RetribusiRoutes;
|
||||
use App\Modules\Retribusi\Summary\SummaryRoutes;
|
||||
use App\Support\OpenAPIGenerator;
|
||||
|
||||
// Load environment variables
|
||||
AppConfig::loadEnv(__DIR__ . '/..');
|
||||
@@ -55,9 +56,29 @@ $app->get('/docs', function ($request, $response) {
|
||||
|
||||
// Serve OpenAPI JSON
|
||||
// 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';
|
||||
$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)) {
|
||||
$response->getBody()->write(json_encode(['error' => 'OpenAPI spec not found']));
|
||||
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