Skip to content

Commit ab81158

Browse files
rustamwinvjik
andauthored
Add support for invokable class name & request handler (#89)
Co-authored-by: Sergei Predvoditelev <[email protected]>
1 parent 5609007 commit ab81158

6 files changed

+145
-29
lines changed

CHANGELOG.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# Yii Middleware Dispatcher Change Log
22

3-
## 5.1.1 under development
3+
## 5.2.0 under development
44

5-
- no changes in this release.
5+
- Enh #89: Add support for invokable class names & classes that implements `Psr\Http\Server\RequestHandlerInterface` (@rustamwin)
66

77
## 5.1.0 May 11, 2023
88

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ In the above we have used a callback. Overall the following options are availabl
6464
- A controller handler action in format `[TestController::class, 'index']`. `TestController` instance will be created and
6565
`index()` method will be executed.
6666
- A name of PSR-15 middleware class. The middleware instance will be obtained from container.
67+
- A name of PSR-15 request handler class. The request handler instance will be obtained from container and executed.
68+
- A name or instance of invokable class. If the name of invokable class is provided, the instance will be
69+
obtained from container and executed.
6770
- A function returning a middleware such as
6871
```php
6972
static function (): MiddlewareInterface {

src/MiddlewareFactory.php

+32-1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ public function __construct(
4242
* @param array|callable|string $middlewareDefinition Middleware definition in one of the following formats:
4343
*
4444
* - A name of PSR-15 middleware class. The middleware instance will be obtained from container and executed.
45+
* - A name of PSR-15 request handler class. The request handler instance will be obtained from container and executed.
46+
* - A name of invokable class. The invokable class instance will be obtained from container and executed.
4547
* - A callable with
4648
* `function(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface`
4749
* signature.
@@ -64,6 +66,14 @@ public function create(array|callable|string $middlewareDefinition): MiddlewareI
6466
return $this->container->get($middlewareDefinition);
6567
}
6668

69+
if ($this->isRequestHandlerClassDefinition($middlewareDefinition)) {
70+
return $this->wrapCallable([$middlewareDefinition, 'handle']);
71+
}
72+
73+
if ($this->isInvokableClassDefinition($middlewareDefinition)) {
74+
return $this->wrapCallable([$middlewareDefinition, '__invoke']);
75+
}
76+
6777
if ($this->isCallableDefinition($middlewareDefinition)) {
6878
return $this->wrapCallable($middlewareDefinition);
6979
}
@@ -89,6 +99,27 @@ private function isMiddlewareClassDefinition(array|callable|string $definition):
8999
&& is_subclass_of($definition, MiddlewareInterface::class);
90100
}
91101

102+
/**
103+
* @psalm-assert-if-true class-string<RequestHandlerInterface> $definition
104+
*/
105+
private function isRequestHandlerClassDefinition(array|callable|string $definition): bool
106+
{
107+
return is_string($definition)
108+
&& is_subclass_of($definition, RequestHandlerInterface::class);
109+
}
110+
111+
/**
112+
* @psalm-assert-if-true class-string|object $definition
113+
*/
114+
private function isInvokableClassDefinition(array|callable|string $definition): bool
115+
{
116+
/**
117+
* @psalm-suppress ArgumentTypeCoercion `method_exists()` allow use any string as first argument and returns
118+
* false if it not class name.
119+
*/
120+
return is_string($definition) && method_exists($definition, '__invoke');
121+
}
122+
92123
/**
93124
* @psalm-assert-if-true array{0:class-string, 1:non-empty-string}|callable $definition
94125
*/
@@ -124,7 +155,7 @@ private function isArrayDefinition(array|callable|string $definition): bool
124155
return false;
125156
}
126157

127-
return is_subclass_of((string)($definition['class'] ?? ''), MiddlewareInterface::class);
158+
return is_subclass_of((string) ($definition['class'] ?? ''), MiddlewareInterface::class);
128159
}
129160

130161
/**

tests/MiddlewareDispatcherTest.php

+57-14
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
use Yiisoft\Middleware\Dispatcher\MiddlewareDispatcher;
1818
use Yiisoft\Middleware\Dispatcher\MiddlewareFactory;
1919
use Yiisoft\Middleware\Dispatcher\Tests\Support\FailMiddleware;
20+
use Yiisoft\Middleware\Dispatcher\Tests\Support\InvokeableAction;
21+
use Yiisoft\Middleware\Dispatcher\Tests\Support\SimpleRequestHandler;
2022
use Yiisoft\Middleware\Dispatcher\Tests\Support\TestController;
2123
use Yiisoft\Middleware\Dispatcher\Tests\Support\TestMiddleware;
2224
use Yiisoft\Test\Support\Container\SimpleContainer;
@@ -31,14 +33,48 @@ public function testCallableMiddlewareCalled(): void
3133
$dispatcher = $this
3234
->createDispatcher()
3335
->withMiddlewares([
34-
static fn (): ResponseInterface => new Response(418),
36+
static fn(): ResponseInterface => new Response(418),
3537
]);
3638

3739
$response = $dispatcher->dispatch($request, $this->getRequestHandler());
3840
$this->assertSame(418, $response->getStatusCode());
3941
}
4042

41-
public function testArrayMiddlewareCall(): void
43+
public function testInvokableMiddlewareCalled(): void
44+
{
45+
$request = new ServerRequest('GET', '/');
46+
$container = $this->createContainer([InvokeableAction::class => new InvokeableAction()]);
47+
48+
$dispatcher = $this
49+
->createDispatcher($container)
50+
->withMiddlewares([
51+
InvokeableAction::class,
52+
]);
53+
54+
$response = $dispatcher->dispatch($request, $this->getRequestHandler());
55+
$this->assertSame(200, $response->getStatusCode());
56+
}
57+
58+
public function testRequestHandlerCalled(): void
59+
{
60+
$request = new ServerRequest('GET', '/');
61+
$container = $this->createContainer(
62+
[
63+
SimpleRequestHandler::class => new SimpleRequestHandler(),
64+
]
65+
);
66+
67+
$dispatcher = $this
68+
->createDispatcher($container)
69+
->withMiddlewares([
70+
SimpleRequestHandler::class,
71+
]);
72+
73+
$response = $dispatcher->dispatch($request, $this->getRequestHandler());
74+
$this->assertSame(200, $response->getStatusCode());
75+
}
76+
77+
public function testArrayMiddlewareCalled(): void
4278
{
4379
$request = new ServerRequest('GET', '/');
4480
$container = $this->createContainer([
@@ -60,7 +96,13 @@ public function testMiddlewareFullStackCalled(): void
6096
$request = $request->withAttribute('middleware', 'middleware1');
6197
return $handler->handle($request);
6298
};
63-
$middleware2 = static fn (ServerRequestInterface $request) => new Response(200, [], null, '1.1', implode($request->getAttributes()));
99+
$middleware2 = static fn(ServerRequestInterface $request) => new Response(
100+
200,
101+
[],
102+
null,
103+
'1.1',
104+
implode($request->getAttributes())
105+
);
64106

65107
$dispatcher = $this
66108
->createDispatcher()
@@ -75,8 +117,8 @@ public function testMiddlewareStackInterrupted(): void
75117
{
76118
$request = new ServerRequest('GET', '/');
77119

78-
$middleware1 = static fn () => new Response(403);
79-
$middleware2 = static fn () => new Response(200);
120+
$middleware1 = static fn() => new Response(403);
121+
$middleware2 = static fn() => new Response(200);
80122

81123
$dispatcher = $this
82124
->createDispatcher()
@@ -92,8 +134,10 @@ public function testEventsAreDispatched(): void
92134

93135
$request = new ServerRequest('GET', '/');
94136

95-
$middleware1 = static fn (ServerRequestInterface $request, RequestHandlerInterface $handler) => $handler->handle($request);
96-
$middleware2 = static fn () => new Response();
137+
$middleware1 = static fn(ServerRequestInterface $request, RequestHandlerInterface $handler) => $handler->handle(
138+
$request
139+
);
140+
$middleware2 = static fn() => new Response();
97141

98142
$dispatcher = $this
99143
->createDispatcher(null, $eventDispatcher)
@@ -118,7 +162,7 @@ public function testEventsAreDispatchedWhenMiddlewareFailedWithException(): void
118162

119163
$request = new ServerRequest('GET', '/');
120164
$eventDispatcher = new SimpleEventDispatcher();
121-
$middleware = fn () => new FailMiddleware();
165+
$middleware = fn() => new FailMiddleware();
122166
$dispatcher = $this
123167
->createDispatcher(null, $eventDispatcher)
124168
->withMiddlewares([$middleware]);
@@ -151,10 +195,7 @@ public function testHasMiddlewares(array $definitions, bool $expected): void
151195
{
152196
self::assertSame(
153197
$expected,
154-
$this
155-
->createDispatcher()
156-
->withMiddlewares($definitions)
157-
->hasMiddlewares()
198+
$this->createDispatcher()->withMiddlewares($definitions)->hasMiddlewares()
158199
);
159200
}
160201

@@ -194,8 +235,10 @@ public function handle(ServerRequestInterface $request): ResponseInterface
194235
};
195236
}
196237

197-
private function createDispatcher(ContainerInterface $container = null, ?EventDispatcherInterface $eventDispatcher = null): MiddlewareDispatcher
198-
{
238+
private function createDispatcher(
239+
ContainerInterface $container = null,
240+
?EventDispatcherInterface $eventDispatcher = null
241+
): MiddlewareDispatcher {
199242
if ($container === null) {
200243
$container = $this->createContainer();
201244
}

tests/MiddlewareFactoryTest.php

+30-12
Original file line numberDiff line numberDiff line change
@@ -15,26 +15,42 @@
1515
use Yiisoft\Middleware\Dispatcher\InvalidMiddlewareDefinitionException;
1616
use Yiisoft\Middleware\Dispatcher\MiddlewareFactory;
1717
use Yiisoft\Middleware\Dispatcher\ParametersResolverInterface;
18+
use Yiisoft\Middleware\Dispatcher\Tests\Support\InvalidController;
1819
use Yiisoft\Middleware\Dispatcher\Tests\Support\InvokeableAction;
1920
use Yiisoft\Middleware\Dispatcher\Tests\Support\ParametersResolver\SimpleParametersResolver;
20-
use Yiisoft\Middleware\Dispatcher\Tests\Support\UseParamsController;
21-
use Yiisoft\Middleware\Dispatcher\Tests\Support\UseParamsMiddleware;
22-
use Yiisoft\Middleware\Dispatcher\Tests\Support\InvalidController;
21+
use Yiisoft\Middleware\Dispatcher\Tests\Support\SimpleRequestHandler;
2322
use Yiisoft\Middleware\Dispatcher\Tests\Support\TestController;
2423
use Yiisoft\Middleware\Dispatcher\Tests\Support\TestMiddleware;
24+
use Yiisoft\Middleware\Dispatcher\Tests\Support\UseParamsController;
25+
use Yiisoft\Middleware\Dispatcher\Tests\Support\UseParamsMiddleware;
2526
use Yiisoft\Test\Support\Container\SimpleContainer;
2627

2728
final class MiddlewareFactoryTest extends TestCase
2829
{
2930
public function testCreateFromString(): void
3031
{
3132
$container = $this->getContainer([TestMiddleware::class => new TestMiddleware()]);
32-
$middleware = $this
33-
->getMiddlewareFactory($container)
34-
->create(TestMiddleware::class);
33+
$middleware = $this->getMiddlewareFactory($container)->create(TestMiddleware::class);
34+
3535
self::assertInstanceOf(TestMiddleware::class, $middleware);
3636
}
3737

38+
public function testCreateFromInvokable(): void
39+
{
40+
$container = $this->getContainer([InvokeableAction::class => new InvokeableAction()]);
41+
$middleware = $this->getMiddlewareFactory($container)->create(InvokeableAction::class);
42+
43+
self::assertInstanceOf(MiddlewareInterface::class, $middleware);
44+
}
45+
46+
public function testCreateFromRequestHandler(): void
47+
{
48+
$container = $this->getContainer([SimpleRequestHandler::class => new SimpleRequestHandler()]);
49+
$middleware = $this->getMiddlewareFactory($container)->create(SimpleRequestHandler::class);
50+
51+
self::assertInstanceOf(MiddlewareInterface::class, $middleware);
52+
}
53+
3854
public function testCreateFromArray(): void
3955
{
4056
$container = $this->getContainer([TestController::class => new TestController()]);
@@ -79,7 +95,7 @@ public function testCreateFromClosureResponse(): void
7995
$middleware = $this
8096
->getMiddlewareFactory($container)
8197
->create(
82-
static fn (): ResponseInterface => (new Response())->withStatus(418)
98+
static fn(): ResponseInterface => (new Response())->withStatus(418)
8399
);
84100
self::assertSame(
85101
418,
@@ -98,7 +114,7 @@ public function testCreateFromClosureWithResolver(): void
98114
$middleware = $this
99115
->getMiddlewareFactory($container, new SimpleParametersResolver())
100116
->create(
101-
static fn (string $test = ''): ResponseInterface => (new Response())->withStatus(418, $test)
117+
static fn(string $test = ''): ResponseInterface => (new Response())->withStatus(418, $test)
102118
);
103119
self::assertSame(
104120
'yii',
@@ -156,7 +172,7 @@ public function testCreateFromClosureMiddleware(): void
156172
$middleware = $this
157173
->getMiddlewareFactory($container)
158174
->create(
159-
static fn (): MiddlewareInterface => new TestMiddleware()
175+
static fn(): MiddlewareInterface => new TestMiddleware()
160176
);
161177
self::assertSame(
162178
'42',
@@ -234,7 +250,7 @@ public function testInvalidMiddlewareWithWrongCallable(): void
234250
$middleware = $this
235251
->getMiddlewareFactory($container)
236252
->create(
237-
static fn () => 42
253+
static fn() => 42
238254
);
239255

240256
$this->expectException(InvalidMiddlewareDefinitionException::class);
@@ -307,8 +323,10 @@ public function testInvalidMiddlewareWithWrongArrayWithIntItems(): void
307323
->create([7, 42]);
308324
}
309325

310-
private function getMiddlewareFactory(ContainerInterface $container = null, ParametersResolverInterface $parametersResolver = null): MiddlewareFactory
311-
{
326+
private function getMiddlewareFactory(
327+
ContainerInterface $container = null,
328+
ParametersResolverInterface $parametersResolver = null
329+
): MiddlewareFactory {
312330
if ($container !== null) {
313331
return new MiddlewareFactory($container, $parametersResolver);
314332
}
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\Middleware\Dispatcher\Tests\Support;
6+
7+
use Nyholm\Psr7\Response;
8+
use Psr\Http\Message\ResponseInterface;
9+
use Psr\Http\Message\ServerRequestInterface;
10+
use Psr\Http\Server\RequestHandlerInterface;
11+
12+
class SimpleRequestHandler implements RequestHandlerInterface
13+
{
14+
/**
15+
* @inheritDoc
16+
*/
17+
public function handle(ServerRequestInterface $request): ResponseInterface
18+
{
19+
return new Response(200);
20+
}
21+
}

0 commit comments

Comments
 (0)