-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #285 from dotkernel/issue-169
Implemented API evolution pattern
- Loading branch information
Showing
13 changed files
with
516 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Api\App\Attribute; | ||
|
||
use Api\App\Exception\DeprecationSunsetException; | ||
use Api\App\Message; | ||
use Attribute; | ||
use Laminas\Validator\Date; | ||
|
||
use function sprintf; | ||
|
||
#[Attribute(Attribute::TARGET_METHOD)] | ||
final readonly class MethodDeprecation | ||
{ | ||
public function __construct(public string $sunset, public string $link, public string $deprecationReason = '') | ||
{ | ||
if (! (new Date())->isValid($sunset)) { | ||
throw new DeprecationSunsetException(sprintf(Message::INVALID_VALUE, 'sunset')); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Api\App\Attribute; | ||
|
||
use Api\App\Exception\DeprecationSunsetException; | ||
use Api\App\Message; | ||
use Attribute; | ||
use Laminas\Validator\Date; | ||
|
||
use function sprintf; | ||
|
||
#[Attribute(Attribute::TARGET_CLASS)] | ||
final readonly class ResourceDeprecation | ||
{ | ||
public function __construct(public string $sunset, public string $link, public string $deprecationReason = '') | ||
{ | ||
if (! (new Date())->isValid($sunset)) { | ||
throw new DeprecationSunsetException(sprintf(Message::INVALID_VALUE, 'sunset')); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Api\App\Exception; | ||
|
||
use RuntimeException; | ||
|
||
class DeprecationConflictException extends RuntimeException | ||
{ | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Api\App\Exception; | ||
|
||
use RuntimeException; | ||
|
||
class DeprecationSunsetException extends RuntimeException | ||
{ | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Api\App\Middleware; | ||
|
||
use Api\App\Attribute\MethodDeprecation; | ||
use Api\App\Attribute\ResourceDeprecation; | ||
use Api\App\Exception\DeprecationConflictException; | ||
use Api\App\Handler\ResponseTrait; | ||
use Api\App\Message; | ||
use Laminas\Stratigility\MiddlewarePipe; | ||
use Mezzio\Middleware\LazyLoadingMiddleware; | ||
use Mezzio\Router\RouteResult; | ||
use Psr\Http\Message\ResponseInterface; | ||
use Psr\Http\Message\ServerRequestInterface; | ||
use Psr\Http\Server\MiddlewareInterface; | ||
use Psr\Http\Server\RequestHandlerInterface; | ||
use ReflectionClass; | ||
use ReflectionException; | ||
use ReflectionMethod; | ||
|
||
use function array_key_exists; | ||
use function array_keys; | ||
use function sprintf; | ||
|
||
class DeprecationMiddleware implements MiddlewareInterface | ||
{ | ||
use ResponseTrait; | ||
|
||
public const RESOURCE_DEPRECATION_ATTRIBUTE = ResourceDeprecation::class; | ||
public const METHOD_DEPRECATION_ATTRIBUTE = MethodDeprecation::class; | ||
|
||
/** | ||
* @throws ReflectionException | ||
*/ | ||
public function process( | ||
ServerRequestInterface $request, | ||
RequestHandlerInterface $handler | ||
): ResponseInterface { | ||
$response = $handler->handle($request); | ||
$routeResult = $request->getAttribute(RouteResult::class); | ||
if (! $routeResult instanceof RouteResult || $routeResult->isFailure()) { | ||
return $response; | ||
} | ||
|
||
$reflectionHandler = null; | ||
$matchedRoute = $routeResult->getMatchedRoute(); | ||
if (! $matchedRoute) { | ||
return $response; | ||
} | ||
|
||
$routeMiddleware = $matchedRoute->getMiddleware(); | ||
if ($routeMiddleware instanceof LazyLoadingMiddleware) { | ||
/** @var class-string $routeMiddlewareName */ | ||
$routeMiddlewareName = $routeMiddleware->middlewareName; | ||
$reflectionMiddlewareClass = new ReflectionClass($routeMiddlewareName); | ||
if ($reflectionMiddlewareClass->implementsInterface(RequestHandlerInterface::class)) { | ||
$reflectionHandler = $reflectionMiddlewareClass; | ||
} | ||
} elseif ($routeMiddleware instanceof MiddlewarePipe) { | ||
$reflectionClass = new ReflectionClass($routeMiddleware); | ||
$middlewarePipeline = $reflectionClass->getProperty('pipeline')->getValue($routeMiddleware); | ||
for ($middlewarePipeline->rewind(); $middlewarePipeline->valid(); $middlewarePipeline->next()) { | ||
$reflectionMiddlewareClass = new ReflectionClass($middlewarePipeline->current()->middlewareName); | ||
if ($reflectionMiddlewareClass->implementsInterface(RequestHandlerInterface::class)) { | ||
$reflectionHandler = $reflectionMiddlewareClass; | ||
} | ||
} | ||
} | ||
|
||
if (! $reflectionHandler) { | ||
return $response; | ||
} | ||
|
||
$attributes = $this->getAttributes($reflectionHandler, self::RESOURCE_DEPRECATION_ATTRIBUTE); | ||
foreach ($reflectionHandler->getMethods() as $method) { | ||
$methodRef = new ReflectionMethod($method->class, $method->name); | ||
$attributes += $this->getAttributes($methodRef, self::METHOD_DEPRECATION_ATTRIBUTE); | ||
} | ||
|
||
if ([self::RESOURCE_DEPRECATION_ATTRIBUTE, self::METHOD_DEPRECATION_ATTRIBUTE] === array_keys($attributes)) { | ||
throw new DeprecationConflictException( | ||
sprintf( | ||
Message::RESTRICTION_DEPRECATION, | ||
self::RESOURCE_DEPRECATION_ATTRIBUTE, | ||
self::METHOD_DEPRECATION_ATTRIBUTE | ||
) | ||
); | ||
} | ||
|
||
$sunset = null; | ||
$link = null; | ||
if (array_key_exists(self::RESOURCE_DEPRECATION_ATTRIBUTE, $attributes)) { | ||
$sunset = $attributes[self::RESOURCE_DEPRECATION_ATTRIBUTE]['sunset']; | ||
$link = $attributes[self::RESOURCE_DEPRECATION_ATTRIBUTE]['link']; | ||
} | ||
|
||
if (array_key_exists(self::METHOD_DEPRECATION_ATTRIBUTE, $attributes)) { | ||
$sunset = $attributes[self::METHOD_DEPRECATION_ATTRIBUTE]['sunset']; | ||
$link = $attributes[self::METHOD_DEPRECATION_ATTRIBUTE]['link']; | ||
} | ||
|
||
if ($sunset !== null) { | ||
$response = $response->withHeader('sunset', $sunset); | ||
} | ||
|
||
if ($link !== null) { | ||
$response = $response->withHeader('link', $link); | ||
} | ||
|
||
return $response; | ||
} | ||
|
||
/** | ||
* @param class-string $type | ||
*/ | ||
public function getAttributes(ReflectionClass|ReflectionMethod $reflection, string $type): array | ||
{ | ||
$attributes = []; | ||
foreach ($reflection->getAttributes($type) as $attribute) { | ||
$attribute->newInstance(); | ||
$attributes[$attribute->getName()] = $attribute->getArguments(); | ||
} | ||
|
||
return $attributes; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace ApiTest\Unit\App\Attribute; | ||
|
||
use Api\App\Attribute\MethodDeprecation; | ||
use Api\App\Exception\DeprecationSunsetException; | ||
use PHPUnit\Framework\TestCase; | ||
use ReflectionClass; | ||
|
||
class MethodDeprecationTest extends TestCase | ||
{ | ||
public function testInvalidDateThrowsException(): void | ||
{ | ||
$class = new class { | ||
#[MethodDeprecation( | ||
sunset: 'invalid-01-01', | ||
link: 'test-link', | ||
deprecationReason: 'test-deprecation-reason', | ||
)] | ||
public function test(): void | ||
{ | ||
} | ||
}; | ||
|
||
$reflectionClass = new ReflectionClass($class); | ||
$attributes = $this->getAttributes($reflectionClass); | ||
|
||
$this->expectException(DeprecationSunsetException::class); | ||
|
||
$attributes[0]->newInstance(); | ||
} | ||
|
||
public function testValidDatePassesValidation(): void | ||
{ | ||
$class = new class { | ||
#[MethodDeprecation( | ||
sunset: '2038-01-01', | ||
link: 'test-link', | ||
deprecationReason: 'test-deprecation-reason', | ||
)] | ||
public function test(): void | ||
{ | ||
} | ||
}; | ||
|
||
$reflectionClass = new ReflectionClass($class); | ||
$attributes = $this->getAttributes($reflectionClass); | ||
|
||
$attribute = $attributes[0]->newInstance(); | ||
|
||
$this->assertSame('2038-01-01', $attribute->sunset); | ||
$this->assertSame('test-link', $attribute->link); | ||
$this->assertSame('test-deprecation-reason', $attribute->deprecationReason); | ||
} | ||
|
||
private function getAttributes(ReflectionClass $reflectionClass): array | ||
{ | ||
$methods = $reflectionClass->getMethods(); | ||
return $methods[0]->getAttributes(MethodDeprecation::class); | ||
} | ||
} |
Oops, something went wrong.