Skip to content

Commit 2cf933e

Browse files
committed
PSR HTTP message converters for controllers
1 parent c62f7d0 commit 2cf933e

File tree

11 files changed

+449
-2
lines changed

11 files changed

+449
-2
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ composer.lock
33
phpunit.xml
44
.php_cs.cache
55
.phpunit.result.cache
6+
/Tests/Fixtures/App/var
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
namespace Symfony\Bridge\PsrHttpMessage\ArgumentValueResolver;
4+
5+
use Psr\Http\Message\MessageInterface;
6+
use Psr\Http\Message\RequestInterface;
7+
use Psr\Http\Message\ServerRequestInterface;
8+
use Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface;
9+
use Symfony\Component\HttpFoundation\Request;
10+
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
11+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
12+
13+
/**
14+
* Injects the RequestInterface, MessageInterface or ServerRequestInterface when requested.
15+
*
16+
* @author Iltar van der Berg <kjarli@gmail.com>
17+
* @author Alexander M. Turek <me@derrabus.de>
18+
*/
19+
final class PsrServerRequestResolver implements ArgumentValueResolverInterface
20+
{
21+
private const SUPPORTED_TYPES = [
22+
ServerRequestInterface::class => true,
23+
RequestInterface::class => true,
24+
MessageInterface::class => true,
25+
];
26+
27+
private $httpMessageFactory;
28+
29+
public function __construct(HttpMessageFactoryInterface $httpMessageFactory)
30+
{
31+
$this->httpMessageFactory = $httpMessageFactory;
32+
}
33+
34+
/**
35+
* {@inheritdoc}
36+
*/
37+
public function supports(Request $request, ArgumentMetadata $argument): bool
38+
{
39+
return self::SUPPORTED_TYPES[$argument->getType()] ?? false;
40+
}
41+
42+
/**
43+
* {@inheritdoc}
44+
*/
45+
public function resolve(Request $request, ArgumentMetadata $argument): \Traversable
46+
{
47+
yield $this->httpMessageFactory->createRequest($request);
48+
}
49+
}

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
# 2.1.0 (TBD)
5+
6+
* Added a `PsrResponseListener` to automatically convert PSR-7 responses returned by controllers
7+
* Added a `PsrServerRequestResolver` that allows injecting PSR-7 request objects into controllers
8+
49
# 2.0.2 (2020-09-29)
510

611
* Fix populating server params from URI in HttpFoundationFactory

EventListener/PsrResponseListener.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
namespace Symfony\Bridge\PsrHttpMessage\EventListener;
4+
5+
use Psr\Http\Message\ResponseInterface;
6+
use Symfony\Bridge\PsrHttpMessage\HttpFoundationFactoryInterface;
7+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
8+
use Symfony\Component\HttpKernel\Event\ViewEvent;
9+
use Symfony\Component\HttpKernel\KernelEvents;
10+
11+
/**
12+
* Converts PSR-7 Response to HttpFoundation Response using the bridge.
13+
*
14+
* @author Kévin Dunglas <dunglas@gmail.com>
15+
* @author Alexander M. Turek <me@derrabus.de>
16+
*/
17+
final class PsrResponseListener implements EventSubscriberInterface
18+
{
19+
private $httpFoundationFactory;
20+
21+
public function __construct(HttpFoundationFactoryInterface $httpFoundationFactory)
22+
{
23+
$this->httpFoundationFactory = $httpFoundationFactory;
24+
}
25+
26+
/**
27+
* Do the conversion if applicable and update the response of the event.
28+
*/
29+
public function onKernelView(ViewEvent $event): void
30+
{
31+
$controllerResult = $event->getControllerResult();
32+
33+
if (!$controllerResult instanceof ResponseInterface) {
34+
return;
35+
}
36+
37+
$event->setResponse($this->httpFoundationFactory->createResponse($controllerResult));
38+
}
39+
40+
/**
41+
* {@inheritdoc}
42+
*/
43+
public static function getSubscribedEvents(): array
44+
{
45+
return [
46+
KernelEvents::VIEW => 'onKernelView',
47+
];
48+
}
49+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
namespace Symfony\Bridge\PsrHttpMessage\Tests\ArgumentValueResolver;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use Psr\Http\Message\MessageInterface;
7+
use Psr\Http\Message\RequestInterface;
8+
use Psr\Http\Message\ServerRequestInterface;
9+
use Symfony\Bridge\PsrHttpMessage\ArgumentValueResolver\PsrServerRequestResolver;
10+
use Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface;
11+
use Symfony\Component\HttpFoundation\Request;
12+
use Symfony\Component\HttpKernel\Controller\ArgumentResolver;
13+
14+
/**
15+
* @author Alexander M. Turek <me@derrabus.de>
16+
*/
17+
final class PsrServerRequestResolverTest extends TestCase
18+
{
19+
public function testServerRequest()
20+
{
21+
$symfonyRequest = $this->createMock(Request::class);
22+
$psrRequest = $this->createMock(ServerRequestInterface::class);
23+
24+
$resolver = $this->bootstrapResolver($symfonyRequest, $psrRequest);
25+
26+
self::assertSame([$psrRequest], $resolver->getArguments($symfonyRequest, static function (ServerRequestInterface $serverRequest): void {}));
27+
}
28+
29+
public function testRequest()
30+
{
31+
$symfonyRequest = $this->createMock(Request::class);
32+
$psrRequest = $this->createMock(ServerRequestInterface::class);
33+
34+
$resolver = $this->bootstrapResolver($symfonyRequest, $psrRequest);
35+
36+
self::assertSame([$psrRequest], $resolver->getArguments($symfonyRequest, static function (RequestInterface $request): void {}));
37+
}
38+
39+
public function testMessage()
40+
{
41+
$symfonyRequest = $this->createMock(Request::class);
42+
$psrRequest = $this->createMock(ServerRequestInterface::class);
43+
44+
$resolver = $this->bootstrapResolver($symfonyRequest, $psrRequest);
45+
46+
self::assertSame([$psrRequest], $resolver->getArguments($symfonyRequest, static function (MessageInterface $request): void {}));
47+
}
48+
49+
private function bootstrapResolver(Request $symfonyRequest, ServerRequestInterface $psrRequest): ArgumentResolver
50+
{
51+
$messageFactory = $this->createMock(HttpMessageFactoryInterface::class);
52+
$messageFactory->expects(self::once())
53+
->method('createRequest')
54+
->with(self::identicalTo($symfonyRequest))
55+
->willReturn($psrRequest);
56+
57+
return new ArgumentResolver(null, [new PsrServerRequestResolver($messageFactory)]);
58+
}
59+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace Symfony\Bridge\PsrHttpMessage\Tests\EventListener;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use Symfony\Bridge\PsrHttpMessage\EventListener\PsrResponseListener;
7+
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
8+
use Symfony\Bridge\PsrHttpMessage\Tests\Fixtures\Response;
9+
use Symfony\Component\HttpFoundation\Request;
10+
use Symfony\Component\HttpKernel\Event\ViewEvent;
11+
use Symfony\Component\HttpKernel\HttpKernelInterface;
12+
13+
/**
14+
* @author Kévin Dunglas <dunglas@gmail.com>
15+
*/
16+
class PsrResponseListenerTest extends TestCase
17+
{
18+
public function testConvertsControllerResult()
19+
{
20+
$listener = new PsrResponseListener(new HttpFoundationFactory());
21+
$event = $this->createEventMock(new Response());
22+
$listener->onKernelView($event);
23+
24+
self::assertTrue($event->hasResponse());
25+
}
26+
27+
public function testDoesNotConvertControllerResult()
28+
{
29+
$listener = new PsrResponseListener(new HttpFoundationFactory());
30+
$event = $this->createEventMock([]);
31+
32+
$listener->onKernelView($event);
33+
self::assertFalse($event->hasResponse());
34+
35+
$event = $this->createEventMock(null);
36+
37+
$listener->onKernelView($event);
38+
self::assertFalse($event->hasResponse());
39+
}
40+
41+
private function createEventMock($controllerResult): ViewEvent
42+
{
43+
return new ViewEvent($this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::MASTER_REQUEST, $controllerResult);
44+
}
45+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace Symfony\Bridge\PsrHttpMessage\Tests\Fixtures\App\Controller;
4+
5+
use Psr\Http\Message\MessageInterface;
6+
use Psr\Http\Message\RequestInterface;
7+
use Psr\Http\Message\ResponseFactoryInterface;
8+
use Psr\Http\Message\ResponseInterface;
9+
use Psr\Http\Message\ServerRequestInterface;
10+
use Psr\Http\Message\StreamFactoryInterface;
11+
12+
final class PsrRequestController
13+
{
14+
private $responseFactory;
15+
private $streamFactory;
16+
17+
public function __construct(ResponseFactoryInterface $responseFactory, StreamFactoryInterface $streamFactory)
18+
{
19+
$this->responseFactory = $responseFactory;
20+
$this->streamFactory = $streamFactory;
21+
}
22+
23+
public function serverRequestAction(ServerRequestInterface $request): ResponseInterface
24+
{
25+
return $this->responseFactory
26+
->createResponse()
27+
->withBody($this->streamFactory->createStream(sprintf('<html><body>%s</body></html>', $request->getMethod())));
28+
}
29+
30+
public function requestAction(RequestInterface $request): ResponseInterface
31+
{
32+
return $this->responseFactory
33+
->createResponse()
34+
->withStatus(403)
35+
->withBody($this->streamFactory->createStream(sprintf('<html><body>%s %s</body></html>', $request->getMethod(), $request->getBody()->getContents())));
36+
}
37+
38+
public function messageAction(MessageInterface $request): ResponseInterface
39+
{
40+
return $this->responseFactory
41+
->createResponse()
42+
->withStatus(422)
43+
->withBody($this->streamFactory->createStream(sprintf('<html><body>%s</body></html>', $request->getHeader('X-My-Header')[0])));
44+
}
45+
}

Tests/Fixtures/App/Kernel.php

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
namespace Symfony\Bridge\PsrHttpMessage\Tests\Fixtures\App;
4+
5+
use Nyholm\Psr7\Factory\Psr17Factory;
6+
use Psr\Http\Message\ResponseFactoryInterface;
7+
use Psr\Http\Message\ServerRequestFactoryInterface;
8+
use Psr\Http\Message\StreamFactoryInterface;
9+
use Psr\Http\Message\UploadedFileFactoryInterface;
10+
use Psr\Log\NullLogger;
11+
use Symfony\Bridge\PsrHttpMessage\ArgumentValueResolver\PsrServerRequestResolver;
12+
use Symfony\Bridge\PsrHttpMessage\EventListener\PsrResponseListener;
13+
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
14+
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
15+
use Symfony\Bridge\PsrHttpMessage\HttpFoundationFactoryInterface;
16+
use Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface;
17+
use Symfony\Bridge\PsrHttpMessage\Tests\Fixtures\App\Controller\PsrRequestController;
18+
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
19+
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
20+
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
21+
use Symfony\Component\HttpKernel\Kernel as SymfonyKernel;
22+
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
23+
24+
class Kernel extends SymfonyKernel
25+
{
26+
use MicroKernelTrait;
27+
28+
public function registerBundles(): iterable
29+
{
30+
yield new FrameworkBundle();
31+
}
32+
33+
public function getProjectDir(): string
34+
{
35+
return __DIR__;
36+
}
37+
38+
protected function configureRoutes(RoutingConfigurator $routes): void
39+
{
40+
$routes
41+
->add('server_request', '/server-request')->controller([PsrRequestController::class, 'serverRequestAction'])->methods(['GET'])
42+
->add('request', '/request')->controller([PsrRequestController::class, 'requestAction'])->methods(['POST'])
43+
->add('message', '/message')->controller([PsrRequestController::class, 'messageAction'])->methods(['PUT'])
44+
;
45+
}
46+
47+
protected function configureContainer(ContainerConfigurator $container): void
48+
{
49+
$container->extension('framework', [
50+
'router' => ['utf8' => true],
51+
'test' => true,
52+
]);
53+
54+
$container->services()
55+
->set('nyholm.psr_factory', Psr17Factory::class)
56+
->alias(ResponseFactoryInterface::class, 'nyholm.psr_factory')
57+
->alias(ServerRequestFactoryInterface::class, 'nyholm.psr_factory')
58+
->alias(StreamFactoryInterface::class, 'nyholm.psr_factory')
59+
->alias(UploadedFileFactoryInterface::class, 'nyholm.psr_factory')
60+
;
61+
62+
$container->services()
63+
->defaults()->autowire()->autoconfigure()
64+
->set(HttpFoundationFactoryInterface::class, HttpFoundationFactory::class)
65+
->set(HttpMessageFactoryInterface::class, PsrHttpFactory::class)
66+
->set(PsrResponseListener::class)
67+
->set(PsrServerRequestResolver::class)
68+
;
69+
70+
$container->services()
71+
->set('logger', NullLogger::class)
72+
->set(PsrRequestController::class)->public()->autowire()
73+
;
74+
}
75+
}

0 commit comments

Comments
 (0)