Skip to content

Commit 81db2d4

Browse files
committed
feature #89 PSR HTTP message converters for controllers (derrabus)
This PR was merged into the 2.0-dev branch. Discussion ---------- PSR HTTP message converters for controllers This PR proposes to add two classes to `symfony/psr-http-message-bridge` that have been removed from `sensio/framework-extra-bundle`. By adding `PsrServerRequestResolver` and `PsrResponseListener` to its service container, with autowring and autoconfiguring enabled, an application gains the ability to operate controllers on PSR-7 message objects instead of HttpFoundation. This is especially useful if a developer wants to reuse generic packages like `league/oauth2-server`. Configuration files for autowiring the two classes can be provided as Flex recipes. Commits ------- aa26e61 PSR HTTP message converters for controllers
2 parents e62b239 + aa26e61 commit 81db2d4

File tree

11 files changed

+450
-2
lines changed

11 files changed

+450
-2
lines changed

Diff for: .gitignore

+1
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

Diff for: ArgumentValueResolver/PsrServerRequestResolver.php

+49
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 <[email protected]>
17+
* @author Alexander M. Turek <[email protected]>
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+
}

Diff for: CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
# 2.1.0 (2021-02-17)
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

Diff for: EventListener/PsrResponseListener.php

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
namespace Symfony\Bridge\PsrHttpMessage\EventListener;
4+
5+
use Psr\Http\Message\ResponseInterface;
6+
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
7+
use Symfony\Bridge\PsrHttpMessage\HttpFoundationFactoryInterface;
8+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
9+
use Symfony\Component\HttpKernel\Event\ViewEvent;
10+
use Symfony\Component\HttpKernel\KernelEvents;
11+
12+
/**
13+
* Converts PSR-7 Response to HttpFoundation Response using the bridge.
14+
*
15+
* @author Kévin Dunglas <[email protected]>
16+
* @author Alexander M. Turek <[email protected]>
17+
*/
18+
final class PsrResponseListener implements EventSubscriberInterface
19+
{
20+
private $httpFoundationFactory;
21+
22+
public function __construct(HttpFoundationFactoryInterface $httpFoundationFactory = null)
23+
{
24+
$this->httpFoundationFactory = $httpFoundationFactory ?? new HttpFoundationFactory();
25+
}
26+
27+
/**
28+
* Do the conversion if applicable and update the response of the event.
29+
*/
30+
public function onKernelView(ViewEvent $event): void
31+
{
32+
$controllerResult = $event->getControllerResult();
33+
34+
if (!$controllerResult instanceof ResponseInterface) {
35+
return;
36+
}
37+
38+
$event->setResponse($this->httpFoundationFactory->createResponse($controllerResult));
39+
}
40+
41+
/**
42+
* {@inheritdoc}
43+
*/
44+
public static function getSubscribedEvents(): array
45+
{
46+
return [
47+
KernelEvents::VIEW => 'onKernelView',
48+
];
49+
}
50+
}
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 <[email protected]>
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+
}

Diff for: Tests/EventListener/PsrResponseListenerTest.php

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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\Tests\Fixtures\Response;
8+
use Symfony\Component\HttpFoundation\Request;
9+
use Symfony\Component\HttpKernel\Event\ViewEvent;
10+
use Symfony\Component\HttpKernel\HttpKernelInterface;
11+
12+
/**
13+
* @author Kévin Dunglas <[email protected]>
14+
*/
15+
class PsrResponseListenerTest extends TestCase
16+
{
17+
public function testConvertsControllerResult()
18+
{
19+
$listener = new PsrResponseListener();
20+
$event = $this->createEventMock(new Response());
21+
$listener->onKernelView($event);
22+
23+
self::assertTrue($event->hasResponse());
24+
}
25+
26+
public function testDoesNotConvertControllerResult()
27+
{
28+
$listener = new PsrResponseListener();
29+
$event = $this->createEventMock([]);
30+
31+
$listener->onKernelView($event);
32+
self::assertFalse($event->hasResponse());
33+
34+
$event = $this->createEventMock(null);
35+
36+
$listener->onKernelView($event);
37+
self::assertFalse($event->hasResponse());
38+
}
39+
40+
private function createEventMock($controllerResult): ViewEvent
41+
{
42+
return new ViewEvent($this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::MASTER_REQUEST, $controllerResult);
43+
}
44+
}
+45
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+
}

Diff for: Tests/Fixtures/App/Kernel.php

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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+
'secret' => 'for your eyes only',
52+
'test' => true,
53+
]);
54+
55+
$container->services()
56+
->set('nyholm.psr_factory', Psr17Factory::class)
57+
->alias(ResponseFactoryInterface::class, 'nyholm.psr_factory')
58+
->alias(ServerRequestFactoryInterface::class, 'nyholm.psr_factory')
59+
->alias(StreamFactoryInterface::class, 'nyholm.psr_factory')
60+
->alias(UploadedFileFactoryInterface::class, 'nyholm.psr_factory')
61+
;
62+
63+
$container->services()
64+
->defaults()->autowire()->autoconfigure()
65+
->set(HttpFoundationFactoryInterface::class, HttpFoundationFactory::class)
66+
->set(HttpMessageFactoryInterface::class, PsrHttpFactory::class)
67+
->set(PsrResponseListener::class)
68+
->set(PsrServerRequestResolver::class)
69+
;
70+
71+
$container->services()
72+
->set('logger', NullLogger::class)
73+
->set(PsrRequestController::class)->public()->autowire()
74+
;
75+
}
76+
}

0 commit comments

Comments
 (0)