Skip to content

PSR HTTP message converters for controllers #89

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ composer.lock
phpunit.xml
.php_cs.cache
.phpunit.result.cache
/Tests/Fixtures/App/var
49 changes: 49 additions & 0 deletions ArgumentValueResolver/PsrServerRequestResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

namespace Symfony\Bridge\PsrHttpMessage\ArgumentValueResolver;

use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;

/**
* Injects the RequestInterface, MessageInterface or ServerRequestInterface when requested.
*
* @author Iltar van der Berg <kjarli@gmail.com>
* @author Alexander M. Turek <me@derrabus.de>
*/
final class PsrServerRequestResolver implements ArgumentValueResolverInterface
{
private const SUPPORTED_TYPES = [
ServerRequestInterface::class => true,
RequestInterface::class => true,
MessageInterface::class => true,
];

private $httpMessageFactory;

public function __construct(HttpMessageFactoryInterface $httpMessageFactory)
{
$this->httpMessageFactory = $httpMessageFactory;
}

/**
* {@inheritdoc}
*/
public function supports(Request $request, ArgumentMetadata $argument): bool
{
return self::SUPPORTED_TYPES[$argument->getType()] ?? false;
}

/**
* {@inheritdoc}
*/
public function resolve(Request $request, ArgumentMetadata $argument): \Traversable
{
yield $this->httpMessageFactory->createRequest($request);
}
}
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

# 2.1.0 (2021-02-17)

* Added a `PsrResponseListener` to automatically convert PSR-7 responses returned by controllers
* Added a `PsrServerRequestResolver` that allows injecting PSR-7 request objects into controllers

# 2.0.2 (2020-09-29)

* Fix populating server params from URI in HttpFoundationFactory
Expand Down
50 changes: 50 additions & 0 deletions EventListener/PsrResponseListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace Symfony\Bridge\PsrHttpMessage\EventListener;

use Psr\Http\Message\ResponseInterface;
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
use Symfony\Bridge\PsrHttpMessage\HttpFoundationFactoryInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ViewEvent;
use Symfony\Component\HttpKernel\KernelEvents;

/**
* Converts PSR-7 Response to HttpFoundation Response using the bridge.
*
* @author Kévin Dunglas <dunglas@gmail.com>
* @author Alexander M. Turek <me@derrabus.de>
*/
final class PsrResponseListener implements EventSubscriberInterface
{
private $httpFoundationFactory;

public function __construct(HttpFoundationFactoryInterface $httpFoundationFactory = null)
{
$this->httpFoundationFactory = $httpFoundationFactory ?? new HttpFoundationFactory();
}

/**
* Do the conversion if applicable and update the response of the event.
*/
public function onKernelView(ViewEvent $event): void
{
$controllerResult = $event->getControllerResult();

if (!$controllerResult instanceof ResponseInterface) {
return;
}

$event->setResponse($this->httpFoundationFactory->createResponse($controllerResult));
}

/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array
{
return [
KernelEvents::VIEW => 'onKernelView',
];
}
}
59 changes: 59 additions & 0 deletions Tests/ArgumentValueResolver/PsrServerRequestResolverTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

namespace Symfony\Bridge\PsrHttpMessage\Tests\ArgumentValueResolver;

use PHPUnit\Framework\TestCase;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Bridge\PsrHttpMessage\ArgumentValueResolver\PsrServerRequestResolver;
use Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver;

/**
* @author Alexander M. Turek <me@derrabus.de>
*/
final class PsrServerRequestResolverTest extends TestCase
{
public function testServerRequest()
{
$symfonyRequest = $this->createMock(Request::class);
$psrRequest = $this->createMock(ServerRequestInterface::class);

$resolver = $this->bootstrapResolver($symfonyRequest, $psrRequest);

self::assertSame([$psrRequest], $resolver->getArguments($symfonyRequest, static function (ServerRequestInterface $serverRequest): void {}));
}

public function testRequest()
{
$symfonyRequest = $this->createMock(Request::class);
$psrRequest = $this->createMock(ServerRequestInterface::class);

$resolver = $this->bootstrapResolver($symfonyRequest, $psrRequest);

self::assertSame([$psrRequest], $resolver->getArguments($symfonyRequest, static function (RequestInterface $request): void {}));
}

public function testMessage()
{
$symfonyRequest = $this->createMock(Request::class);
$psrRequest = $this->createMock(ServerRequestInterface::class);

$resolver = $this->bootstrapResolver($symfonyRequest, $psrRequest);

self::assertSame([$psrRequest], $resolver->getArguments($symfonyRequest, static function (MessageInterface $request): void {}));
}

private function bootstrapResolver(Request $symfonyRequest, ServerRequestInterface $psrRequest): ArgumentResolver
{
$messageFactory = $this->createMock(HttpMessageFactoryInterface::class);
$messageFactory->expects(self::once())
->method('createRequest')
->with(self::identicalTo($symfonyRequest))
->willReturn($psrRequest);

return new ArgumentResolver(null, [new PsrServerRequestResolver($messageFactory)]);
}
}
44 changes: 44 additions & 0 deletions Tests/EventListener/PsrResponseListenerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

namespace Symfony\Bridge\PsrHttpMessage\Tests\EventListener;

use PHPUnit\Framework\TestCase;
use Symfony\Bridge\PsrHttpMessage\EventListener\PsrResponseListener;
use Symfony\Bridge\PsrHttpMessage\Tests\Fixtures\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\ViewEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;

/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class PsrResponseListenerTest extends TestCase
{
public function testConvertsControllerResult()
{
$listener = new PsrResponseListener();
$event = $this->createEventMock(new Response());
$listener->onKernelView($event);

self::assertTrue($event->hasResponse());
}

public function testDoesNotConvertControllerResult()
{
$listener = new PsrResponseListener();
$event = $this->createEventMock([]);

$listener->onKernelView($event);
self::assertFalse($event->hasResponse());

$event = $this->createEventMock(null);

$listener->onKernelView($event);
self::assertFalse($event->hasResponse());
}

private function createEventMock($controllerResult): ViewEvent
{
return new ViewEvent($this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::MASTER_REQUEST, $controllerResult);
}
}
45 changes: 45 additions & 0 deletions Tests/Fixtures/App/Controller/PsrRequestController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace Symfony\Bridge\PsrHttpMessage\Tests\Fixtures\App\Controller;

use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamFactoryInterface;

final class PsrRequestController
{
private $responseFactory;
private $streamFactory;

public function __construct(ResponseFactoryInterface $responseFactory, StreamFactoryInterface $streamFactory)
{
$this->responseFactory = $responseFactory;
$this->streamFactory = $streamFactory;
}

public function serverRequestAction(ServerRequestInterface $request): ResponseInterface
{
return $this->responseFactory
->createResponse()
->withBody($this->streamFactory->createStream(sprintf('<html><body>%s</body></html>', $request->getMethod())));
}

public function requestAction(RequestInterface $request): ResponseInterface
{
return $this->responseFactory
->createResponse()
->withStatus(403)
->withBody($this->streamFactory->createStream(sprintf('<html><body>%s %s</body></html>', $request->getMethod(), $request->getBody()->getContents())));
}

public function messageAction(MessageInterface $request): ResponseInterface
{
return $this->responseFactory
->createResponse()
->withStatus(422)
->withBody($this->streamFactory->createStream(sprintf('<html><body>%s</body></html>', $request->getHeader('X-My-Header')[0])));
}
}
76 changes: 76 additions & 0 deletions Tests/Fixtures/App/Kernel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

namespace Symfony\Bridge\PsrHttpMessage\Tests\Fixtures\App;

use Nyholm\Psr7\Factory\Psr17Factory;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ServerRequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\UploadedFileFactoryInterface;
use Psr\Log\NullLogger;
use Symfony\Bridge\PsrHttpMessage\ArgumentValueResolver\PsrServerRequestResolver;
use Symfony\Bridge\PsrHttpMessage\EventListener\PsrResponseListener;
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
use Symfony\Bridge\PsrHttpMessage\HttpFoundationFactoryInterface;
use Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface;
use Symfony\Bridge\PsrHttpMessage\Tests\Fixtures\App\Controller\PsrRequestController;
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\HttpKernel\Kernel as SymfonyKernel;
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;

class Kernel extends SymfonyKernel
{
use MicroKernelTrait;

public function registerBundles(): iterable
{
yield new FrameworkBundle();
}

public function getProjectDir(): string
{
return __DIR__;
}

protected function configureRoutes(RoutingConfigurator $routes): void
{
$routes
->add('server_request', '/server-request')->controller([PsrRequestController::class, 'serverRequestAction'])->methods(['GET'])
->add('request', '/request')->controller([PsrRequestController::class, 'requestAction'])->methods(['POST'])
->add('message', '/message')->controller([PsrRequestController::class, 'messageAction'])->methods(['PUT'])
;
}

protected function configureContainer(ContainerConfigurator $container): void
{
$container->extension('framework', [
'router' => ['utf8' => true],
'secret' => 'for your eyes only',
'test' => true,
]);

$container->services()
->set('nyholm.psr_factory', Psr17Factory::class)
->alias(ResponseFactoryInterface::class, 'nyholm.psr_factory')
->alias(ServerRequestFactoryInterface::class, 'nyholm.psr_factory')
->alias(StreamFactoryInterface::class, 'nyholm.psr_factory')
->alias(UploadedFileFactoryInterface::class, 'nyholm.psr_factory')
;
Comment on lines +55 to +61
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 The recipe for nyholm/psr7 provides this piece of configuration already.


$container->services()
->defaults()->autowire()->autoconfigure()
->set(HttpFoundationFactoryInterface::class, HttpFoundationFactory::class)
->set(HttpMessageFactoryInterface::class, PsrHttpFactory::class)
->set(PsrResponseListener::class)
->set(PsrServerRequestResolver::class)
;
Comment on lines +63 to +69
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 A new recipe for psr-http-message-bridge should provide this piece of configuration.


$container->services()
->set('logger', NullLogger::class)
->set(PsrRequestController::class)->public()->autowire()
;
}
}
Loading