Skip to content

Commit

Permalink
Implemented API evolution
Browse files Browse the repository at this point in the history
  • Loading branch information
MarioRadu committed Jun 14, 2024
1 parent ace75bc commit 4daef72
Show file tree
Hide file tree
Showing 13 changed files with 505 additions and 0 deletions.
2 changes: 2 additions & 0 deletions config/pipeline.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Api\App\Middleware\AuthenticationMiddleware;
use Api\App\Middleware\AuthorizationMiddleware;
use Api\App\Middleware\ContentNegotiationMiddleware;
use Api\App\Middleware\DeprecationMiddleware;
use Dot\ErrorHandler\ErrorHandlerInterface;
use Dot\ResponseHeader\Middleware\ResponseHeaderMiddleware;
use Mezzio\Application;
Expand Down Expand Up @@ -61,6 +62,7 @@
$app->pipe(MethodNotAllowedMiddleware::class);

$app->pipe(ContentNegotiationMiddleware::class);
$app->pipe(DeprecationMiddleware::class);

$app->pipe(ResponseHeaderMiddleware::class);

Expand Down
23 changes: 23 additions & 0 deletions src/App/src/Attribute/MethodDeprecation.php
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;

Check warning on line 10 in src/App/src/Attribute/MethodDeprecation.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined class

Undefined class 'Date'

Check warning on line 10 in src/App/src/Attribute/MethodDeprecation.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined namespace

Undefined namespace 'Validator'

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)) {

Check warning on line 19 in src/App/src/Attribute/MethodDeprecation.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined class

Undefined class 'Date'
throw new DeprecationSunsetException(sprintf(Message::INVALID_VALUE, 'sunset'));
}
}
}
23 changes: 23 additions & 0 deletions src/App/src/Attribute/ResourceDeprecation.php
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;

Check warning on line 10 in src/App/src/Attribute/ResourceDeprecation.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined class

Undefined class 'Date'

Check warning on line 10 in src/App/src/Attribute/ResourceDeprecation.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined namespace

Undefined namespace 'Validator'

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)) {

Check warning on line 19 in src/App/src/Attribute/ResourceDeprecation.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined class

Undefined class 'Date'
throw new DeprecationSunsetException(sprintf(Message::INVALID_VALUE, 'sunset'));
}
}
}
4 changes: 4 additions & 0 deletions src/App/src/ConfigProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
use Api\App\Factory\EntityListenerResolverFactory;
use Api\App\Factory\RouteListCommandFactory;
use Api\App\Factory\TokenGenerateCommandFactory;
use Api\App\Handler\ErrorReportHandler;
use Api\App\Handler\HomeHandler;
use Api\App\Middleware\AuthenticationMiddleware;
use Api\App\Middleware\AuthorizationMiddleware;
use Api\App\Middleware\ContentNegotiationMiddleware;
use Api\App\Middleware\DeprecationMiddleware;
use Api\App\Middleware\ErrorResponseMiddleware;
use Api\App\Service\ErrorReportService;
use Api\App\Service\ErrorReportServiceInterface;
Expand Down Expand Up @@ -66,10 +68,12 @@ public function getDependencies(): array
AuthenticationMiddleware::class => AuthenticationMiddlewareFactory::class,
AuthorizationMiddleware::class => AttributedServiceFactory::class,
ContentNegotiationMiddleware::class => AttributedServiceFactory::class,
DeprecationMiddleware::class => AttributedServiceFactory::class,

Check warning on line 71 in src/App/src/ConfigProvider.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined class

Undefined class 'AttributedServiceFactory'
Environment::class => TwigEnvironmentFactory::class,
TwigExtension::class => TwigExtensionFactory::class,
TwigRenderer::class => TwigRendererFactory::class,
HomeHandler::class => AttributedServiceFactory::class,
ErrorReportHandler::class => AttributedServiceFactory::class,
ErrorResponseMiddleware::class => AttributedServiceFactory::class,
RouteListCommand::class => RouteListCommandFactory::class,
TokenGenerateCommand::class => TokenGenerateCommandFactory::class,
Expand Down
11 changes: 11 additions & 0 deletions src/App/src/Exception/DeprecationConflictException.php
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
{
}
11 changes: 11 additions & 0 deletions src/App/src/Exception/DeprecationSunsetException.php
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
{
}
6 changes: 6 additions & 0 deletions src/App/src/Handler/ErrorReportHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Api\App\Handler;

use Api\App\Attribute\MethodDeprecation;
use Api\App\Exception\ForbiddenException;
use Api\App\Message;
use Api\App\Service\ErrorReportServiceInterface;
Expand Down Expand Up @@ -38,6 +39,11 @@ public function __construct(
* @throws ForbiddenException
* @throws RuntimeException
*/
#[MethodDeprecation(
sunset: '2038-01-01',
link: 'https://docs.dotkernel.org/api-documentation/v5/core-features/versioning',
deprecationReason: 'Method deprecation example.',
)]
public function post(ServerRequestInterface $request): ResponseInterface
{
$this->errorReportService
Expand Down
6 changes: 6 additions & 0 deletions src/App/src/Handler/HomeHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,18 @@

namespace Api\App\Handler;

use Api\App\Attribute\ResourceDeprecation;
use Dot\DependencyInjection\Attribute\Inject;
use Mezzio\Hal\HalResponseFactory;
use Mezzio\Hal\ResourceGenerator;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\RequestHandlerInterface;

#[ResourceDeprecation(
sunset: '2038-01-01',
link: 'https://docs.dotkernel.org/api-documentation/v5/core-features/versioning',
deprecationReason: 'Resource deprecation example.',
)]
class HomeHandler implements RequestHandlerInterface
{
use HandlerTrait;
Expand Down
1 change: 1 addition & 0 deletions src/App/src/Message.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,5 @@ class Message
public const VALIDATOR_REQUIRED_FIELD = 'This field is required and cannot be empty.';
public const VALIDATOR_REQUIRED_FIELD_BY_NAME = '%s is required and cannot be empty.';
public const VALIDATOR_REQUIRED_UPLOAD = 'A file must be uploaded first.';
public const RESTRICTION_DEPRECATION = 'Cannot use both `%s` and `%s` attributes on the same object.';
}
116 changes: 116 additions & 0 deletions src/App/src/Middleware/DeprecationMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?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 Mezzio\Middleware\LazyLoadingMiddleware;

Check warning on line 12 in src/App/src/Middleware/DeprecationMiddleware.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined class

Undefined class 'LazyLoadingMiddleware'

Check warning on line 12 in src/App/src/Middleware/DeprecationMiddleware.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined namespace

Undefined namespace 'Middleware'
use Mezzio\Router\RouteResult;

Check warning on line 13 in src/App/src/Middleware/DeprecationMiddleware.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined class

Undefined class 'RouteResult'

Check warning on line 13 in src/App/src/Middleware/DeprecationMiddleware.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined namespace

Undefined namespace 'Router'
use Psr\Http\Message\ResponseInterface;

Check warning on line 14 in src/App/src/Middleware/DeprecationMiddleware.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined class

Undefined class 'ResponseInterface'

Check warning on line 14 in src/App/src/Middleware/DeprecationMiddleware.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined namespace

Undefined namespace 'Message'
use Psr\Http\Message\ServerRequestInterface;

Check warning on line 15 in src/App/src/Middleware/DeprecationMiddleware.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined class

Undefined class 'ServerRequestInterface'

Check warning on line 15 in src/App/src/Middleware/DeprecationMiddleware.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined namespace

Undefined namespace 'Message'
use Psr\Http\Server\MiddlewareInterface;

Check warning on line 16 in src/App/src/Middleware/DeprecationMiddleware.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined class

Undefined class 'MiddlewareInterface'

Check warning on line 16 in src/App/src/Middleware/DeprecationMiddleware.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined namespace

Undefined namespace 'Server'
use Psr\Http\Server\RequestHandlerInterface;

Check warning on line 17 in src/App/src/Middleware/DeprecationMiddleware.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined class

Undefined class 'RequestHandlerInterface'

Check warning on line 17 in src/App/src/Middleware/DeprecationMiddleware.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined namespace

Undefined namespace 'Server'
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;

use function array_keys;
use function sprintf;

class DeprecationMiddleware implements MiddlewareInterface

Check warning on line 25 in src/App/src/Middleware/DeprecationMiddleware.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined class

Undefined class 'MiddlewareInterface'
{
use ResponseTrait;

public const RESOURCE_DEPRECATION_ATTRIBUTE = ResourceDeprecation::class;
public const METHOD_DEPRECATION_ATTRIBUTE = MethodDeprecation::class;

/**
* @throws ReflectionException
*/
public function process(
ServerRequestInterface $request,

Check warning on line 36 in src/App/src/Middleware/DeprecationMiddleware.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined class

Undefined class 'ServerRequestInterface'
RequestHandlerInterface $handler

Check warning on line 37 in src/App/src/Middleware/DeprecationMiddleware.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined class

Undefined class 'RequestHandlerInterface'
): ResponseInterface {

Check warning on line 38 in src/App/src/Middleware/DeprecationMiddleware.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined class

Undefined class 'ResponseInterface'
$response = $handler->handle($request);
$routeResult = $request->getAttribute(RouteResult::class);

Check warning on line 40 in src/App/src/Middleware/DeprecationMiddleware.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined class

Undefined class 'RouteResult'
if (! $routeResult instanceof RouteResult || $routeResult->isFailure()) {

Check warning on line 41 in src/App/src/Middleware/DeprecationMiddleware.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined class

Undefined class 'RouteResult'
return $response;
}

$reflectionHandler = null;
$routeMiddleware = $routeResult->getMatchedRoute()->getMiddleware();
if ($routeMiddleware instanceof LazyLoadingMiddleware) {

Check warning on line 47 in src/App/src/Middleware/DeprecationMiddleware.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined class

Undefined class 'LazyLoadingMiddleware'
$reflectionMiddlewareClass = new ReflectionClass($routeMiddleware->middlewareName);
if ($reflectionMiddlewareClass->implementsInterface(RequestHandlerInterface::class)) {

Check warning on line 49 in src/App/src/Middleware/DeprecationMiddleware.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined class

Undefined class 'RequestHandlerInterface'
$reflectionHandler = $reflectionMiddlewareClass;
}
} else {
$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)) {

Check warning on line 57 in src/App/src/Middleware/DeprecationMiddleware.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined class

Undefined class 'RequestHandlerInterface'
$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 = '';
$link = '';
if ($attributes[self::RESOURCE_DEPRECATION_ATTRIBUTE] ?? '') {
$sunset = $attributes[self::RESOURCE_DEPRECATION_ATTRIBUTE]['sunset'];
$link = $attributes[self::RESOURCE_DEPRECATION_ATTRIBUTE]['link'];
}

if ($attributes[self::METHOD_DEPRECATION_ATTRIBUTE] ?? '') {
$sunset = $attributes[self::METHOD_DEPRECATION_ATTRIBUTE]['sunset'];
$link = $attributes[self::METHOD_DEPRECATION_ATTRIBUTE]['link'];
}

if ($sunset) {
$response = $response->withHeader('sunset', $sunset);
}

if ($link) {
$response = $response->withHeader('link', $link);
}

return $response;
}

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;
}
}
63 changes: 63 additions & 0 deletions test/Unit/App/Attribute/MethodDeprecationTest.php
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;

Check warning on line 9 in test/Unit/App/Attribute/MethodDeprecationTest.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined class

Undefined class 'TestCase'

Check warning on line 9 in test/Unit/App/Attribute/MethodDeprecationTest.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined namespace

Undefined namespace 'Framework'
use ReflectionClass;

class MethodDeprecationTest extends TestCase

Check warning on line 12 in test/Unit/App/Attribute/MethodDeprecationTest.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined class

Undefined class 'TestCase'
{
public function testInvalidDateThrowsException(): void
{
$class = new class {
#[MethodDeprecation(
sunset: 'invalid-01-01',
link: 'test-link',
deprecationReason: 'test-deprecation-reason',
)]
public function test()
{
}
};

$reflectionClass = new ReflectionClass($class);
$attributes = $this->getAttributes($reflectionClass);

$this->expectException(DeprecationSunsetException::class);

Check warning on line 30 in test/Unit/App/Attribute/MethodDeprecationTest.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined method

Method 'expectException' is undefined

$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()
{
}
};

$reflectionClass = new ReflectionClass($class);
$attributes = $this->getAttributes($reflectionClass);

$attribute = $attributes[0]->newInstance();

$this->assertSame('2038-01-01', $attribute->sunset);

Check warning on line 53 in test/Unit/App/Attribute/MethodDeprecationTest.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined method

Method 'assertSame' is undefined
$this->assertSame('test-link', $attribute->link);

Check warning on line 54 in test/Unit/App/Attribute/MethodDeprecationTest.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined method

Method 'assertSame' is undefined
$this->assertSame('test-deprecation-reason', $attribute->deprecationReason);

Check warning on line 55 in test/Unit/App/Attribute/MethodDeprecationTest.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined method

Method 'assertSame' is undefined
}

private function getAttributes(ReflectionClass $reflectionClass): array
{
$methods = $reflectionClass->getMethods();
return $methods[0]->getAttributes(MethodDeprecation::class);
}
}
49 changes: 49 additions & 0 deletions test/Unit/App/Attribute/ResourceDeprecationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace ApiTest\Unit\App\Attribute;

use Api\App\Attribute\ResourceDeprecation;
use Api\App\Exception\DeprecationSunsetException;
use PHPUnit\Framework\TestCase;

Check warning on line 9 in test/Unit/App/Attribute/ResourceDeprecationTest.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined class

Undefined class 'TestCase'

Check warning on line 9 in test/Unit/App/Attribute/ResourceDeprecationTest.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined namespace

Undefined namespace 'Framework'
use ReflectionClass;

class ResourceDeprecationTest extends TestCase

Check warning on line 12 in test/Unit/App/Attribute/ResourceDeprecationTest.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined class

Undefined class 'TestCase'
{
public function testInvalidDateThrowsException(): void
{
$class = new #[ResourceDeprecation(
sunset: 'invalid-01-01',
link: 'test-link',
deprecationReason: 'test-deprecation-reason',
)] class {
};

$reflectionClass = new ReflectionClass($class);
$attributes = $reflectionClass->getAttributes(ResourceDeprecation::class);

$this->expectException(DeprecationSunsetException::class);

Check warning on line 26 in test/Unit/App/Attribute/ResourceDeprecationTest.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined method

Method 'expectException' is undefined

$attributes[0]->newInstance();
}

public function testValidDatePassesValidation(): void
{
$class = new #[ResourceDeprecation(
sunset: '2038-01-01',
link: 'test-link',
deprecationReason: 'test-deprecation-reason',
)] class {
};

$reflectionClass = new ReflectionClass($class);
$attributes = $reflectionClass->getAttributes(ResourceDeprecation::class);

$attribute = $attributes[0]->newInstance();

$this->assertSame('2038-01-01', $attribute->sunset);

Check warning on line 45 in test/Unit/App/Attribute/ResourceDeprecationTest.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined method

Method 'assertSame' is undefined
$this->assertSame('test-link', $attribute->link);

Check warning on line 46 in test/Unit/App/Attribute/ResourceDeprecationTest.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined method

Method 'assertSame' is undefined
$this->assertSame('test-deprecation-reason', $attribute->deprecationReason);

Check warning on line 47 in test/Unit/App/Attribute/ResourceDeprecationTest.php

View workflow job for this annotation

GitHub Actions / Qodana for PHP

Undefined method

Method 'assertSame' is undefined
}
}
Loading

0 comments on commit 4daef72

Please sign in to comment.