Skip to content

Commit 81db2d4

Browse files
committed
feature symfony#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

.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 (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

EventListener/PsrResponseListener.php

Lines changed: 50 additions & 0 deletions
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 <dunglas@gmail.com>
16+
* @author Alexander M. Turek <me@derrabus.de>
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+
}
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: 44 additions & 0 deletions
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 <dunglas@gmail.com>
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+
}
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: 76 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy