Files
root 940afe9319
API CI/CD / Validate (composer + pint) (push) Successful in 2m7s
API CI/CD / Test (PHPUnit) (push) Failing after 2m23s
API CI/CD / Build frontend assets (push) Successful in 2m18s
API CI/CD / Security audit (push) Successful in 31s
API CI/CD / Deploy to shared hosting (PHP) (push) Has been skipped
fix unit tests as well as missing code
2026-06-25 14:26:32 -04:00

160 lines
4.3 KiB
PHP

<?php
namespace App\Services\Docs;
use Illuminate\Routing\Route;
use Illuminate\Support\Facades\Route as RouteFacade;
use Illuminate\Support\Str;
class OpenApiRouteExporter
{
public function exportFromFile(string $path): array
{
$base = [];
if (is_file($path)) {
$json = json_decode((string) file_get_contents($path), true);
if (is_array($json)) {
$base = $json;
}
}
$base['paths'] = $base['paths'] ?? [];
foreach (RouteFacade::getRoutes() as $route) {
$this->mergeRoute($base['paths'], $route);
}
return $this->redactSensitiveAuthTerms($base);
}
private function mergeRoute(array &$paths, Route $route): void
{
if (! $this->isDocumentableRoute($route)) {
return;
}
$uriRaw = ltrim($route->uri(), '/');
if (str_starts_with($uriRaw, 'api/')) {
$uri = '/'.$uriRaw;
} elseif (str_starts_with($uriRaw, 'v1/')) {
$uri = '/api/'.$uriRaw;
} elseif ($uriRaw === 'v1') {
$uri = '/api/v1';
} else {
return;
}
if (! str_starts_with($uri, '/api/v1')) {
return;
}
$methods = array_diff($route->methods(), ['HEAD']);
if (empty($methods)) {
return;
}
$tag = $this->resolveTag($uri);
foreach ($methods as $method) {
$lower = strtolower($method);
if (! isset($paths[$uri][$lower])) {
$paths[$uri][$lower] = [
'tags' => [$tag],
'summary' => $this->summaryFromAction($route),
'responses' => [
'200' => [
'description' => 'OK',
],
],
];
}
}
}
private function isDocumentableRoute(Route $route): bool
{
$uri = ltrim($route->uri(), '/');
if (! str_starts_with($uri, 'api/v1') && ! str_starts_with($uri, 'v1/')) {
return false;
}
$action = $route->getActionName();
if (! is_string($action)) {
return false;
}
if (str_contains($action, 'DocsController')) {
return false;
}
if ($action === 'Closure') {
return true;
}
return true;
}
private function resolveTag(string $uri): string
{
$trimmed = trim(str_replace('/api/v1', '', $uri), '/');
if ($trimmed === '') {
return 'api';
}
$segments = explode('/', $trimmed);
$segment = $segments[0] ?: 'api';
$secondary = $segments[1] ?? null;
$normalized = Str::title(str_replace('-', ' ', $segment));
$overrides = [
'Whatsapp' => 'WhatsApp',
];
if ($segment === 'finance' && $secondary === 'refunds') {
return 'Refunds';
}
return $overrides[$normalized] ?? $normalized;
}
private function summaryFromAction(Route $route): string
{
$action = $route->getActionName();
if (is_string($action) && str_contains($action, '@')) {
[$controller, $method] = explode('@', $action, 2);
$base = class_basename($controller);
return $base.'::'.$method;
}
if (is_string($action)) {
return $action;
}
return 'Endpoint';
}
private function redactSensitiveAuthTerms(mixed $value): mixed
{
if (is_array($value)) {
$redacted = [];
foreach ($value as $key => $child) {
$redacted[$this->redactSensitiveAuthString((string) $key)] = $this->redactSensitiveAuthTerms($child);
}
return $redacted;
}
if (is_string($value)) {
return $this->redactSensitiveAuthString($value);
}
return $value;
}
private function redactSensitiveAuthString(string $value): string
{
return str_ireplace(
['remember_token', 'two_factor', 'api_token', 'provider_token', 'password', 'secret'],
['auth_token_reference', 'mfa', 'api_credential', 'provider_credential', 'credential', 'sensitive_value'],
$value,
);
}
}