Skip to content

Commit faecae4

Browse files
authored
Merge pull request #570 from dotkernel/issue-556
updated sunset mechanism
2 parents 5a64329 + 9539a10 commit faecae4

2 files changed

Lines changed: 134 additions & 13 deletions

File tree

‎src/App/src/Middleware/DeprecationMiddleware.php‎

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
namespace Api\App\Middleware;
66

77
use Api\App\Attribute\ResourceDeprecation;
8-
use Api\App\Exception\ConflictException;
8+
use Api\App\Exception\ExpiredException;
99
use Api\App\Exception\RuntimeException;
1010
use Api\App\Service\HandlerService;
11+
use DateTimeImmutable;
1112
use Dot\DependencyInjection\Attribute\Inject;
1213
use Psr\Http\Message\ResponseInterface;
1314
use Psr\Http\Message\ServerRequestInterface;
@@ -20,15 +21,14 @@
2021
use function array_merge;
2122
use function array_values;
2223
use function implode;
23-
use function is_string;
2424
use function sprintf;
2525

2626
class DeprecationMiddleware implements MiddlewareInterface
2727
{
2828
public const string RESOURCE_DEPRECATION_ATTRIBUTE = ResourceDeprecation::class;
2929

3030
/**
31-
* @param array<non-empty-string, mixed> $config
31+
* @param array{documentation_url?: non-empty-string} $config
3232
*/
3333
#[Inject(
3434
'config.application.versioning',
@@ -39,44 +39,69 @@ public function __construct(
3939
}
4040

4141
/**
42-
* @throws ConflictException
42+
* @throws ExpiredException
4343
* @throws ReflectionException
4444
* @throws RuntimeException
4545
*/
4646
public function process(
4747
ServerRequestInterface $request,
4848
RequestHandlerInterface $handler
4949
): ResponseInterface {
50-
$response = $handler->handle($request);
51-
5250
$reflectionHandler = HandlerService::getReflectionHandler($request);
5351
if (! $reflectionHandler instanceof ReflectionClass) {
54-
return $response;
52+
return $handler->handle($request);
5553
}
5654

5755
$attributes = $this->getReflectionAttributes($reflectionHandler);
5856
if (empty($attributes)) {
59-
return $response;
57+
return $handler->handle($request);
6058
}
6159

6260
$attribute = $this->getAttribute($attributes);
6361
if (null === $attribute) {
64-
return $response;
62+
return $handler->handle($request);
63+
}
64+
65+
$documentationUrl = $this->getDocumentationUrl($attribute);
66+
67+
if ($this->isSunsetReached($attribute['sunset'])) {
68+
throw ExpiredException::create(
69+
detail: sprintf('This resource is no longer available (sunset on %s).', $attribute['sunset']),
70+
type: $documentationUrl ?? '',
71+
);
6572
}
6673

74+
$response = $handler->handle($request);
75+
6776
if (! empty($attribute['sunset'])) {
6877
$response = $response->withHeader('sunset', $attribute['sunset']);
6978
}
7079

71-
/** @var non-empty-string $baseUrl */
72-
$baseUrl = $attribute['link'] ?? $this->config['documentation_url'] ?? null;
73-
if (is_string($baseUrl)) {
74-
$response = $response->withHeader('link', $this->formatLink($baseUrl, $attribute));
80+
if (null !== $documentationUrl) {
81+
$response = $response->withHeader('link', $this->formatLink($documentationUrl, $attribute));
7582
}
7683

7784
return $response;
7885
}
7986

87+
/**
88+
* @param array<non-empty-string, mixed> $attribute
89+
* @return non-empty-string|null
90+
*/
91+
private function getDocumentationUrl(array $attribute): ?string
92+
{
93+
return $attribute['link'] ?? $this->config['documentation_url'] ?? null;
94+
}
95+
96+
private function isSunsetReached(?string $sunset): bool
97+
{
98+
if (empty($sunset)) {
99+
return false;
100+
}
101+
102+
return new DateTimeImmutable() >= new DateTimeImmutable($sunset);
103+
}
104+
80105
/**
81106
* @param array<int, mixed> $attributes
82107
* @return array<non-empty-string, mixed>|null

‎test/Unit/App/Middleware/DeprecationMiddlewareTest.php‎

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66

77
use Api\App\Attribute\ResourceDeprecation;
88
use Api\App\Exception\ConflictException;
9+
use Api\App\Exception\ExpiredException;
910
use Api\App\Middleware\DeprecationMiddleware;
1011
use Fig\Http\Message\RequestMethodInterface;
12+
use Fig\Http\Message\StatusCodeInterface;
1113
use Laminas\Diactoros\Response\EmptyResponse;
1214
use Laminas\Stratigility\MiddlewarePipe;
1315
use Mezzio\Middleware\LazyLoadingMiddleware;
@@ -272,6 +274,100 @@ public function handle(ServerRequestInterface $request): ResponseInterface
272274
$this->assertSame('2038-01-01', $response->getHeader('sunset')[0]);
273275
}
274276

277+
/**
278+
* @throws Exception
279+
* @throws ReflectionException
280+
*/
281+
public function testPastSunsetThrowsExpiredExceptionWithoutRunningHandler(): void
282+
{
283+
$deprecated = new #[ResourceDeprecation(sunset: '2000-01-01', link: 'test-link')]
284+
class implements RequestHandlerInterface {
285+
public function handle(ServerRequestInterface $request): ResponseInterface
286+
{
287+
return new EmptyResponse();
288+
}
289+
};
290+
291+
$this->routeRequestTo($deprecated);
292+
293+
$handler = $this->createMock(RequestHandlerInterface::class);
294+
$handler->expects($this->never())->method('handle');
295+
296+
$this->expectException(ExpiredException::class);
297+
298+
$this->deprecationMiddleware->process($this->request, $handler);
299+
}
300+
301+
/**
302+
* @throws Exception
303+
* @throws ReflectionException
304+
*/
305+
public function testExpiredExceptionCarriesStatusMessageAndLink(): void
306+
{
307+
$deprecated = new #[ResourceDeprecation(sunset: '2000-01-01', link: 'test-link')]
308+
class implements RequestHandlerInterface {
309+
public function handle(ServerRequestInterface $request): ResponseInterface
310+
{
311+
return new EmptyResponse();
312+
}
313+
};
314+
315+
$this->routeRequestTo($deprecated);
316+
317+
try {
318+
$this->deprecationMiddleware->process($this->request, $this->handler);
319+
$this->fail('ExpiredException was not thrown');
320+
} catch (ExpiredException $exception) {
321+
$this->assertSame(StatusCodeInterface::STATUS_GONE, $exception->getStatus());
322+
$this->assertSame('This resource is no longer available (sunset on 2000-01-01).', $exception->getDetail());
323+
$this->assertSame('test-link', $exception->getType());
324+
}
325+
}
326+
327+
/**
328+
* @throws Exception
329+
* @throws ReflectionException
330+
*/
331+
public function testExpiredExceptionFallsBackToConfiguredDocumentationUrl(): void
332+
{
333+
$deprecated = new #[ResourceDeprecation(sunset: '2000-01-01')]
334+
class implements RequestHandlerInterface {
335+
public function handle(ServerRequestInterface $request): ResponseInterface
336+
{
337+
return new EmptyResponse();
338+
}
339+
};
340+
341+
$this->routeRequestTo($deprecated);
342+
343+
try {
344+
$this->deprecationMiddleware->process($this->request, $this->handler);
345+
$this->fail('ExpiredException was not thrown');
346+
} catch (ExpiredException $exception) {
347+
$this->assertSame(self::VERSIONING_CONFIG['documentation_url'], $exception->getType());
348+
}
349+
}
350+
351+
/**
352+
* @throws Exception
353+
*/
354+
private function routeRequestTo(RequestHandlerInterface $deprecated): void
355+
{
356+
$routeResult = $this->createStub(RouteResult::class);
357+
$route = $this->createStub(Route::class);
358+
$lazyLoadingMiddleware = new LazyLoadingMiddleware(
359+
$this->createStub(MiddlewareContainer::class),
360+
$deprecated::class,
361+
);
362+
363+
$route->method('getMiddleware')->willReturn($lazyLoadingMiddleware);
364+
$routeResult->method('isFailure')->willReturn(false);
365+
$routeResult->method('getMatchedRoute')->willReturn($route);
366+
$this->request->method('getAttribute')->willReturn($routeResult);
367+
$this->request->method('getMethod')->willReturn(RequestMethodInterface::METHOD_GET);
368+
$this->handler->method('handle')->willReturn($this->response);
369+
}
370+
275371
/**
276372
* @param non-empty-string $baseLink
277373
* @param array<non-empty-string, mixed> $attribute

0 commit comments

Comments
 (0)