1
0
mirror of synced 2025-02-19 21:43:20 +03:00

correct deserialization for inline json, stick to json mode because implementing same for xml is not important

This commit is contained in:
Pavel 2020-10-01 10:34:42 +03:00
parent 9b081e2e7f
commit 95b7c27e6c
9 changed files with 433 additions and 205 deletions

View File

@ -0,0 +1,70 @@
<?php
/**
* PHP version 7.4
*
* @category JsonDeserializationVisitorFactory
* @package RetailCrm\Component\JMS\Factory
* @author RetailCRM <integration@retailcrm.ru>
* @license http://retailcrm.ru Proprietary
* @link http://retailcrm.ru
* @see http://help.retailcrm.ru
*/
namespace RetailCrm\Component\JMS\Factory;
use RetailCrm\Component\JMS\Visitor\Deserialization\JsonDeserializationVisitor;
use JMS\Serializer\Visitor\DeserializationVisitorInterface;
use JMS\Serializer\Visitor\Factory\DeserializationVisitorFactory;
/**
* Class JsonDeserializationVisitorFactory
*
* @category JsonDeserializationVisitorFactory
* @package RetailCrm\Component\JMS\Factory
* @author RetailDriver LLC <integration@retailcrm.ru>
* @license https://retailcrm.ru Proprietary
* @link http://retailcrm.ru
* @see https://help.retailcrm.ru
*/
class JsonDeserializationVisitorFactory implements DeserializationVisitorFactory
{
/**
* @var int
*/
private $options = 0;
/**
* @var int
*/
private $depth = 512;
/**
* @return \JMS\Serializer\Visitor\DeserializationVisitorInterface
*/
public function getVisitor(): DeserializationVisitorInterface
{
return new JsonDeserializationVisitor($this->options, $this->depth);
}
/**
* @param int $options
*
* @return $this
*/
public function setOptions(int $options): self
{
$this->options = $options;
return $this;
}
/**
* @param int $depth
*
* @return $this
*/
public function setDepth(int $depth): self
{
$this->depth = $depth;
return $this;
}
}

View File

@ -0,0 +1,294 @@
<?php
/**
* PHP version 7.4
*
* @category JsonDeserializationVisitor
* @package RetailCrm\Component\JMS\Visitor\Deserialization
* @author RetailCRM <integration@retailcrm.ru>
* @license http://retailcrm.ru Proprietary
* @link http://retailcrm.ru
* @see http://help.retailcrm.ru
*/
namespace RetailCrm\Component\JMS\Visitor\Deserialization;
use JMS\Serializer\AbstractVisitor;
use JMS\Serializer\Exception\LogicException;
use JMS\Serializer\Exception\NotAcceptableException;
use JMS\Serializer\Exception\RuntimeException;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Visitor\DeserializationVisitorInterface;
use SplStack;
/**
* Class JsonDeserializationVisitor
*
* @category JsonDeserializationVisitor
* @package RetailCrm\Component\JMS\Visitor\Deserialization
* @author RetailDriver LLC <integration@retailcrm.ru>
* @license https://retailcrm.ru Proprietary
* @link http://retailcrm.ru
* @see https://help.retailcrm.ru
*/
class JsonDeserializationVisitor extends AbstractVisitor implements DeserializationVisitorInterface
{
/**
* @var int
*/
private $options = 0;
/**
* @var int
*/
private $depth = 512;
/**
* @var \SplStack
*/
private $objectStack;
/**
* @var object|null
*/
private $currentObject;
public function __construct(
int $options = 0,
int $depth = 512
) {
$this->objectStack = new SplStack();
$this->options = $options;
$this->depth = $depth;
}
/**
* {@inheritdoc}
*/
public function visitNull($data, array $type): void
{
}
/**
* {@inheritdoc}
*/
public function visitString($data, array $type): string
{
return (string) $data;
}
/**
* {@inheritdoc}
*/
public function visitBoolean($data, array $type): bool
{
return (bool) $data;
}
/**
* {@inheritdoc}
*/
public function visitInteger($data, array $type): int
{
return (int) $data;
}
/**
* {@inheritdoc}
*/
public function visitDouble($data, array $type): float
{
return (float) $data;
}
/**
* {@inheritdoc}
*/
public function visitArray($data, array $type): array
{
if (!\is_array($data)) {
throw new RuntimeException(sprintf('Expected array, but got %s: %s', \gettype($data), json_encode($data)));
}
// If no further parameters were given, keys/values are just passed as is.
if (!$type['params']) {
return $data;
}
switch (\count($type['params'])) {
case 1: // Array is a list.
$listType = $type['params'][0];
$result = [];
foreach ($data as $v) {
$result[] = $this->navigator->accept($v, $listType);
}
return $result;
case 2: // Array is a map.
[$keyType, $entryType] = $type['params'];
$result = [];
foreach ($data as $k => $v) {
$result[$this->navigator->accept($k, $keyType)] = $this->navigator->accept($v, $entryType);
}
return $result;
default:
throw new RuntimeException(sprintf(
'Array type cannot have more than 2 parameters, but got %s.',
json_encode($type['params'])
));
}
}
/**
* {@inheritdoc}
*/
public function visitDiscriminatorMapProperty($data, ClassMetadata $metadata): string
{
if (isset($data[$metadata->discriminatorFieldName])) {
return (string) $data[$metadata->discriminatorFieldName];
}
throw new LogicException(sprintf(
'The discriminator field name "%s" for base-class "%s" was not found in input data.',
$metadata->discriminatorFieldName,
$metadata->name
));
}
/**
* {@inheritdoc}
*/
public function startVisitingObject(ClassMetadata $metadata, object $object, array $type): void
{
$this->setCurrentObject($object);
}
/**
* {@inheritdoc}
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.NPathComplexity)
*/
public function visitProperty(PropertyMetadata $metadata, $data)
{
$name = $metadata->serializedName;
if (null === $data) {
return;
}
if (!\is_array($data)) {
throw new RuntimeException(sprintf(
'Invalid data %s (%s), expected "%s".',
json_encode($data),
$metadata->type['name'],
$metadata->class
));
}
if (in_array('InlineJsonBody', $metadata->groups ?? [])) {
if (!array_key_exists($metadata->serializedName, $data)) {
throw new RuntimeException(sprintf(
'Cannot find expected key in the data: %s',
$metadata->serializedName
));
}
$data[$metadata->serializedName] = json_decode(
$data[$metadata->serializedName],
true,
512,
JSON_THROW_ON_ERROR
);
}
if (true === $metadata->inline) {
if (!$metadata->type) {
throw RuntimeException::noMetadataForProperty($metadata->class, $metadata->name);
}
return $this->navigator->accept($data, $metadata->type);
}
if (!array_key_exists($name, $data)) {
throw new NotAcceptableException();
}
if (!$metadata->type) {
throw RuntimeException::noMetadataForProperty($metadata->class, $metadata->name);
}
return null !== $data[$name] ? $this->navigator->accept($data[$name], $metadata->type) : null;
}
/**
* {@inheritdoc}
*/
public function endVisitingObject(ClassMetadata $metadata, $data, array $type): object
{
$obj = $this->currentObject;
$this->revertCurrentObject();
return $obj;
}
/**
* {@inheritdoc}
*/
public function getResult($data)
{
return $data;
}
public function setCurrentObject(object $object): void
{
$this->objectStack->push($this->currentObject);
$this->currentObject = $object;
}
public function getCurrentObject(): ?object
{
return $this->currentObject;
}
public function revertCurrentObject(): ?object
{
return $this->currentObject = $this->objectStack->pop();
}
/**
* {@inheritdoc}
*/
public function prepare($str)
{
$decoded = json_decode($str, true, $this->depth, $this->options);
switch (json_last_error()) {
case JSON_ERROR_NONE:
return $decoded;
case JSON_ERROR_DEPTH:
throw new RuntimeException('Could not decode JSON, maximum stack depth exceeded.');
case JSON_ERROR_STATE_MISMATCH:
throw new RuntimeException('Could not decode JSON, underflow or the nodes mismatch.');
case JSON_ERROR_CTRL_CHAR:
throw new RuntimeException('Could not decode JSON, unexpected control character found.');
case JSON_ERROR_SYNTAX:
throw new RuntimeException('Could not decode JSON, syntax error - malformed JSON.');
case JSON_ERROR_UTF8:
throw new RuntimeException('Could not decode JSON, malformed UTF-8 characters (incorrectly encoded?)');
default:
throw new RuntimeException('Could not decode JSON.');
}
}
}

View File

@ -1,201 +0,0 @@
<?php
/**
* PHP version 7.3
*
* @category InlineJsonBodyHandler
* @package RetailCrm\Component\Serializer
* @author RetailCRM <integration@retailcrm.ru>
* @license MIT
* @link http://retailcrm.ru
* @see http://help.retailcrm.ru
*/
namespace RetailCrm\Component\Serializer;
use JMS\Serializer\Context;
use JMS\Serializer\GraphNavigator;
use JMS\Serializer\GraphNavigatorInterface;
use JMS\Serializer\Handler\SubscribingHandlerInterface;
use JMS\Serializer\JsonSerializationVisitor;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Metadata\StaticPropertyMetadata;
use JMS\Serializer\Visitor\DeserializationVisitorInterface;
use JMS\Serializer\Visitor\SerializationVisitorInterface;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use ReflectionClass;
/**
* Class InlineJsonBodyHandler
*
* @category InlineJsonBodyHandler
* @package RetailCrm\Component\Serializer
* @author RetailDriver LLC <integration@retailcrm.ru>
* @license MIT
* @link http://retailcrm.ru
* @see https://help.retailcrm.ru
* @todo Doesn't work as expected.
*/
class InlineJsonBodyHandler implements SubscribingHandlerInterface
{
/**
* @param \JMS\Serializer\Visitor\SerializationVisitorInterface $visitor
* @param mixed $item
* @param array $type
* @param \JMS\Serializer\Context $context
*
* @return false|string
* @throws \JsonException
* @throws \ReflectionException
*/
public function serialize(SerializationVisitorInterface $visitor, $item, array $type, Context $context)
{
$typeName = $type['name'];
$classMetadata = $context->getMetadataFactory()->getMetadataForClass($typeName);
$reflection = new ReflectionClass($type['name']);
$visitor->startVisitingObject($classMetadata, $item, ['name' => $typeName]);
foreach ($reflection->getProperties() as $property) {
if ($property->isStatic()) {
continue;
}
if (!$property->isPublic()) {
$property->setAccessible(true);
}
$value = $property->getValue($item);
$metadata = new StaticPropertyMetadata($type['name'], $property->getName(), $value);
$visitor->visitProperty($metadata, $value);
}
return json_encode(
$visitor->endVisitingObject($classMetadata, $item, ['name' => $typeName]),
JSON_THROW_ON_ERROR
);
}
/**
* @param \JMS\Serializer\Visitor\DeserializationVisitorInterface $visitor
* @param string $json
* @param array $type
* @param \JMS\Serializer\Context $context
*
* @return object
* @throws \JsonException
* @throws \ReflectionException
*/
public function deserialize(DeserializationVisitorInterface $visitor, $json, array $type, Context $context)
{
$typeName = $type['name'];
$instance = new $type['name'];
$classMetadata = $context->getMetadataFactory()->getMetadataForClass($typeName);
$reflection = new ReflectionClass($type['name']);
$jsonData = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
$visitor->startVisitingObject($classMetadata, $instance, ['name' => $typeName]);
foreach ($reflection->getProperties() as $property) {
if ($property->isStatic()) {
continue;
}
if (!$property->isPublic()) {
$property->setAccessible(true);
}
$metadata = new PropertyMetadata($type['name'], $property->getName());
$property->setValue($instance, $visitor->visitProperty($metadata, $jsonData));
}
$result = $visitor->endVisitingObject($classMetadata, $instance, ['name' => $typeName]);
return $visitor->getResult($result);
}
/**
* @return array
*/
public static function getSubscribingMethods()
{
$methods = [];
foreach (self::getInlineJsonBodyModels() as $type) {
$methods[] = [
'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION,
'format' => 'json',
'type' => $type,
'method' => 'serialize',
];
$methods[] = [
'direction' => GraphNavigatorInterface::DIRECTION_DESERIALIZATION,
'format' => 'json',
'type' => $type,
'method' => 'deserialize',
];
$methods[] = [
'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION,
'format' => 'xml',
'type' => $type,
'method' => 'serialize',
];
$methods[] = [
'direction' => GraphNavigatorInterface::DIRECTION_DESERIALIZATION,
'format' => 'xml',
'type' => $type,
'method' => 'deserialize',
];
}
return $methods;
}
/**
* @return array
* @todo That's horrifying. Maybe find better solution?
*/
private static function getInlineJsonBodyModels(): array
{
$items = [];
$rootDir = realpath(dirname(__DIR__) . '/..');
$directory = new RecursiveDirectoryIterator(
__DIR__ . '/../../Model/Response/Body/InlineJsonBody',
RecursiveDirectoryIterator::SKIP_DOTS
);
$fileIterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::LEAVES_ONLY);
/** @var \SplFileObject $file */
foreach ($fileIterator as $file) {
if ('php' !== $file->getExtension()) {
continue;
}
$items[] = self::pathToClasspath($rootDir, $file->getPath(), $file->getFilename());
}
return $items;
}
/**
* @param string $root
* @param string $path
* @param string $fileName
*
* @return string
*/
private static function pathToClasspath(string $root, string $path, string $fileName): string
{
return 'RetailCrm\\' .
str_replace(
DIRECTORY_SEPARATOR,
'\\',
str_replace(
$root . DIRECTORY_SEPARATOR,
'',
realpath($path)
)
) . '\\' . trim(substr($fileName, 0, -4));
}
}

View File

@ -17,9 +17,10 @@ use JMS\Serializer\Handler\HandlerRegistry;
use JMS\Serializer\Serializer;
use JMS\Serializer\SerializerBuilder;
use JMS\Serializer\SerializerInterface;
use JMS\Serializer\Visitor\Factory\JsonSerializationVisitorFactory;
use Psr\Container\ContainerInterface;
use RetailCrm\Component\Constants;
use RetailCrm\Component\Serializer\InlineJsonBodyHandler;
use RetailCrm\Component\JMS\Factory\JsonDeserializationVisitorFactory;
use RetailCrm\Interfaces\FactoryInterface;
/**
@ -133,8 +134,9 @@ class SerializerFactory implements FactoryInterface
'xml',
$returnNull
);
$registry->registerSubscribingHandler(new InlineJsonBodyHandler());
})->addDefaultHandlers()
->setSerializationVisitor('json', new JsonSerializationVisitorFactory())
->setDeserializationVisitor('json', new JsonDeserializationVisitorFactory())
->setSerializationContextFactory(new SerializationContextFactory())
->build();
}

View File

@ -27,9 +27,9 @@ use JMS\Serializer\Annotation as JMS;
class HttpDnsGetResponseResult
{
/**
* @var array $env
* @var array<string, \RetailCrm\Model\Response\Type\HttpDnsEnvEntry> $env
*
* @JMS\Type("int")
* @JMS\Type("array<string,array<string, RetailCrm\Model\Response\Type\HttpDnsEnvEntry>>")
* @JMS\SerializedName("env")
*/
public $env;

View File

@ -32,6 +32,7 @@ class HttpDnsGetResponseData
*
* @JMS\Type("RetailCrm\Model\Response\Body\InlineJsonBody\HttpDnsGetResponseResult")
* @JMS\SerializedName("result")
* @JMS\Groups(groups={"InlineJsonBody"})
*/
public $result;
}

View File

@ -0,0 +1,44 @@
<?php
/**
* PHP version 7.4
*
* @category HttpDnsEnvEntry
* @package RetailCrm\Model\Response\Type
* @author RetailCRM <integration@retailcrm.ru>
* @license http://retailcrm.ru Proprietary
* @link http://retailcrm.ru
* @see http://help.retailcrm.ru
*/
namespace RetailCrm\Model\Response\Type;
use JMS\Serializer\Annotation as JMS;
/**
* Class HttpDnsEnvEntry
*
* @category HttpDnsEnvEntry
* @package RetailCrm\Model\Response\Type
* @author RetailDriver LLC <integration@retailcrm.ru>
* @license https://retailcrm.ru Proprietary
* @link http://retailcrm.ru
* @see https://help.retailcrm.ru
*/
class HttpDnsEnvEntry
{
/**
* @var array
*
* @JMS\Type("array<string>")
* @JMS\SerializedName("vip")
*/
public $vip;
/**
* @var string
*
* @JMS\Type("string")
* @JMS\SerializedName("proto")
*/
public $proto;
}

View File

@ -161,6 +161,10 @@ class Client
*/
public function sendRequest(BaseRequest $request): TopResponseInterface
{
if ('json' !== $request->format) {
throw new TopClientException(sprintf('Client only supports JSON mode, got `%s` mode', $request->format));
}
$this->processor->process($request, $this->appData);
$httpRequest = $this->requestFactory->fromModel($request, $this->appData);

View File

@ -60,4 +60,18 @@ class ClientTest extends TestCase
$client->sendRequest(new HttpDnsGetRequest());
}
public function testClientRequestXmlUnsupported()
{
$client = ClientBuilder::create()
->setContainer($this->getContainer(self::getMockClient()))
->setAppData(new AppData(AppData::OVERSEAS_ENDPOINT, 'appKey', 'appSecret'))
->build();
$request = new HttpDnsGetRequest();
$request->format = 'xml';
$this->expectExceptionMessage('Client only supports JSON mode, got `xml` mode');
$client->sendRequest($request);
}
}