Skip to content
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

BUGFIX: Neos Ui JSON serializable property values #3723

Draft
wants to merge 2 commits into
base: 9.0
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
32 changes: 28 additions & 4 deletions Classes/Domain/Service/NodePropertyConverterService.php
Expand Up @@ -14,6 +14,11 @@

namespace Neos\Neos\Ui\Domain\Service;

use Neos\ContentRepository\Core\Infrastructure\Property\Normalizer\ValueObjectArrayDenormalizer;
use Neos\ContentRepository\Core\Infrastructure\Property\Normalizer\ValueObjectBoolDenormalizer;
use Neos\ContentRepository\Core\Infrastructure\Property\Normalizer\ValueObjectFloatDenormalizer;
use Neos\ContentRepository\Core\Infrastructure\Property\Normalizer\ValueObjectIntDenormalizer;
use Neos\ContentRepository\Core\Infrastructure\Property\Normalizer\ValueObjectStringDenormalizer;
use Neos\ContentRepository\Core\NodeType\NodeType;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindReferencesFilter;
use Neos\ContentRepository\Core\Projection\ContentGraph\Node;
Expand Down Expand Up @@ -209,15 +214,34 @@ public function getPropertiesArray(Node $node)
}

/**
* Convert the given value to a simple type or an array of simple types.
* Convert the given value to a simple type or an array of simple types or to a \JsonSerializable.
*
* @param mixed $propertyValue
* @param string $dataType
* @return mixed
* @param mixed $propertyValue the deserialized node property value
* @param string $dataType the property type from the node type schema
* @return \JsonSerializable|int|float|string|bool|null|array<mixed>
* @throws PropertyException
*/
protected function convertValue($propertyValue, $dataType)
{
if (
$propertyValue instanceof \JsonSerializable
// todo maybe combine them for performance with a static `isDenormalizable` utility.
&& (new ValueObjectArrayDenormalizer())->supportsDenormalization([], $propertyValue::class)
Copy link
Member Author

Choose a reason for hiding this comment

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

and neos/flow-development-collection#2766 seems to be missing as a feature for the new cr...

&& (new ValueObjectBoolDenormalizer())->supportsDenormalization(true, $propertyValue::class)
&& (new ValueObjectFloatDenormalizer())->supportsDenormalization(1.1, $propertyValue::class)
&& (new ValueObjectIntDenormalizer())->supportsDenormalization(1, $propertyValue::class)
&& (new ValueObjectStringDenormalizer())->supportsDenormalization('', $propertyValue::class)
) {
/**
* Value object support as they can be stored directly the node properties via the serializer:
* {@see \Neos\ContentRepository\Core\Infrastructure\Property\PropertyConverter}
*
* If the value is json-serializable and deserializable via e.g. fromArray
* We return the json-serializable directly.
*/
return $propertyValue;
}

$parsedType = TypeHandling::parseType($dataType);

// This hardcoded handling is to circumvent rewriting PropertyMappers that convert objects.
Expand Down
253 changes: 253 additions & 0 deletions Tests/Functional/Domain/Service/NodePropertyConverterServiceTest.php
@@ -0,0 +1,253 @@
<?php
declare(strict_types=1);

namespace Neos\Neos\Ui\Tests\Functional\Domain\Service;

use GuzzleHttp\Psr7\Uri;
use Neos\ContentRepository\Core\NodeType\NodeType;
use Neos\ContentRepository\Core\Projection\ContentGraph\Node;
use Neos\Flow\Tests\FunctionalTestCase;
use Neos\Media\Domain\Model\ImageInterface;
use Neos\Neos\Domain\Model\Domain;
use Neos\Neos\Ui\Domain\Service\NodePropertyConverterService;

/**
* Functional test case which tests the node property converter
*/
class NodePropertyConverterServiceTest extends FunctionalTestCase
{
/**
* @test
*/
public function anArrayOfArrayIsReturnedAsIs()
{
$this->markTestSkipped('Has to be converted into behat test.');

$expected = $propertyValue = [[]];

$nodeType = $this
->getMockBuilder(NodeType::class)
->setMethods(['getPropertyType'])
->disableOriginalConstructor()
->getMock();
$nodeType
->expects(self::once())
->method('getPropertyType')
->willReturn('array');

$node = $this
->getMockBuilder(Node::class)
->setMethods(['getProperty', 'getNodeType'])
->disableOriginalConstructor()
->getMock();
$node
->expects(self::once())
->method('getProperty')
->willReturn($propertyValue);
$node
->expects(self::once())
->method('getNodeType')
->willReturn($nodeType);

$nodePropertyConverterService = new NodePropertyConverterService();

$actual = $nodePropertyConverterService->getProperty($node, 'dontcare');

self::assertEquals($expected, $actual);
}

/**
* @test
*/
public function arrayOfObjectsWithToStringMethodIsReturnedAsIsUnlessTypeConverterIsProvided()
{
$this->markTestSkipped('Has to be converted into behat test.');


$objectWithToStringMethod = new Domain();
$objectWithToStringMethod->setScheme('http');
$objectWithToStringMethod->setHostname('neos.io');
$objectWithToStringMethod->setPort(80);

$expected = $propertyValue = [$objectWithToStringMethod];

$nodeType = $this
->getMockBuilder(NodeType::class)
->setMethods(['getPropertyType'])
->disableOriginalConstructor()
->getMock();
$nodeType
->expects(self::once())
->method('getPropertyType')
->willReturn('array');

$node = $this
->getMockBuilder(Node::class)
->setMethods(['getProperty', 'getNodeType'])
->disableOriginalConstructor()
->getMock();
$node
->expects(self::once())
->method('getProperty')
->willReturn($propertyValue);
$node
->expects(self::once())
->method('getNodeType')
->willReturn($nodeType);

$nodePropertyConverterService = new NodePropertyConverterService();

$actual = $nodePropertyConverterService->getProperty($node, 'dontcare');

self::assertEquals($expected, $actual);
}

/**
* @test
*/
public function arrayOfStringsHasToProvideTypeConverterToBeConvertedToArrayOfStrings()
{
$this->markTestSkipped('Has to be converted into behat test.');

$expected = $propertyValue = ['Hello'];

$nodeType = $this
->getMockBuilder(NodeType::class)
->setMethods(['getPropertyType'])
->disableOriginalConstructor()
->getMock();
$nodeType
->expects(self::once())
->method('getPropertyType')
->willReturn('array<string>');

$node = $this
->getMockBuilder(Node::class)
->setMethods(['getProperty', 'getNodeType'])
->disableOriginalConstructor()
->getMock();
$node
->expects(self::once())
->method('getProperty')
->willReturn($propertyValue);
$node
->expects(self::once())
->method('getNodeType')
->willReturn($nodeType);

$nodePropertyConverterService = new NodePropertyConverterService();

$actual = $nodePropertyConverterService->getProperty($node, 'dontcare');

self::assertEquals($expected, $actual);
}

/**
* @test
*/
public function complexTypesWithGivenTypeConverterAreConvertedByTypeConverter()
{
$this->markTestSkipped('Has to be converted into behat test.');

$propertyValue = $this->getMockForAbstractClass(ImageInterface::class);
$expected = [
'__identity' => null,
'__type' => get_class($propertyValue)
];

$nodeType = $this
->getMockBuilder(NodeType::class)
->setMethods(['getPropertyType'])
->disableOriginalConstructor()
->getMock();
$nodeType
->expects(self::any())
->method('getPropertyType')
->willReturn(ImageInterface::class);

$node = $this
->getMockBuilder(Node::class)
->setMethods(['getProperty', 'getNodeType'])
->disableOriginalConstructor()
->getMock();
$node
->expects(self::any())
->method('getProperty')
->willReturn($propertyValue);
$node
->expects(self::any())
->method('getNodeType')
->willReturn($nodeType);

$nodePropertyConverterService = new NodePropertyConverterService();

$actual = $nodePropertyConverterService->getProperty($node, 'dontcare');

self::assertEquals($expected, $actual);
}

/**
* @test
*/
public function jsonSerializedAbleTypesAreDirectlySerialized()
{
$this->markTestSkipped('Has to be converted into behat test.');

$voClassName = 'Value' . md5(uniqid(mt_rand(), true));
eval('class ' . $voClassName . ' implements \JsonSerializable' . <<<'PHP'
{
public function __construct(
public \Psr\Http\Message\UriInterface $uri
) {
}

public static function fromArray(array $array): self
{
return new self(
new \GuzzleHttp\Psr7\Uri($array['uri'])
);
}

public function jsonSerialize(): array
{
return [
'uri' => $this->uri->__toString()
];
}
}
PHP);

$propertyValue = new $voClassName(new Uri('localhost://foo.html'));
$expected = '{"uri":"localhost:\\/\\/foo.html"}';

$nodeType = $this
->getMockBuilder(NodeType::class)
->setMethods(['getPropertyType'])
->disableOriginalConstructor()
->getMock();
$nodeType
->expects(self::any())
->method('getPropertyType')
->willReturn(ImageInterface::class);

$node = $this
->getMockBuilder(Node::class)
->setMethods(['getProperty', 'getNodeType'])
->disableOriginalConstructor()
->getMock();
$node
->expects(self::any())
->method('getProperty')
->willReturn($propertyValue);
$node
->expects(self::any())
->method('getNodeType')
->willReturn($nodeType);

$nodePropertyConverterService = new NodePropertyConverterService();

$actual = $nodePropertyConverterService->getProperty($node, 'dontcare');

self::assertEquals($expected, json_encode($actual));
}
}