diff --git a/composer.json b/composer.json index d7a9ea7..4ad948e 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,7 @@ ], "require": { "php": "^7.0", + "doctrine/annotations": "^1.4", "psr/http-message": "^1.0", "react/promise": "^2.4" }, diff --git a/composer.lock b/composer.lock index 90aeb21..2894def 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,130 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "c33ff5abc88b076be8abba58d43acf0f", + "content-hash": "d2e31ee763fbb9a82426fd3ee9b9cce5", "packages": [ + { + "name": "doctrine/annotations", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/annotations.git", + "reference": "54cacc9b81758b14e3ce750f205a393d52339e97" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/54cacc9b81758b14e3ce750f205a393d52339e97", + "reference": "54cacc9b81758b14e3ce750f205a393d52339e97", + "shasum": "" + }, + "require": { + "doctrine/lexer": "1.*", + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "doctrine/cache": "1.*", + "phpunit/phpunit": "^5.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Docblock Annotations Parser", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "annotations", + "docblock", + "parser" + ], + "time": "2017-02-24T16:22:25+00:00" + }, + { + "name": "doctrine/lexer", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/83893c552fd2045dd78aef794c31e694c37c0b8c", + "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "Doctrine\\Common\\Lexer\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Base library for a lexer that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "lexer", + "parser" + ], + "time": "2014-09-09T13:34:57+00:00" + }, { "name": "psr/http-message", "version": "1.0.1", @@ -194,74 +316,6 @@ ], "time": "2016-03-09T15:10:22+00:00" }, - { - "name": "doctrine/annotations", - "version": "v1.4.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/annotations.git", - "reference": "54cacc9b81758b14e3ce750f205a393d52339e97" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/54cacc9b81758b14e3ce750f205a393d52339e97", - "reference": "54cacc9b81758b14e3ce750f205a393d52339e97", - "shasum": "" - }, - "require": { - "doctrine/lexer": "1.*", - "php": "^5.6 || ^7.0" - }, - "require-dev": { - "doctrine/cache": "1.*", - "phpunit/phpunit": "^5.7" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4.x-dev" - } - }, - "autoload": { - "psr-4": { - "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - } - ], - "description": "Docblock Annotations Parser", - "homepage": "http://www.doctrine-project.org", - "keywords": [ - "annotations", - "docblock", - "parser" - ], - "time": "2017-02-24T16:22:25+00:00" - }, { "name": "doctrine/instantiator", "version": "1.0.5", @@ -316,60 +370,6 @@ ], "time": "2015-06-14T21:17:01+00:00" }, - { - "name": "doctrine/lexer", - "version": "v1.0.1", - "source": { - "type": "git", - "url": "https://github.com/doctrine/lexer.git", - "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/83893c552fd2045dd78aef794c31e694c37c0b8c", - "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c", - "shasum": "" - }, - "require": { - "php": ">=5.3.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-0": { - "Doctrine\\Common\\Lexer\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - } - ], - "description": "Base library for a lexer that can be used in Top-Down, Recursive Descent Parsers.", - "homepage": "http://www.doctrine-project.org", - "keywords": [ - "lexer", - "parser" - ], - "time": "2014-09-09T13:34:57+00:00" - }, { "name": "friendsofphp/php-cs-fixer", "version": "v2.3.2", diff --git a/src/Annotation/Priority.php b/src/Annotation/Priority.php new file mode 100644 index 0000000..81d782b --- /dev/null +++ b/src/Annotation/Priority.php @@ -0,0 +1,31 @@ +priority = current($priorities); + } + + /** + * @return int + */ + public function priority(): int + { + return $this->priority; + } +} diff --git a/src/MiddlewareInterface.php b/src/MiddlewareInterface.php index 4135bd4..6fb7218 100644 --- a/src/MiddlewareInterface.php +++ b/src/MiddlewareInterface.php @@ -13,8 +13,10 @@ interface MiddlewareInterface { /** - * Priority ranging from 0 to 1000. Where 1000 will be executed first on `pre` and 0 last on `pre`. - * For `post` the order is reversed. + * Priority ranging from 0 to 1000. Where 1000 will be executed first on `pre`/`post`/`error` + * and 0 last on `pre`/`post`/`error`. + * + * @deprecated Use annotations for more fine grained control * * @return int */ diff --git a/src/MiddlewareRunner.php b/src/MiddlewareRunner.php index 85570c0..3b25aeb 100644 --- a/src/MiddlewareRunner.php +++ b/src/MiddlewareRunner.php @@ -2,9 +2,12 @@ namespace ApiClients\Foundation\Middleware; +use ApiClients\Foundation\Middleware\Annotation\Priority as PriorityAnnotation; +use Doctrine\Common\Annotations\AnnotationReader; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use React\Promise\CancellablePromiseInterface; +use ReflectionMethod; use Throwable; use function React\Promise\reject; use function React\Promise\resolve; @@ -21,6 +24,11 @@ final class MiddlewareRunner */ private $middlewares; + /** + * @var AnnotationReader + */ + private $annotationReader; + /** * @var string */ @@ -34,8 +42,9 @@ final class MiddlewareRunner public function __construct(array $options, MiddlewareInterface ...$middlewares) { $this->options = $options; - $this->middlewares = $this->orderMiddlewares(...$middlewares); $this->id = bin2hex(random_bytes(32)); + $this->middlewares = $middlewares; + $this->annotationReader = new AnnotationReader(); } /** @@ -47,7 +56,10 @@ public function pre( ): CancellablePromiseInterface { $promise = resolve($request); - foreach ($this->middlewares as $middleware) { + $middlewares = $this->middlewares; + $middlewares = $this->orderMiddlewares('pre', ...$middlewares); + + foreach ($middlewares as $middleware) { $requestMiddleware = $middleware; $promise = $promise->then(function (RequestInterface $request) use ($requestMiddleware) { return $requestMiddleware->pre($request, $this->options, $this->id); @@ -66,9 +78,10 @@ public function post( ): CancellablePromiseInterface { $promise = resolve($response); - $this->middlewares = array_reverse($this->middlewares); + $middlewares = $this->middlewares; + $middlewares = $this->orderMiddlewares('post', ...$middlewares); - foreach ($this->middlewares as $middleware) { + foreach ($middlewares as $middleware) { $responseMiddleware = $middleware; $promise = $promise->then(function (ResponseInterface $response) use ($responseMiddleware) { return $responseMiddleware->post($response, $this->options, $this->id); @@ -87,9 +100,10 @@ public function error( ): CancellablePromiseInterface { $promise = reject($throwable); - $this->middlewares = array_reverse($this->middlewares); + $middlewares = $this->middlewares; + $middlewares = $this->orderMiddlewares('error', ...$middlewares); - foreach ($this->middlewares as $middleware) { + foreach ($middlewares as $middleware) { $errorMiddleware = $middleware; $promise = $promise->then(null, function (Throwable $throwable) use ($errorMiddleware) { return reject($errorMiddleware->error($throwable, $this->options, $this->id)); @@ -102,15 +116,31 @@ public function error( /** * Sort the middlewares by priority. * + * @param string $method * @param MiddlewareInterface[] $middlewares * @return array */ - protected function orderMiddlewares(MiddlewareInterface ...$middlewares): array + protected function orderMiddlewares(string $method, MiddlewareInterface ...$middlewares): array { - usort($middlewares, function (MiddlewareInterface $left, MiddlewareInterface $right) { - return $right->priority() <=> $left->priority(); + usort($middlewares, function (MiddlewareInterface $left, MiddlewareInterface $right) use ($method) { + return $this->getPriority($method, $right) <=> $this->getPriority($method, $left); }); return $middlewares; } + + private function getPriority(string $method, MiddlewareInterface $middleware): int + { + $methodReflection = new ReflectionMethod($middleware, $method); + /** @var PriorityAnnotation $annotation */ + $annotation = $this->annotationReader->getMethodAnnotation($methodReflection, PriorityAnnotation::class); + + if ($annotation !== null && + get_class($annotation) === PriorityAnnotation::class + ) { + return $annotation->priority(); + } + + return $middleware->priority(); + } } diff --git a/tests/Annotation/PriorityTest.php b/tests/Annotation/PriorityTest.php new file mode 100644 index 0000000..2328752 --- /dev/null +++ b/tests/Annotation/PriorityTest.php @@ -0,0 +1,16 @@ +priority()); + } +} diff --git a/tests/MiddlewareRunnerTest.php b/tests/MiddlewareRunnerTest.php index 7ed2f75..45fb7c7 100644 --- a/tests/MiddlewareRunnerTest.php +++ b/tests/MiddlewareRunnerTest.php @@ -4,6 +4,8 @@ use ApiClients\Foundation\Middleware\MiddlewareInterface; use ApiClients\Foundation\Middleware\MiddlewareRunner; +use ApiClients\Tests\Foundation\Middleware\TestMiddlewares\OneMiddleware; +use ApiClients\Tests\Foundation\Middleware\TestMiddlewares\TwoMiddleware; use ApiClients\Tools\TestUtilities\TestCase; use Closure; use Exception; @@ -68,12 +70,51 @@ public function testAll() Phake::verify($middlewareOne)->pre($request, $options, $id), Phake::verify($middlewareTwo)->pre($request, $options, $id), Phake::verify($middlewareThree)->pre($request, $options, $id), - Phake::verify($middlewareThree)->post($response, $options, $id), - Phake::verify($middlewareTwo)->post($response, $options, $id), Phake::verify($middlewareOne)->post($response, $options, $id), + Phake::verify($middlewareTwo)->post($response, $options, $id), + Phake::verify($middlewareThree)->post($response, $options, $id), Phake::verify($middlewareOne)->error($exception, $options, $id), Phake::verify($middlewareTwo)->error($exception, $options, $id), Phake::verify($middlewareThree)->error($exception, $options, $id) ); } + + public function testAnnotations() + { + $loop = Factory::create(); + $request = new Request('GET', 'https://example.com/'); + $response = new Response(200); + $exception = new Exception(); + $options = []; + + $middlewareOne = new OneMiddleware(); + $middlewareTwo = new TwoMiddleware(); + + $args = [ + $options, + $middlewareOne, + $middlewareTwo, + ]; + + $executioner = new MiddlewareRunner(...$args); + self::assertSame($request, await($executioner->pre($request), $loop)); + self::assertSame($response, await($executioner->post($response), $loop)); + try { + await($executioner->error($exception), $loop); + } catch (Throwable $throwable) { + self::assertSame($exception, $throwable); + } + + $calls = array_merge_recursive($middlewareOne->getCalls(), $middlewareTwo->getCalls()); + ksort($calls); + + self::assertSame([ + TwoMiddleware::class . ':pre', + OneMiddleware::class . ':pre', + OneMiddleware::class . ':post', + TwoMiddleware::class . ':post', + OneMiddleware::class . ':error', + TwoMiddleware::class . ':error', + ], array_values($calls)); + } } diff --git a/tests/TestMiddlewares/OneMiddleware.php b/tests/TestMiddlewares/OneMiddleware.php new file mode 100644 index 0000000..f779121 --- /dev/null +++ b/tests/TestMiddlewares/OneMiddleware.php @@ -0,0 +1,80 @@ +calls; + } + + /** + * @param RequestInterface $request + * @param array $options + * @return CancellablePromiseInterface + * @PriorityAnnotation(Priority::LAST); + */ + public function pre( + RequestInterface $request, + array $options = [], + string $transactionId = null + ): CancellablePromiseInterface { + usleep(100); + $this->calls[(string)microtime(true)] = __CLASS__ . ':pre'; + + return resolve($request); + } + + /** + * @param ResponseInterface $response + * @param array $options + * @return CancellablePromiseInterface + * @PriorityAnnotation(Priority::FIRST); + */ + public function post( + ResponseInterface $response, + array $options = [], + string $transactionId = null + ): CancellablePromiseInterface { + usleep(100); + $this->calls[(string)microtime(true)] = __CLASS__ . ':post'; + + return resolve($response); + } + + /** + * @param Throwable $throwable + * @param array $options + * @return CancellablePromiseInterface + * @PriorityAnnotation(Priority::FIRST); + */ + public function error( + Throwable $throwable, + array $options = [], + string $transactionId = null + ): CancellablePromiseInterface { + usleep(100); + $this->calls[(string)microtime(true)] = __CLASS__ . ':error'; + + return reject($throwable); + } +} diff --git a/tests/TestMiddlewares/TwoMiddleware.php b/tests/TestMiddlewares/TwoMiddleware.php new file mode 100644 index 0000000..abcfca8 --- /dev/null +++ b/tests/TestMiddlewares/TwoMiddleware.php @@ -0,0 +1,75 @@ +calls; + } + + /** + * @param RequestInterface $request + * @param array $options + * @return CancellablePromiseInterface + */ + public function pre( + RequestInterface $request, + array $options = [], + string $transactionId = null + ): CancellablePromiseInterface { + usleep(100); + $this->calls[(string)microtime(true)] = __CLASS__ . ':pre'; + + return resolve($request); + } + + /** + * @param ResponseInterface $response + * @param array $options + * @return CancellablePromiseInterface + */ + public function post( + ResponseInterface $response, + array $options = [], + string $transactionId = null + ): CancellablePromiseInterface { + usleep(100); + $this->calls[(string)microtime(true)] = __CLASS__ . ':post'; + + return resolve($response); + } + + /** + * @param Throwable $throwable + * @param array $options + * @return CancellablePromiseInterface + */ + public function error( + Throwable $throwable, + array $options = [], + string $transactionId = null + ): CancellablePromiseInterface { + usleep(100); + $this->calls[(string)microtime(true)] = __CLASS__ . ':error'; + + return reject($throwable); + } +}