callback serializer decorator, first version of response builder, WIP: runtime serializer support

This commit is contained in:
Pavel 2021-05-14 20:06:58 +03:00
parent fd797567d0
commit 02423dfb54
10 changed files with 449 additions and 9 deletions

View File

@ -22,9 +22,11 @@
}, },
"require": { "require": {
"php": ">=7.2.0", "php": ">=7.2.0",
"ext-json": "*",
"psr/http-client": "^1.0", "psr/http-client": "^1.0",
"psr/http-message": "^1.0", "psr/http-message": "^1.0",
"php-http/httplug": "^1.0 || ^2.0" "php-http/httplug": "^1.0 || ^2.0",
"nyholm/psr7": "^1.4"
}, },
"require-dev": { "require-dev": {
"squizlabs/php_codesniffer": "^3.6", "squizlabs/php_codesniffer": "^3.6",
@ -35,8 +37,7 @@
"jms/serializer": "^2 | ^3.12", "jms/serializer": "^2 | ^3.12",
"symfony/phpunit-bridge": "^5.2", "symfony/phpunit-bridge": "^5.2",
"symfony/var-dumper": "^5.2", "symfony/var-dumper": "^5.2",
"symfony/serializer": "^5.2", "symfony/serializer": "^5.2"
"nyholm/psr7": "^1.4"
}, },
"provide": { "provide": {
"psr/http-client-implementation": "1.0", "psr/http-client-implementation": "1.0",

View File

@ -0,0 +1,22 @@
<?php
/**
* PHP 7.3
*
* @category JsonException
* @package Pock\Exception
*/
namespace Pock\Exception;
use Exception;
/**
* Class JsonException
*
* @category JsonException
* @package Pock\Exception
*/
class JsonException extends Exception
{
}

View File

@ -27,6 +27,10 @@ abstract class AbstractSerializerFactory implements SerializerCreatorInterface
*/ */
public static function create(): ?SerializerInterface public static function create(): ?SerializerInterface
{ {
if (null !== static::getMainSerializer()) {
return static::getMainSerializer();
}
foreach (static::getCreators() as $creator) { foreach (static::getCreators() as $creator) {
if (!method_exists($creator, 'create')) { if (!method_exists($creator, 'create')) {
continue; continue;
@ -48,4 +52,11 @@ abstract class AbstractSerializerFactory implements SerializerCreatorInterface
* @return string[] * @return string[]
*/ */
abstract protected static function getCreators(): array; abstract protected static function getCreators(): array;
/**
* Returns serializer instance (if it was set by user later).
*
* @return \Pock\Serializer\SerializerInterface|null
*/
abstract protected static function getMainSerializer(): ?SerializerInterface;
} }

View File

@ -10,6 +10,7 @@
namespace Pock\Factory; namespace Pock\Factory;
use Pock\Creator\JmsJsonSerializerCreator; use Pock\Creator\JmsJsonSerializerCreator;
use Pock\Serializer\SerializerInterface;
/** /**
* Class JsonSerializerFactory * Class JsonSerializerFactory
@ -19,6 +20,9 @@ use Pock\Creator\JmsJsonSerializerCreator;
*/ */
class JsonSerializerFactory extends AbstractSerializerFactory class JsonSerializerFactory extends AbstractSerializerFactory
{ {
/** @var \Pock\Serializer\SerializerInterface|null */
private static $mainSerializer;
/** /**
* @inheritDoc * @inheritDoc
*/ */
@ -28,4 +32,20 @@ class JsonSerializerFactory extends AbstractSerializerFactory
JmsJsonSerializerCreator::class, JmsJsonSerializerCreator::class,
]; ];
} }
/**
* @param \Pock\Serializer\SerializerInterface|null $serializer
*/
public static function setSerializer(?SerializerInterface $serializer): void
{
static::$mainSerializer = $serializer;
}
/**
* @inheritDoc
*/
protected static function getMainSerializer(): ?SerializerInterface
{
return static::$mainSerializer;
}
} }

View File

@ -10,6 +10,7 @@
namespace Pock\Factory; namespace Pock\Factory;
use Pock\Creator\JmsXmlSerializerCreator; use Pock\Creator\JmsXmlSerializerCreator;
use Pock\Serializer\SerializerInterface;
/** /**
* Class XmlSerializerFactory * Class XmlSerializerFactory
@ -19,6 +20,9 @@ use Pock\Creator\JmsXmlSerializerCreator;
*/ */
class XmlSerializerFactory extends AbstractSerializerFactory class XmlSerializerFactory extends AbstractSerializerFactory
{ {
/** @var \Pock\Serializer\SerializerInterface|null */
private static $mainSerializer;
/** /**
* @inheritDoc * @inheritDoc
*/ */
@ -28,4 +32,20 @@ class XmlSerializerFactory extends AbstractSerializerFactory
JmsXmlSerializerCreator::class JmsXmlSerializerCreator::class
]; ];
} }
/**
* @param \Pock\Serializer\SerializerInterface|null $serializer
*/
public static function setSerializer(?SerializerInterface $serializer): void
{
static::$mainSerializer = $serializer;
}
/**
* @inheritDoc
*/
protected static function getMainSerializer(): ?SerializerInterface
{
return static::$mainSerializer;
}
} }

View File

@ -29,8 +29,8 @@ class PockBuilder
/** @var \Pock\Matchers\MultipleMatcher */ /** @var \Pock\Matchers\MultipleMatcher */
private $matcher; private $matcher;
/** @var \Psr\Http\Message\ResponseInterface|null */ /** @var \Pock\PockResponseBuilder|null */
private $response; private $responseBuilder;
/** @var \Throwable|null */ /** @var \Throwable|null */
private $throwable; private $throwable;
@ -120,6 +120,22 @@ class PockBuilder
return $this; return $this;
} }
/**
* @param int $statusCode
*
* @return \Pock\PockResponseBuilder
*/
public function reply(int $statusCode = 200): PockResponseBuilder
{
if (null === $this->responseBuilder) {
$this->responseBuilder = new PockResponseBuilder($statusCode);
return $this->responseBuilder;
}
return $this->responseBuilder->withStatusCode($statusCode);
}
/** /**
* Resets the builder. * Resets the builder.
* *
@ -128,7 +144,7 @@ class PockBuilder
public function reset(): PockBuilder public function reset(): PockBuilder
{ {
$this->matcher = new MultipleMatcher(); $this->matcher = new MultipleMatcher();
$this->response = null; $this->responseBuilder = null;
$this->throwable = null; $this->throwable = null;
$this->maxHits = 1; $this->maxHits = 1;
$this->mocks = []; $this->mocks = [];
@ -159,14 +175,25 @@ class PockBuilder
private function closePrevious(): void private function closePrevious(): void
{ {
if (null !== $this->response || null !== $this->throwable) { if (null !== $this->responseBuilder || null !== $this->throwable) {
if (0 === count($this->matcher)) { if (0 === count($this->matcher)) {
$this->matcher->addMatcher(new AnyRequestMatcher()); $this->matcher->addMatcher(new AnyRequestMatcher());
} }
$this->mocks[] = new Mock($this->matcher, $this->response, $this->throwable, $this->maxHits); $response = null;
if (null !== $this->responseBuilder) {
$response = $this->responseBuilder->getResponse();
}
$this->mocks[] = new Mock(
$this->matcher,
$response,
$this->throwable,
$this->maxHits
);
$this->matcher = new MultipleMatcher(); $this->matcher = new MultipleMatcher();
$this->response = null; $this->responseBuilder = null;
$this->throwable = null; $this->throwable = null;
$this->maxHits = 1; $this->maxHits = 1;
} }

209
src/PockResponseBuilder.php Normal file
View File

@ -0,0 +1,209 @@
<?php
/**
* PHP 7.3
*
* @category PockResponseBuilder
* @package Pock
*/
namespace Pock;
use InvalidArgumentException;
use JsonSerializable;
use Nyholm\Psr7\Factory\Psr17Factory;
use Pock\Exception\JsonException;
use Pock\Factory\JsonSerializerFactory;
use Pock\Factory\XmlSerializerFactory;
use Pock\Serializer\SerializerInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Class PockResponseBuilder
*
* @category PockResponseBuilder
* @package Pock
*/
class PockResponseBuilder
{
/** @var \Psr\Http\Message\ResponseInterface */
private $response;
/** @var Psr17Factory */
private $factory;
/** @var SerializerInterface|null */
private static $jsonSerializer;
/** @var SerializerInterface|null */
private static $xmlSerializer;
/**
* PockResponseBuilder constructor.
*
* @param int $statusCode
*/
public function __construct(int $statusCode = 200)
{
$this->factory = new Psr17Factory();
$this->response = $this->factory->createResponse($statusCode);
}
/**
* Reply with specified status code.
*
* @param int $statusCode
*
* @return \Pock\PockResponseBuilder
*/
public function withStatusCode(int $statusCode = 200): PockResponseBuilder
{
$this->response = $this->response->withStatus($statusCode);
return $this;
}
/**
* Reply with specified body. It can be:
* - PSR-7 StreamInterface - it will be used without any changes.
* - string - it will be used as stream contents.
* - resource - it's data will be used as stream contents.
*
* @param \Psr\Http\Message\StreamInterface|resource|string $stream
*
* @return $this
*/
public function withBody($stream): PockResponseBuilder
{
if (is_string($stream)) {
$stream = $this->factory->createStream($stream);
}
if (is_resource($stream)) {
$stream = $this->factory->createStreamFromResource($stream);
}
$this->response = $this->response->withBody($stream);
return $this;
}
/**
* @param mixed $data
*
* @return $this
* @throws \Pock\Exception\JsonException
*/
public function withJson($data): PockResponseBuilder
{
if (is_string($data) || is_numeric($data)) {
return $this->withBody((string) $data);
}
if (is_array($data)) {
return $this->withBody(static::jsonEncode($data));
}
if (is_object($data)) {
if ($data instanceof JsonSerializable) {
return $this->withBody(static::jsonEncode($data));
}
return $this->withBody(static::jsonSerializer()->serialize($data));
}
throw new InvalidArgumentException('Cannot serialize data with type ' . gettype($data));
}
/**
* @param mixed $data
*
* @return $this
* @throws \Pock\Exception\JsonException
*/
public function withXml($data): PockResponseBuilder
{
if (is_string($data)) {
return $this->withBody($data);
}
if (is_array($data) || is_object($data)) {
return $this->withBody(static::xmlSerializer()->serialize($data));
}
throw new InvalidArgumentException('Cannot serialize data with type ' . gettype($data));
}
/**
* @return \Psr\Http\Message\ResponseInterface
*/
public function getResponse(): ResponseInterface
{
return $this->response;
}
/**
* Encode JSON, throw an exception on error.
*
* @param mixed $data
*
* @return string
* @throws \Pock\Exception\JsonException
*/
private static function jsonEncode($data): string
{
$data = json_encode($data);
if (JSON_ERROR_NONE !== json_last_error()) {
throw new JsonException(json_last_error_msg(), json_last_error());
}
return (string) $data;
}
/**
* @return \Pock\Serializer\SerializerInterface
* @throws \Pock\Exception\JsonException
*
* @SuppressWarnings(PHPMD.StaticAccess)
*/
private static function jsonSerializer(): SerializerInterface
{
if (null !== static::$jsonSerializer) {
return static::$jsonSerializer;
}
$serializer = JsonSerializerFactory::create();
if (null === $serializer) {
throw new JsonException('No JSON serializer available');
}
static::$jsonSerializer = $serializer;
return $serializer;
}
/**
* @return \Pock\Serializer\SerializerInterface
* @throws \Pock\Exception\JsonException
*
* @SuppressWarnings(PHPMD.StaticAccess)
*/
private static function xmlSerializer(): SerializerInterface
{
if (null !== static::$xmlSerializer) {
return static::$xmlSerializer;
}
$serializer = XmlSerializerFactory::create();
if (null === $serializer) {
throw new JsonException('No XML serializer available');
}
static::$xmlSerializer = $serializer;
return $serializer;
}
}

View File

@ -0,0 +1,51 @@
<?php
/**
* PHP 7.3
*
* @category CallbackSerializerDecorator
* @package Pock\Serializer
*/
namespace Pock\Serializer;
use RuntimeException;
/**
* Class CallbackSerializerDecorator
*
* @category CallbackSerializerDecorator
* @package Pock\Serializer
*/
class CallbackSerializerDecorator implements SerializerInterface
{
/** @var callable */
private $callback;
/**
* CallbackSerializerDecorator constructor.
*
* @param callable $callback
*/
public function __construct(callable $callback)
{
$this->callback = $callback;
}
/**
* @inheritDoc
*/
public function serialize($data): string
{
$result = call_user_func($this->callback, $data);
if (is_string($result)) {
return $result;
}
throw new RuntimeException(sprintf(
'Invalid data from serialization callback: expected string, %s given',
gettype($result)
));
}
}

View File

@ -0,0 +1,29 @@
<?php
/**
* PHP 7.3
*
* @category CallbackSerializerDecoratorTest
* @package Pock\Tests\Decorator
*/
namespace Pock\Tests\Decorator;
use PHPUnit\Framework\TestCase;
use Pock\Serializer\CallbackSerializerDecorator;
/**
* Class CallbackSerializerDecoratorTest
*
* @category CallbackSerializerDecoratorTest
* @package Pock\Tests\Decorator
*/
class CallbackSerializerDecoratorTest extends TestCase
{
public function testSerialize(): void
{
self::assertEquals('{}', (new CallbackSerializerDecorator(function ($data) {
return $data;
}))->serialize('{}'));
}
}

View File

@ -0,0 +1,50 @@
<?php
/**
* PHP 7.3
*
* @category AbstractSerializerFactoryTest
* @package Pock\Tests\Factory
*/
namespace Pock\Tests\Factory;
use PHPUnit\Framework\TestCase;
use Pock\Factory\JsonSerializerFactory;
use Pock\Factory\XmlSerializerFactory;
use Pock\Serializer\CallbackSerializerDecorator;
use Pock\Serializer\SerializerInterface;
/**
* Class AbstractSerializerFactoryTest
*
* @category AbstractSerializerFactoryTest
* @package Pock\Tests\Factory
*/
class AbstractSerializerFactoryTest extends TestCase
{
public function testSetSerializer(): void
{
$jsonSerializer = new CallbackSerializerDecorator(function ($data) {
return 'jsonSerializer';
});
$xmlSerializer = new CallbackSerializerDecorator(function ($data) {
return 'xmlSerializer';
});
JsonSerializerFactory::setSerializer($jsonSerializer);
XmlSerializerFactory::setSerializer($xmlSerializer);
$resultJsonSerializer = JsonSerializerFactory::create();
$resultXmlSerializer = XmlSerializerFactory::create();
JsonSerializerFactory::setSerializer(null);
XmlSerializerFactory::setSerializer(null);
self::assertInstanceOf(SerializerInterface::class, $resultJsonSerializer);
self::assertInstanceOf(SerializerInterface::class, $resultXmlSerializer);
self::assertEquals($jsonSerializer, $resultJsonSerializer);
self::assertEquals($xmlSerializer, $resultXmlSerializer);
self::assertNotEquals($jsonSerializer, $xmlSerializer);
}
}