Skip to content

Commit

Permalink
Merge pull request #50 from hiqsol/allow-raw-formatter
Browse files Browse the repository at this point in the history
Change `Formatter` interface to allow returning complete PSR 7 response
  • Loading branch information
lcobucci authored Jul 2, 2020
2 parents 93ec247 + 90b7f26 commit 1c1e0b2
Show file tree
Hide file tree
Showing 20 changed files with 183 additions and 87 deletions.
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ composer require lcobucci/content-negotiation-middleware middlewares/negotiation
### Adventure mode

If you're ready for an adventure and don't want to use `middlewares/negotiation`
to handle the detection or `zendframework/zend-diactoros` to create the response
to handle the detection or `laminas/diactoros` to create the response
body (`StreamInterface` implementation), don't despair! You'll only have to use
the normal `ContentTypeMiddleware::__construct()` instead of
`ContentTypeMiddleware::fromRecommendedSettings()`.
Expand All @@ -53,7 +53,7 @@ The nice thing about `assert()` is that we can (and should) disable it in produc
so that we don't have useless statements.

So, for production mode, we recommend you to set `zend.assertions` to `-1` in your `php.ini`.
For development you should leave `zend.assertions` as `1` and set `assert.exception` to `1`, which
For development, you should leave `zend.assertions` as `1` and set `assert.exception` to `1`, which
will make PHP throw an [`AssertionError`](https://secure.php.net/manual/en/class.assertionerror.php)
when things go wrong.

Expand All @@ -70,7 +70,7 @@ declare(strict_types=1);
use Lcobucci\ContentNegotiation\ContentTypeMiddleware;
use Lcobucci\ContentNegotiation\Formatter\Json;
use Lcobucci\ContentNegotiation\Formatter\StringCast;
use Zend\Diactoros\StreamFactory;
use Laminas\Diactoros\StreamFactory;

$middleware = ContentTypeMiddleware::fromRecommendedSettings(
// First argument is the list of formats you want to support:
Expand Down Expand Up @@ -130,7 +130,7 @@ use Lcobucci\ContentNegotiation\UnformattedResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Zend\Diactoros\Response;
use Laminas\Diactoros\Response;

final class MyHandler implements RequestHandlerInterface
{
Expand Down Expand Up @@ -167,15 +167,17 @@ declare(strict_types=1);
namespace Me\MyApp;

use Lcobucci\ContentNegotiation\Formatter;
use Lcobucci\ContentNegotiation\UnformattedResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamFactoryInterface;

final class MyFancyFormatter implements Formatter
{
public function format($content, array $attributes = []): string
public function format(UnformattedResponse $response, StreamFactoryInterface $streamFactory): ResponseInterface
{
// Performs all the magic with $content and creates $result with a
// `string` containing the formatted data.
$content = ''; // Do some fancy formatting of $response->getUnformattedContent() and put into $content

return $result;
return $response->withBody($streamFactory->createStream($content));
}
}
```
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
},
"extra": {
"branch-alias": {
"dev-master": "2.3.x-dev"
"dev-master": "3.0.x-dev"
}
},
"require": {
Expand Down
23 changes: 3 additions & 20 deletions src/ContentTypeMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace Lcobucci\ContentNegotiation;

use Fig\Http\Message\StatusCodeInterface;
use Lcobucci\ContentNegotiation\Formatter\NotAcceptable;
use Middlewares\ContentType;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
Expand Down Expand Up @@ -66,9 +66,9 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
}

$contentType = $this->extractContentType($response->getHeaderLine('Content-Type'));
$formatter = $this->formatters[$contentType] ?? null;
$formatter = $this->formatters[$contentType] ?? new NotAcceptable();

return $this->formatResponse($response, $formatter);
return $formatter->format($response, $this->streamFactory);
}

private function extractContentType(string $contentType): string
Expand All @@ -81,21 +81,4 @@ private function extractContentType(string $contentType): string

return substr($contentType, 0, $charsetSeparatorPosition);
}

/**
* @throws ContentCouldNotBeFormatted
*/
private function formatResponse(UnformattedResponse $response, ?Formatter $formatter): ResponseInterface
{
if ($formatter === null) {
return $response->withBody($this->streamFactory->createStream())
->withStatus(StatusCodeInterface::STATUS_NOT_ACCEPTABLE);
}

return $response->withBody(
$this->streamFactory->createStream(
$formatter->format($response->getUnformattedContent(), $response->getAttributes())
)
);
}
}
8 changes: 4 additions & 4 deletions src/Formatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@

namespace Lcobucci\ContentNegotiation;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamFactoryInterface;

interface Formatter
{
/**
* @param mixed $content
* @param mixed[] $attributes
*
* @throws ContentCouldNotBeFormatted
*/
public function format($content, array $attributes = []): string;
public function format(UnformattedResponse $response, StreamFactoryInterface $streamFactory): ResponseInterface;
}
29 changes: 29 additions & 0 deletions src/Formatter/ContentOnly.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);

namespace Lcobucci\ContentNegotiation\Formatter;

use Lcobucci\ContentNegotiation\ContentCouldNotBeFormatted;
use Lcobucci\ContentNegotiation\Formatter;
use Lcobucci\ContentNegotiation\UnformattedResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamFactoryInterface;

abstract class ContentOnly implements Formatter
{
/** @throws ContentCouldNotBeFormatted */
public function format(UnformattedResponse $response, StreamFactoryInterface $streamFactory): ResponseInterface
{
return $response->withBody(
$streamFactory->createStream(
$this->formatContent($response->getUnformattedContent(), $response->getAttributes())
)
);
}

/**
* @param mixed $content
* @param mixed[] $attributes
*/
abstract public function formatContent($content, array $attributes = []): string;
}
5 changes: 2 additions & 3 deletions src/Formatter/JmsSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@

use JMS\Serializer\SerializerInterface;
use Lcobucci\ContentNegotiation\ContentCouldNotBeFormatted;
use Lcobucci\ContentNegotiation\Formatter;
use Throwable;
use function sprintf;

final class JmsSerializer implements Formatter
final class JmsSerializer extends ContentOnly
{
private SerializerInterface $serializer;
private string $format;
Expand All @@ -23,7 +22,7 @@ public function __construct(SerializerInterface $serializer, string $format)
/**
* {@inheritdoc}
*/
public function format($content, array $attributes = []): string
public function formatContent($content, array $attributes = []): string
{
try {
return $this->serializer->serialize($content, $this->format);
Expand Down
5 changes: 2 additions & 3 deletions src/Formatter/Json.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
namespace Lcobucci\ContentNegotiation\Formatter;

use Lcobucci\ContentNegotiation\ContentCouldNotBeFormatted;
use Lcobucci\ContentNegotiation\Formatter;
use Throwable;
use function json_encode;
use function sprintf;
Expand All @@ -15,7 +14,7 @@
use const JSON_THROW_ON_ERROR;
use const JSON_UNESCAPED_SLASHES;

final class Json implements Formatter
final class Json extends ContentOnly
{
private const DEFAULT_FLAGS = JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES;

Expand All @@ -29,7 +28,7 @@ public function __construct(int $flags = self::DEFAULT_FLAGS)
/**
* {@inheritdoc}
*/
public function format($content, array $attributes = []): string
public function formatContent($content, array $attributes = []): string
{
try {
return json_encode($content, $this->flags | JSON_THROW_ON_ERROR);
Expand Down
19 changes: 19 additions & 0 deletions src/Formatter/NotAcceptable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);

namespace Lcobucci\ContentNegotiation\Formatter;

use Fig\Http\Message\StatusCodeInterface;
use Lcobucci\ContentNegotiation\Formatter;
use Lcobucci\ContentNegotiation\UnformattedResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamFactoryInterface;

class NotAcceptable implements Formatter
{
public function format(UnformattedResponse $response, StreamFactoryInterface $streamFactory): ResponseInterface
{
return $response->withBody($streamFactory->createStream())
->withStatus(StatusCodeInterface::STATUS_NOT_ACCEPTABLE);
}
}
5 changes: 2 additions & 3 deletions src/Formatter/Plates.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@
namespace Lcobucci\ContentNegotiation\Formatter;

use Lcobucci\ContentNegotiation\ContentCouldNotBeFormatted;
use Lcobucci\ContentNegotiation\Formatter;
use League\Plates\Engine;
use Throwable;

final class Plates implements Formatter
final class Plates extends ContentOnly
{
private const DEFAULT_ATTRIBUTE = 'template';

Expand All @@ -24,7 +23,7 @@ public function __construct(Engine $engine, string $attributeName = self::DEFAUL
/**
* {@inheritdoc}
*/
public function format($content, array $attributes = []): string
public function formatContent($content, array $attributes = []): string
{
try {
return $this->render($content, $attributes);
Expand Down
5 changes: 2 additions & 3 deletions src/Formatter/StringCast.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@
namespace Lcobucci\ContentNegotiation\Formatter;

use Lcobucci\ContentNegotiation\ContentCouldNotBeFormatted;
use Lcobucci\ContentNegotiation\Formatter;
use function is_object;
use function method_exists;

final class StringCast implements Formatter
final class StringCast extends ContentOnly
{
/**
* {@inheritdoc}
*/
public function format($content, array $attributes = []): string
public function formatContent($content, array $attributes = []): string
{
if (is_object($content) && ! method_exists($content, '__toString')) {
throw new ContentCouldNotBeFormatted('Given data could not be cast to string');
Expand Down
5 changes: 2 additions & 3 deletions src/Formatter/Twig.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@
namespace Lcobucci\ContentNegotiation\Formatter;

use Lcobucci\ContentNegotiation\ContentCouldNotBeFormatted;
use Lcobucci\ContentNegotiation\Formatter;
use Throwable;
use Twig\Environment;

final class Twig implements Formatter
final class Twig extends ContentOnly
{
private const DEFAULT_ATTRIBUTE = 'template';

Expand All @@ -26,7 +25,7 @@ public function __construct(
/**
* {@inheritdoc}
*/
public function format($content, array $attributes = []): string
public function formatContent($content, array $attributes = []): string
{
try {
return $this->render($content, $attributes);
Expand Down
9 changes: 5 additions & 4 deletions tests/ContentTypeMiddlewareTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ final class ContentTypeMiddlewareTest extends TestCase
* @covers ::process()
*
* @uses \Lcobucci\ContentNegotiation\Formatter\Json
* @uses \Lcobucci\ContentNegotiation\Formatter\ContentOnly
*/
public function processShouldReturnFormattedResponseDirectly(): void
{
Expand All @@ -64,10 +65,10 @@ public function processShouldReturnFormattedResponseDirectly(): void
* @covers ::fromRecommendedSettings()
* @covers ::process()
* @covers ::extractContentType()
* @covers ::formatResponse()
*
* @uses \Lcobucci\ContentNegotiation\UnformattedResponse
* @uses \Lcobucci\ContentNegotiation\Formatter\Json
* @uses \Lcobucci\ContentNegotiation\Formatter\NotAcceptable
*/
public function processShouldReturnAResponseWithErrorWhenFormatterWasNotFound(): void
{
Expand All @@ -90,9 +91,9 @@ public function processShouldReturnAResponseWithErrorWhenFormatterWasNotFound():
* @covers ::fromRecommendedSettings()
* @covers ::process()
* @covers ::extractContentType()
* @covers ::formatResponse()
*
* @uses \Lcobucci\ContentNegotiation\UnformattedResponse
* @uses \Lcobucci\ContentNegotiation\Formatter\ContentOnly
* @uses \Lcobucci\ContentNegotiation\Formatter\Json
*/
public function processShouldReturnAResponseWithFormattedContent(): void
Expand All @@ -117,9 +118,9 @@ public function processShouldReturnAResponseWithFormattedContent(): void
* @covers ::fromRecommendedSettings()
* @covers ::process()
* @covers ::extractContentType()
* @covers ::formatResponse()
*
* @uses \Lcobucci\ContentNegotiation\UnformattedResponse
* @uses \Lcobucci\ContentNegotiation\Formatter\ContentOnly
* @uses \Lcobucci\ContentNegotiation\Formatter\Json
*/
public function processShouldPassAttributesToTheFormatterProperly(): void
Expand Down Expand Up @@ -148,9 +149,9 @@ public function processShouldPassAttributesToTheFormatterProperly(): void
* @covers ::fromRecommendedSettings()
* @covers ::process()
* @covers ::extractContentType()
* @covers ::formatResponse()
*
* @uses \Lcobucci\ContentNegotiation\UnformattedResponse
* @uses \Lcobucci\ContentNegotiation\Formatter\ContentOnly
* @uses \Lcobucci\ContentNegotiation\Formatter\Json
*/
public function processShouldReturnAResponseWithFormattedContentEvenWithoutForcingTheCharset(): void
Expand Down
33 changes: 33 additions & 0 deletions tests/Formatter/ContentOnlyTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);

namespace Lcobucci\ContentNegotiation\Tests\Formatter;

use Laminas\Diactoros\Response;
use Laminas\Diactoros\StreamFactory;
use Lcobucci\ContentNegotiation\Formatter\ContentOnly;
use Lcobucci\ContentNegotiation\UnformattedResponse;
use PHPUnit\Framework\TestCase;

/**
* @coversDefaultClass \Lcobucci\ContentNegotiation\Formatter\ContentOnly
*/
final class ContentOnlyTest extends TestCase
{
/**
* @test
*
* @covers ::format
*
* @uses \Lcobucci\ContentNegotiation\UnformattedResponse
*/
public function formatShouldDelegateTheFormattingToTheConcreteClasses(): void
{
$formatter = $this->getMockForAbstractClass(ContentOnly::class);
$formatter->method('formatContent')->willReturn('A fancy result');

$response = $formatter->format(new UnformattedResponse(new Response(), 'testing'), new StreamFactory());

self::assertSame('A fancy result', $response->getBody()->getContents());
}
}
Loading

0 comments on commit 1c1e0b2

Please sign in to comment.