diff --git a/Neos.ContentRepository/Classes/Domain/Model/Node.php b/Neos.ContentRepository/Classes/Domain/Model/Node.php index 0bddfc4d93c..13909e0aebd 100644 --- a/Neos.ContentRepository/Classes/Domain/Model/Node.php +++ b/Neos.ContentRepository/Classes/Domain/Model/Node.php @@ -947,6 +947,10 @@ public function getProperty($propertyName, bool $returnNodesAsIdentifiers = fals } try { + /** + * In case the value is a value object it _will_ already be deserialized due to the feature in flow_json_array + * {@see \Neos\Flow\Persistence\Doctrine\DataTypes\JsonArrayType::deserializeValueObject} + */ return $this->propertyMapper->convert($value, $expectedPropertyType); } catch (\Neos\Flow\Property\Exception $exception) { throw new NodeException(sprintf('Failed to convert property "%s" of node "%s" to the expected type of "%s": %s', $propertyName, $this->getIdentifier(), $expectedPropertyType, $exception->getMessage()), 1630675703, $exception); diff --git a/Neos.Neos/Classes/Service/Mapping/NodePropertyConverterService.php b/Neos.Neos/Classes/Service/Mapping/NodePropertyConverterService.php index 7523b37cf65..564b0169d2b 100644 --- a/Neos.Neos/Classes/Service/Mapping/NodePropertyConverterService.php +++ b/Neos.Neos/Classes/Service/Mapping/NodePropertyConverterService.php @@ -19,6 +19,7 @@ use Neos\Flow\Property\PropertyMapper; use Neos\Flow\Property\PropertyMappingConfiguration; use Neos\Flow\Property\PropertyMappingConfigurationInterface; +use Neos\Flow\Property\TypeConverter\DenormalizingObjectConverter; use Neos\Utility\ObjectAccess; use Neos\Utility\TypeHandling; use Neos\ContentRepository\Domain\Model\NodeInterface; @@ -155,15 +156,27 @@ public function getPropertiesArray(NodeInterface $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 * @throws PropertyException */ protected function convertValue($propertyValue, $dataType) { + if ($propertyValue instanceof \JsonSerializable && DenormalizingObjectConverter::isDenormalizable($propertyValue::class)) { + /** + * Value object support, as they can be stored directly the node properties flow_json_array + * + * If the value is json-serializable and deserializeable via the {@see DenormalizingObjectConverter} (via e.g. fromArray) + * We return the json-serializable directly. + * + * {@see \Neos\Flow\Persistence\Doctrine\DataTypes\JsonArrayType::deserializeValueObject()} + */ + return $propertyValue; + } + $parsedType = TypeHandling::parseType($dataType); // This hardcoded handling is to circumvent rewriting PropertyMappers that convert objects. Usually they expect the source to be an object already and break if not. diff --git a/Neos.Neos/Tests/Functional/Service/Mapping/NodePropertyConverterServiceTest.php b/Neos.Neos/Tests/Functional/Service/Mapping/NodePropertyConverterServiceTest.php index c65e302ef08..e1f6783bf1a 100644 --- a/Neos.Neos/Tests/Functional/Service/Mapping/NodePropertyConverterServiceTest.php +++ b/Neos.Neos/Tests/Functional/Service/Mapping/NodePropertyConverterServiceTest.php @@ -11,6 +11,7 @@ * source code. */ +use GuzzleHttp\Psr7\Uri; use Neos\ContentRepository\Domain\Model\Node; use Neos\ContentRepository\Domain\Model\NodeType; use Neos\Flow\Tests\FunctionalTestCase; @@ -183,4 +184,67 @@ public function complexTypesWithGivenTypeConverterAreConvertedByTypeConverter() self::assertEquals($expected, $actual); } + + /** + * @test + */ + public function jsonSerializedAbleTypesAreDirectlySerialized() + { + $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)); + } }