Skip to content

Commit

Permalink
PSR HTTP message converters for controllers
Browse files Browse the repository at this point in the history
  • Loading branch information
derrabus committed Feb 17, 2021
1 parent e62b239 commit aa26e61
Show file tree
Hide file tree
Showing 11 changed files with 450 additions and 2 deletions.
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 <[email protected]>
* @author Alexander M. Turek <[email protected]>
*/
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 <[email protected]>
* @author Alexander M. Turek <[email protected]>
*/
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 <[email protected]>
*/
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 <[email protected]>
*/
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')
;

$container->services()
->defaults()->autowire()->autoconfigure()
->set(HttpFoundationFactoryInterface::class, HttpFoundationFactory::class)
->set(HttpMessageFactoryInterface::class, PsrHttpFactory::class)
->set(PsrResponseListener::class)
->set(PsrServerRequestResolver::class)
;

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

0 comments on commit aa26e61

Please sign in to comment.