diff --git a/src/Attribute/Middleware.php b/src/Attribute/Middleware.php new file mode 100644 index 0000000..479ec14 --- /dev/null +++ b/src/Attribute/Middleware.php @@ -0,0 +1,24 @@ +definition = $definition; + } + + public function getDefinition(): mixed + { + return $this->definition; + } +} diff --git a/src/MiddlewareDispatcher.php b/src/MiddlewareDispatcher.php index 1ac762b..b77c1a3 100644 --- a/src/MiddlewareDispatcher.php +++ b/src/MiddlewareDispatcher.php @@ -29,6 +29,9 @@ public function __construct( private MiddlewareFactory $middlewareFactory, private ?EventDispatcherInterface $eventDispatcher = null ) { + if ($eventDispatcher !== null && !$middlewareFactory->hasEventDispatcher()) { + $this->middlewareFactory = $this->middlewareFactory->withEventDispatcher($eventDispatcher); + } } /** diff --git a/src/MiddlewareFactory.php b/src/MiddlewareFactory.php index e9df36a..c01ea4f 100644 --- a/src/MiddlewareFactory.php +++ b/src/MiddlewareFactory.php @@ -6,17 +6,18 @@ use Closure; use Psr\Container\ContainerInterface; +use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use ReflectionClass; use ReflectionFunction; use ReflectionParameter; use Yiisoft\Definitions\ArrayDefinition; use Yiisoft\Definitions\Exception\InvalidConfigException; use Yiisoft\Definitions\Helpers\DefinitionValidator; use Yiisoft\Injector\Injector; +use Yiisoft\Middleware\Dispatcher\Attribute\Middleware; use function in_array; use function is_array; @@ -29,15 +30,29 @@ */ final class MiddlewareFactory { + private ?EventDispatcherInterface $eventDispatcher = null; + /** * @param ContainerInterface $container Container to use for resolving definitions. */ public function __construct( - private readonly ContainerInterface $container, - private readonly ?ParametersResolverInterface $parametersResolver = null + private ContainerInterface $container, + private ?ParametersResolverInterface $parametersResolver = null ) { } + public function withEventDispatcher(?EventDispatcherInterface $eventDispatcher): self + { + $new = clone $this; + $new->eventDispatcher = $eventDispatcher; + return $new; + } + + public function hasEventDispatcher(): bool + { + return $this->eventDispatcher !== null; + } + /** * @param array|callable|string $middlewareDefinition Middleware definition in one of the following formats: * @@ -167,12 +182,18 @@ private function wrapCallable(array|callable $callable): MiddlewareInterface return $this->createCallableWrapper($callable); } - return $this->createActionWrapper($callable[0], $callable[1]); + return $this->createCallableWrapper([$this->container->get($callable[0]), $callable[1]]); } private function createCallableWrapper(callable $callback): MiddlewareInterface { - return new class ($callback, $this->container, $this->parametersResolver) implements MiddlewareInterface { + return new class ( + $callback, + $this->container, + $this, + $this->eventDispatcher, + $this->parametersResolver, + ) implements MiddlewareInterface { /** @var callable */ private $callback; /** @@ -180,17 +201,23 @@ private function createCallableWrapper(callable $callback): MiddlewareInterface * @psalm-var array */ private array $callableParameters = []; + public array $middlewares = []; public function __construct( callable $callback, private readonly ContainerInterface $container, + private readonly MiddlewareFactory $middlewareFactory, + private readonly ?EventDispatcherInterface $eventDispatcher, private readonly ?ParametersResolverInterface $parametersResolver ) { $this->callback = $callback; - $callback = Closure::fromCallable($callback); - $callableParameters = (new ReflectionFunction($callback))->getParameters(); - foreach ($callableParameters as $parameter) { + $reflectionFunction = new ReflectionFunction(Closure::fromCallable($callback)); + + foreach ($reflectionFunction->getAttributes(Middleware::class) as $attribute) { + $this->middlewares[] = $attribute->newInstance()->getDefinition(); + } + foreach ($reflectionFunction->getParameters() as $parameter) { $this->callableParameters[$parameter->getName()] = $parameter; } } @@ -207,8 +234,16 @@ public function process( ); } - /** @var MiddlewareInterface|mixed|ResponseInterface $response */ - $response = (new Injector($this->container))->invoke($this->callback, $parameters); + if ($this->middlewares !== []) { + $middlewares = [...$this->middlewares, fn(): mixed => ($this->callback)()]; + $middlewareDispatcher = new MiddlewareDispatcher($this->middlewareFactory, $this->eventDispatcher); + /** @psalm-suppress MixedArgumentTypeCoercion */ + $middlewareDispatcher = $middlewareDispatcher->withMiddlewares($middlewares); + $response = $middlewareDispatcher->dispatch($request, $handler); + } else { + /** @var MiddlewareInterface|mixed|ResponseInterface $response */ + $response = (new Injector($this->container))->invoke($this->callback, $parameters); + } if ($response instanceof ResponseInterface) { return $response; } @@ -221,66 +256,13 @@ public function process( public function __debugInfo(): array { - return ['callback' => $this->callback]; - } - }; - } - - /** - * @param class-string $class - * @param non-empty-string $method - */ - private function createActionWrapper(string $class, string $method): MiddlewareInterface - { - return new class ($this->container, $this->parametersResolver, $class, $method) implements MiddlewareInterface { - /** - * @var ReflectionParameter[] - * @psalm-var array - */ - private array $actionParameters = []; - - public function __construct( - private readonly ContainerInterface $container, - private readonly ?ParametersResolverInterface $parametersResolver, - /** @var class-string */ - private readonly string $class, - /** @var non-empty-string */ - private readonly string $method - ) { - $actionParameters = (new ReflectionClass($this->class))->getMethod($this->method)->getParameters(); - foreach ($actionParameters as $parameter) { - $this->actionParameters[$parameter->getName()] = $parameter; + if (is_array($this->callback) + && isset($this->callback[0], $this->callback[1]) + && is_object($this->callback[0]) + ) { + return ['callback' => [$this->callback[0]::class, $this->callback[1]]]; } - } - - public function process( - ServerRequestInterface $request, - RequestHandlerInterface $handler - ): ResponseInterface { - /** @var mixed $controller */ - $controller = $this->container->get($this->class); - $parameters = [$request, $handler]; - if ($this->parametersResolver !== null) { - $parameters = array_merge( - $parameters, - $this->parametersResolver->resolve($this->actionParameters, $request) - ); - } - - /** @var mixed|ResponseInterface $response */ - $response = (new Injector($this->container))->invoke([$controller, $this->method], $parameters); - if ($response instanceof ResponseInterface) { - return $response; - } - - throw new InvalidMiddlewareDefinitionException([$this->class, $this->method]); - } - - public function __debugInfo() - { - return [ - 'callback' => [$this->class, $this->method], - ]; + return ['callback' => $this->callback]; } }; } diff --git a/src/MiddlewareStack.php b/src/MiddlewareStack.php index 61bbb11..7a0be92 100644 --- a/src/MiddlewareStack.php +++ b/src/MiddlewareStack.php @@ -32,7 +32,7 @@ final class MiddlewareStack implements RequestHandlerInterface */ public function __construct( private readonly array $middlewares, - private RequestHandlerInterface $fallbackHandler, + private readonly RequestHandlerInterface $fallbackHandler, private readonly ?EventDispatcherInterface $eventDispatcher = null ) { if ($middlewares === []) { diff --git a/tests/MiddlewareFactoryTest.php b/tests/MiddlewareFactoryTest.php index dd8515a..ba64b68 100644 --- a/tests/MiddlewareFactoryTest.php +++ b/tests/MiddlewareFactoryTest.php @@ -51,23 +51,43 @@ public function testCreateFromRequestHandler(): void self::assertInstanceOf(MiddlewareInterface::class, $middleware); } - public function testCreateFromArray(): void + public function testDebugInfoFromArrayCallable(): void { $container = $this->getContainer([TestController::class => new TestController()]); $middleware = $this ->getMiddlewareFactory($container) ->create([TestController::class, 'index']); + + $response = $middleware + ->process( + $this->createMock(ServerRequestInterface::class), + $this->createMock(RequestHandlerInterface::class) + ); + + self::assertSame('yii', $response->getHeaderLine('test')); self::assertSame( - 'yii', - $middleware - ->process( - $this->createMock(ServerRequestInterface::class), - $this->createMock(RequestHandlerInterface::class) - ) - ->getHeaderLine('test') + [TestController::class, 'index'], + $middleware->__debugInfo()['callback'] ); + } + + public function testDebugInfoFromClosure(): void + { + $container = $this->getContainer([TestController::class => new TestController()]); + $middlewareDefinition = fn() => new Response(headers: ['test' => 'yii']); + $middleware = $this + ->getMiddlewareFactory($container) + ->create($middlewareDefinition); + + $response = $middleware + ->process( + $this->createMock(ServerRequestInterface::class), + $this->createMock(RequestHandlerInterface::class) + ); + + self::assertSame('yii', $response->getHeaderLine('test')); self::assertSame( - [TestController::class, 'index'], + $middlewareDefinition, $middleware->__debugInfo()['callback'] ); } @@ -78,15 +98,12 @@ public function testCreateFromArrayWithResolver(): void $middleware = $this ->getMiddlewareFactory($container, new SimpleParametersResolver()) ->create([TestController::class, 'indexWithParams']); - self::assertSame( - 'yii', - $middleware - ->process( - $this->createMock(ServerRequestInterface::class), - $this->createMock(RequestHandlerInterface::class) - ) - ->getHeaderLine('test') - ); + $response = $middleware + ->process( + $this->createMock(ServerRequestInterface::class), + $this->createMock(RequestHandlerInterface::class) + ); + self::assertSame('yii', $response->getHeaderLine('test')); } public function testCreateFromClosureResponse(): void @@ -97,15 +114,14 @@ public function testCreateFromClosureResponse(): void ->create( static fn(): ResponseInterface => (new Response())->withStatus(418) ); - self::assertSame( - 418, - $middleware - ->process( - $this->createMock(ServerRequestInterface::class), - $this->createMock(RequestHandlerInterface::class) - ) - ->getStatusCode() - ); + + $response = $middleware + ->process( + $this->createMock(ServerRequestInterface::class), + $this->createMock(RequestHandlerInterface::class) + ); + + self::assertSame(418, $response->getStatusCode()); } public function testCreateFromClosureWithResolver(): void @@ -116,15 +132,13 @@ public function testCreateFromClosureWithResolver(): void ->create( static fn(string $test = ''): ResponseInterface => (new Response())->withStatus(418, $test) ); - self::assertSame( - 'yii', - $middleware - ->process( - $this->createMock(ServerRequestInterface::class), - $this->createMock(RequestHandlerInterface::class) - ) - ->getReasonPhrase() - ); + $response = $middleware + ->process( + $this->createMock(ServerRequestInterface::class), + $this->createMock(RequestHandlerInterface::class) + ); + + self::assertSame('yii', $response->getReasonPhrase()); } public function testCreateCallableFromArrayWithInstance(): void @@ -134,19 +148,15 @@ public function testCreateCallableFromArrayWithInstance(): void $middleware = $this ->getMiddlewareFactory($container) ->create([$controller, 'index']); - self::assertSame( - 'yii', - $middleware - ->process( - $this->createMock(ServerRequestInterface::class), - $this->createMock(RequestHandlerInterface::class) - ) - ->getHeaderLine('test') - ); - self::assertSame( - [$controller, 'index'], - $middleware->__debugInfo()['callback'] - ); + + $response = $middleware + ->process( + $this->createMock(ServerRequestInterface::class), + $this->createMock(RequestHandlerInterface::class) + ); + + self::assertSame('yii', $response->getHeaderLine('test')); + self::assertSame([TestController::class, 'index'], $middleware->__debugInfo()['callback']); } public function testCreateCallableObject(): void @@ -155,15 +165,14 @@ public function testCreateCallableObject(): void $middleware = $this ->getMiddlewareFactory($container) ->create(new InvokeableAction()); - self::assertSame( - 'yii', - $middleware - ->process( - $this->createMock(ServerRequestInterface::class), - $this->createMock(RequestHandlerInterface::class) - ) - ->getHeaderLine('test') - ); + + $response = $middleware + ->process( + $this->createMock(ServerRequestInterface::class), + $this->createMock(RequestHandlerInterface::class) + ); + + self::assertSame('yii', $response->getHeaderLine('test')); } public function testCreateFromClosureMiddleware(): void @@ -174,15 +183,12 @@ public function testCreateFromClosureMiddleware(): void ->create( static fn(): MiddlewareInterface => new TestMiddleware() ); - self::assertSame( - '42', - $middleware - ->process( - $this->createMock(ServerRequestInterface::class), - $this->createMock(RequestHandlerInterface::class) - ) - ->getHeaderLine('test') - ); + $response = $middleware + ->process( + $this->createMock(ServerRequestInterface::class), + $this->createMock(RequestHandlerInterface::class) + ); + self::assertSame('42', $response->getHeaderLine('test')); } public function testCreateWithUseParamsMiddleware(): void @@ -233,15 +239,12 @@ public function testCreateWithArrayDefinition(): void ]); self::assertInstanceOf(TestMiddleware::class, $middleware); - self::assertSame( - '7', - $middleware - ->process( - $this->createMock(ServerRequestInterface::class), - $this->createMock(RequestHandlerInterface::class) - ) - ->getHeaderLine('test') - ); + $response = $middleware + ->process( + $this->createMock(ServerRequestInterface::class), + $this->createMock(RequestHandlerInterface::class) + ); + self::assertSame('7', $response->getHeaderLine('test')); } public function testInvalidMiddlewareWithWrongCallable(): void @@ -323,6 +326,53 @@ public function testInvalidMiddlewareWithWrongArrayWithIntItems(): void ->create([7, 42]); } + /** + * @dataProvider dataControllerMiddlewares + */ + public function testControllerMiddleware(): void + { + $container = $this->getContainer([TestController::class => new TestController()]); + $middleware = $this + ->getMiddlewareFactory($container, new SimpleParametersResolver()) + ->create([TestController::class, 'error']); + $response = $middleware->process( + $this->createMock(ServerRequestInterface::class), + $this->createMock(RequestHandlerInterface::class) + ); + + self::assertSame( + 200, + $response->getStatusCode() + ); + } + + public static function dataControllerMiddlewares(): iterable + { + yield 'controller class + action name' => [ + [TestController::class, 'error'], + ]; + yield 'controller object + action name (callable)' => [ + [new TestController(), 'error1'], + ]; + } + + public function testMultipleControllerMiddlewares(): void + { + $container = $this->getContainer([TestController::class => new TestController()]); + $middleware = $this + ->getMiddlewareFactory($container, new SimpleParametersResolver()) + ->create([TestController::class, 'severalMiddlewares']); + $response = $middleware->process( + $this->createMock(ServerRequestInterface::class), + $this->createMock(RequestHandlerInterface::class) + ); + + self::assertSame(404, $response->getStatusCode()); + self::assertSame('yii1', $response->getHeaderLine('x-test1')); + self::assertSame('yii2', $response->getHeaderLine('x-test2')); + self::assertSame('yii3', $response->getHeaderLine('x-test3')); + } + private function getMiddlewareFactory( ContainerInterface $container = null, ParametersResolverInterface $parametersResolver = null diff --git a/tests/Support/ResponseMiddleware.php b/tests/Support/ResponseMiddleware.php new file mode 100644 index 0000000..b0b96f2 --- /dev/null +++ b/tests/Support/ResponseMiddleware.php @@ -0,0 +1,23 @@ +code); + } +} diff --git a/tests/Support/SetHeaderMiddleware.php b/tests/Support/SetHeaderMiddleware.php new file mode 100644 index 0000000..29abf52 --- /dev/null +++ b/tests/Support/SetHeaderMiddleware.php @@ -0,0 +1,26 @@ +handle($request); + + return $response->withHeader($this->header, $this->value); + } +} diff --git a/tests/Support/TestController.php b/tests/Support/TestController.php index 371b446..6024228 100644 --- a/tests/Support/TestController.php +++ b/tests/Support/TestController.php @@ -6,6 +6,7 @@ use Nyholm\Psr7\Response; use Psr\Http\Message\ResponseInterface; +use Yiisoft\Middleware\Dispatcher\Attribute\Middleware; final class TestController { @@ -25,4 +26,30 @@ public function compositeResolver(int $a = 0, int $b = 0, int $c = 0, int $d = 0 reason: $a . '-' . $b . '-' . $c . '-' . $d, ); } + + #[Middleware([ + 'class' => ResponseMiddleware::class, + '__construct()' => [200], + ])] + public function error(): ResponseInterface + { + return new Response(404); + } + + #[Middleware([ + 'class' => SetHeaderMiddleware::class, + '__construct()' => ['x-test1', 'yii1'], + ])] + #[Middleware([ + 'class' => SetHeaderMiddleware::class, + '__construct()' => ['x-test2', 'yii2'], + ])] + #[Middleware([ + 'class' => SetHeaderMiddleware::class, + '__construct()' => ['x-test3', 'yii3'], + ])] + public function severalMiddlewares(): ResponseInterface + { + return new Response(404); + } }