Files
sam-api/app/Http/Middleware/ApiVersionMiddleware.php

172 lines
5.0 KiB
PHP
Raw Permalink Normal View History

<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Symfony\Component\HttpFoundation\Response;
/**
* API 버전 관리 미들웨어
*
* - 헤더 Accept-Version으로 버전 선택 (기본: v1)
* - 요청된 버전의 라우트가 없으면 하위 버전으로 fallback
* - 응답 헤더에 실제 사용된 버전 표시
*/
class ApiVersionMiddleware
{
/**
* 지원하는 API 버전 목록 (우선순위 )
*/
protected array $supportedVersions = ['v2', 'v1'];
/**
* 기본 버전
*/
protected string $defaultVersion = 'v1';
/**
* Handle an incoming request.
*/
public function handle(Request $request, Closure $next): Response
{
// 1. 요청된 버전 확인 (헤더 > 쿼리 파라미터 > 기본값)
$requestedVersion = $this->getRequestedVersion($request);
// 2. 실제 사용할 버전 결정 (fallback 적용)
$actualVersion = $this->resolveVersion($request, $requestedVersion);
// 3. 요청에 버전 정보 저장 (컨트롤러에서 사용 가능)
$request->attributes->set('api_version', $actualVersion);
$request->attributes->set('api_version_requested', $requestedVersion);
$request->attributes->set('api_version_fallback', $actualVersion !== $requestedVersion);
// 4. 요청 처리
$response = $next($request);
// 5. 응답 헤더에 버전 정보 추가
$response->headers->set('X-API-Version', $actualVersion);
if ($actualVersion !== $requestedVersion) {
$response->headers->set('X-API-Version-Fallback', 'true');
$response->headers->set('X-API-Version-Requested', $requestedVersion);
}
return $response;
}
/**
* 요청에서 버전 정보 추출
*/
protected function getRequestedVersion(Request $request): string
{
// 1. Accept-Version 헤더 (권장)
$version = $request->header('Accept-Version');
if ($version && $this->isValidVersion($version)) {
return $version;
}
// 2. X-API-Version 헤더 (대안)
$version = $request->header('X-API-Version');
if ($version && $this->isValidVersion($version)) {
return $version;
}
// 3. 쿼리 파라미터 (테스트용)
$version = $request->query('api_version');
if ($version && $this->isValidVersion($version)) {
return $version;
}
return $this->defaultVersion;
}
/**
* 유효한 버전인지 확인
*/
protected function isValidVersion(string $version): bool
{
return in_array($version, $this->supportedVersions, true);
}
/**
* 실제 사용할 버전 결정 (fallback 로직)
*/
protected function resolveVersion(Request $request, string $requestedVersion): string
{
// 요청된 버전부터 하위 버전까지 순차 확인
$startIndex = array_search($requestedVersion, $this->supportedVersions, true);
if ($startIndex === false) {
return $this->defaultVersion;
}
// 요청된 버전부터 하위 버전까지 체크
for ($i = $startIndex; $i < count($this->supportedVersions); $i++) {
$version = $this->supportedVersions[$i];
if ($this->versionRouteExists($request, $version)) {
return $version;
}
}
// 모든 버전에서 라우트를 찾지 못하면 기본값 반환
return $this->defaultVersion;
}
/**
* 해당 버전의 라우트가 존재하는지 확인
*/
protected function versionRouteExists(Request $request, string $version): bool
{
$path = $request->path();
// URL에서 버전 부분 교체
// /api/v1/users → /api/v2/users
$versionedPath = preg_replace('/^api\/v\d+/', "api/{$version}", $path);
// 해당 경로의 라우트가 존재하는지 확인
$routes = Route::getRoutes();
foreach ($routes as $route) {
$routeUri = $route->uri();
// 정확히 일치하거나 파라미터 패턴 매칭
if ($this->matchesRoute($versionedPath, $routeUri, $request->method())) {
return true;
}
}
return false;
}
/**
* 경로가 라우트와 일치하는지 확인
*/
protected function matchesRoute(string $path, string $routeUri, string $method): bool
{
// 라우트 URI의 파라미터를 정규식으로 변환
// {id} → [^/]+
$pattern = preg_replace('/\{[^}]+\}/', '[^/]+', $routeUri);
$pattern = '#^'.$pattern.'$#';
return (bool) preg_match($pattern, $path);
}
/**
* 지원 버전 목록 반환 (외부에서 사용)
*/
public function getSupportedVersions(): array
{
return $this->supportedVersions;
}
/**
* 기본 버전 반환
*/
public function getDefaultVersion(): string
{
return $this->defaultVersion;
}
}