feat(api): allow deprecation without sunset date

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-01 21:56:10 +00:00
parent 691ef936d4
commit 93cdb62dfe
4 changed files with 90 additions and 15 deletions

View file

@ -40,6 +40,14 @@ use Symfony\Component\HttpFoundation\Response;
* Route::middleware('api.sunset:2025-06-01,/api/v2/new-endpoint')->group(function () {
* Route::get('/old-endpoint', OldController::class);
* });
*
* You can also mark a route as deprecated without a fixed removal date:
*
* ```php
* Route::middleware('api.sunset')->group(function () {
* Route::get('/old-endpoint', OldController::class);
* });
* ```
* ```
*
* ## Response Headers
@ -56,29 +64,31 @@ class ApiSunset
/**
* Handle an incoming request.
*
* @param string $sunsetDate The sunset date (YYYY-MM-DD or RFC7231 format)
* @param string $sunsetDate The sunset date (YYYY-MM-DD or RFC7231 format), or empty for deprecation-only
* @param string|null $replacement Optional replacement endpoint URL
*/
public function handle(Request $request, Closure $next, string $sunsetDate, ?string $replacement = null): Response
public function handle(Request $request, Closure $next, string $sunsetDate = '', ?string $replacement = null): Response
{
/** @var Response $response */
$response = $next($request);
// Convert date to RFC7231 format if needed
$formattedDate = $this->formatSunsetDate($sunsetDate);
if ($sunsetDate !== '') {
// Convert date to RFC7231 format if needed
$formattedDate = $this->formatSunsetDate($sunsetDate);
// Add Sunset header
$response->headers->set('Sunset', $formattedDate);
// Add Sunset header
$response->headers->set('Sunset', $formattedDate);
}
// Add Deprecation header
$response->headers->set('Deprecation', 'true');
// Add warning header
$version = $request->attributes->get('api_version', 'unknown');
$response->headers->set(
'X-API-Warn',
"This endpoint is deprecated and will be removed on {$sunsetDate}."
);
$warning = 'This endpoint is deprecated.';
if ($sunsetDate !== '') {
$warning = "This endpoint is deprecated and will be removed on {$sunsetDate}.";
}
$response->headers->set('X-API-Warn', $warning);
// Add Link header for replacement if provided
if ($replacement !== null) {

View file

@ -246,11 +246,17 @@ class VersionedRoutes
{
$middleware = ["api.version:{$this->version}"];
if ($this->isDeprecated && $this->sunsetDate) {
if ($this->replacement !== null && $this->replacement !== '') {
$middleware[] = "api.sunset:{$this->sunsetDate},{$this->replacement}";
if ($this->isDeprecated) {
if ($this->sunsetDate !== null && $this->sunsetDate !== '') {
if ($this->replacement !== null && $this->replacement !== '') {
$middleware[] = "api.sunset:{$this->sunsetDate},{$this->replacement}";
} else {
$middleware[] = "api.sunset:{$this->sunsetDate}";
}
} elseif ($this->replacement !== null && $this->replacement !== '') {
$middleware[] = "api.sunset:,$this->replacement";
} else {
$middleware[] = "api.sunset:{$this->sunsetDate}";
$middleware[] = 'api.sunset';
}
}

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
use Core\Front\Api\Middleware\ApiSunset;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
it('adds deprecation headers without a sunset date', function () {
$middleware = new ApiSunset();
$request = Request::create('/legacy-endpoint', 'GET');
$response = $middleware->handle($request, fn () => new Response('OK'));
expect($response->headers->get('Deprecation'))->toBe('true');
expect($response->headers->has('Sunset'))->toBeFalse();
expect($response->headers->has('Link'))->toBeFalse();
expect($response->headers->get('X-API-Warn'))->toBe('This endpoint is deprecated.');
});
it('adds a replacement link without a sunset date', function () {
$middleware = new ApiSunset();
$request = Request::create('/old-endpoint', 'GET');
$response = $middleware->handle($request, fn () => new Response('OK'), '', '/api/v4/users');
expect($response->headers->get('Deprecation'))->toBe('true');
expect($response->headers->has('Sunset'))->toBeFalse();
expect($response->headers->get('Link'))->toBe('</api/v4/users>; rel="successor-version"');
expect($response->headers->get('X-API-Warn'))->toBe('This endpoint is deprecated.');
});

View file

@ -32,3 +32,31 @@ it('preserves the existing deprecated signature without a replacement url', func
expect($attributes['middleware'])->toContain('api.sunset:2025-06-01');
expect($attributes['middleware'])->not->toContain('api.sunset:2025-06-01,/api/v3/users');
});
it('keeps deprecated routes active without a sunset date', function () {
$routes = new class (3) extends VersionedRoutes {
public function attributes(): array
{
return $this->buildRouteAttributes();
}
};
$attributes = $routes->deprecated()->attributes();
expect($attributes['middleware'])->toContain('api.version:3');
expect($attributes['middleware'])->toContain('api.sunset');
});
it('passes a replacement url through deprecated versioned routes without a sunset date', function () {
$routes = new class (4) extends VersionedRoutes {
public function attributes(): array
{
return $this->buildRouteAttributes();
}
};
$attributes = $routes->deprecated(null, '/api/v4/users')->attributes();
expect($attributes['middleware'])->toContain('api.version:4');
expect($attributes['middleware'])->toContain('api.sunset:,/api/v4/users');
});