172 lines
5.0 KiB
PHP
172 lines
5.0 KiB
PHP
|
|
<?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;
|
||
|
|
}
|
||
|
|
}
|