Commit 34c57726 authored by Nikita Hovratov's avatar Nikita Hovratov Committed by Anja Leichsenring
Browse files

[FEATURE] Enable recursive transformation of properties in JsonView

The new property '_recursive' allows to define recursive properties
of objects. These properties are transformed automatically, without
the need to define every entry level manually.

Objects can contain a list of the same type like themselves.
Examples could be folders containing folders or comments having
comments as replies.

Resolves: #90347
Releases: master
Change-Id: I29b34cf3666c5ad142e51371240501cb85941b5a
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/63181

Tested-by: Andreas Fernandez's avatarAndreas Fernandez <a.fernandez@scripting-base.de>
Tested-by: core-ci's avatarcore-ci <typo3@b13.com>
Tested-by: Anja Leichsenring's avatarAnja Leichsenring <aleichsenring@ab-softlab.de>
Reviewed-by: Andreas Fernandez's avatarAndreas Fernandez <a.fernandez@scripting-base.de>
Reviewed-by: Anja Leichsenring's avatarAnja Leichsenring <aleichsenring@ab-softlab.de>
parent 060b8fa9
.. include:: ../../Includes.txt
===========================================================================
Feature: #90347 - Enable recursive transformation of properties in JsonView
===========================================================================
See :issue:`90347`
Description
===========
The Extbase :php:`JsonView` is now able to resolve recursive properties of
objects, e.g. directories containing directories or comments containing comments
as replies.
Examples:
1. This is for 1:1 relations, where a comment has at most 1 comment.
.. code-block:: php
$configuration = [
'comment' => [
'_recursive' => ['comment']
]
];
2. This is for the more common 1:n relation in which you have lists of sub objects.
.. code-block:: php
$configuration = [
'directories' => [
'_descendAll' => [
'_recursive' => ['directories']
],
]
];
You can put all the other configuration like `_only` or `_exclude` at the same
level as `_recursive` and the view will apply this for all levels.
Impact
======
Developers can now use the `_recursive` property in the :php:`JsonView`
configuration in order to resolve recursive properties instead of defining each
level manually.
.. index:: ext:extbase
...@@ -48,6 +48,11 @@ class JsonView extends AbstractView ...@@ -48,6 +48,11 @@ class JsonView extends AbstractView
*/ */
protected $variablesToRender = ['value']; protected $variablesToRender = ['value'];
/**
* @var string
*/
protected $currentVariable = '';
/** /**
* The rendering configuration for this JSON view which * The rendering configuration for this JSON view which
* determines which properties of each variable to render. * determines which properties of each variable to render.
...@@ -67,7 +72,7 @@ class JsonView extends AbstractView ...@@ -67,7 +72,7 @@ class JsonView extends AbstractView
* '_exclude' => ['secretTitle'], * '_exclude' => ['secretTitle'],
* '_descend' => [ * '_descend' => [
* 'customer' => [ * 'customer' => [
* '_only' => [array(]'firstName', 'lastName'] * '_only' => ['firstName', 'lastName']
* ] * ]
* ] * ]
* ], * ],
...@@ -203,17 +208,20 @@ class JsonView extends AbstractView ...@@ -203,17 +208,20 @@ class JsonView extends AbstractView
protected function renderArray() protected function renderArray()
{ {
if (count($this->variablesToRender) === 1) { if (count($this->variablesToRender) === 1) {
$firstLevel = false;
$variableName = current($this->variablesToRender); $variableName = current($this->variablesToRender);
$this->currentVariable = $variableName;
$valueToRender = $this->variables[$variableName] ?? null; $valueToRender = $this->variables[$variableName] ?? null;
$configuration = $this->configuration[$variableName] ?? []; $configuration = $this->configuration[$variableName] ?? [];
} else { } else {
$firstLevel = true;
$valueToRender = []; $valueToRender = [];
foreach ($this->variablesToRender as $variableName) { foreach ($this->variablesToRender as $variableName) {
$valueToRender[$variableName] = $this->variables[$variableName] ?? null; $valueToRender[$variableName] = $this->variables[$variableName] ?? null;
} }
$configuration = $this->configuration; $configuration = $this->configuration;
} }
return $this->transformValue($valueToRender, $configuration); return $this->transformValue($valueToRender, $configuration, $firstLevel);
} }
/** /**
...@@ -222,13 +230,17 @@ class JsonView extends AbstractView ...@@ -222,13 +230,17 @@ class JsonView extends AbstractView
* *
* @param mixed $value The value to transform * @param mixed $value The value to transform
* @param array $configuration Configuration for transforming the value * @param array $configuration Configuration for transforming the value
* @param bool $firstLevel
* @return mixed The transformed value * @return mixed The transformed value
*/ */
protected function transformValue($value, array $configuration) protected function transformValue($value, array $configuration, $firstLevel = false)
{ {
if (is_array($value) || $value instanceof \ArrayAccess) { if (is_array($value) || $value instanceof \ArrayAccess) {
$array = []; $array = [];
foreach ($value as $key => $element) { foreach ($value as $key => $element) {
if ($firstLevel) {
$this->currentVariable = $key;
}
if (isset($configuration['_descendAll']) && is_array($configuration['_descendAll'])) { if (isset($configuration['_descendAll']) && is_array($configuration['_descendAll'])) {
$array[$key] = $this->transformValue($element, $configuration['_descendAll']); $array[$key] = $this->transformValue($element, $configuration['_descendAll']);
} else { } else {
...@@ -279,6 +291,8 @@ class JsonView extends AbstractView ...@@ -279,6 +291,8 @@ class JsonView extends AbstractView
$propertiesToRender[$propertyName] = $propertyValue; $propertiesToRender[$propertyName] = $propertyValue;
} elseif (isset($configuration['_descend']) && array_key_exists($propertyName, $configuration['_descend'])) { } elseif (isset($configuration['_descend']) && array_key_exists($propertyName, $configuration['_descend'])) {
$propertiesToRender[$propertyName] = $this->transformValue($propertyValue, $configuration['_descend'][$propertyName]); $propertiesToRender[$propertyName] = $this->transformValue($propertyValue, $configuration['_descend'][$propertyName]);
} elseif (isset($configuration['_recursive']) && in_array($propertyName, $configuration['_recursive'])) {
$propertiesToRender[$propertyName] = $this->transformValue($propertyValue, $this->configuration[$this->currentVariable]);
} }
} }
if (isset($configuration['_exposeObjectIdentifier']) && $configuration['_exposeObjectIdentifier'] === true) { if (isset($configuration['_exposeObjectIdentifier']) && $configuration['_exposeObjectIdentifier'] === true) {
......
...@@ -150,12 +150,12 @@ class JsonViewTest extends UnitTestCase ...@@ -150,12 +150,12 @@ class JsonViewTest extends UnitTestCase
$dateTimeObject = new \DateTime('2011-02-03T03:15:23', new \DateTimeZone('UTC')); $dateTimeObject = new \DateTime('2011-02-03T03:15:23', new \DateTimeZone('UTC'));
$configuration = []; $configuration = [];
$expected = '2011-02-03T03:15:23+00:00'; $expected = '2011-02-03T03:15:23+00:00';
$output[] = [$dateTimeObject, $configuration, $expected, 'DateTime object in UTC time zone could not be serialized.']; $output[] = [$dateTimeObject, $configuration, $expected, 'DateTime object in UTC time zone should not be serialized.'];
$dateTimeObject = new \DateTime('2013-08-15T15:25:30', new \DateTimeZone('America/Los_Angeles')); $dateTimeObject = new \DateTime('2013-08-15T15:25:30', new \DateTimeZone('America/Los_Angeles'));
$configuration = []; $configuration = [];
$expected = '2013-08-15T15:25:30-07:00'; $expected = '2013-08-15T15:25:30-07:00';
$output[] = [$dateTimeObject, $configuration, $expected, 'DateTime object in America/Los_Angeles time zone could not be serialized.']; $output[] = [$dateTimeObject, $configuration, $expected, 'DateTime object in America/Los_Angeles time zone should not be serialized.'];
return $output; return $output;
} }
...@@ -176,6 +176,182 @@ class JsonViewTest extends UnitTestCase ...@@ -176,6 +176,182 @@ class JsonViewTest extends UnitTestCase
self::assertSame($expected, $actual, $description); self::assertSame($expected, $actual, $description);
} }
/**
* data provider for testRecursive()
* @return array
*/
public function jsonViewTestDataRecursive(): array
{
$object = new class('foo') {
private $value1 = '';
private $child;
public function __construct($value1)
{
$this->value1 = $value1;
}
public function getValue1()
{
return $this->value1;
}
public function setValue1(string $value1)
{
$this->value1 = $value1;
}
public function getChild()
{
return $this->child;
}
public function setChild($child)
{
$this->child = $child;
}
};
$child1 = clone $object;
$child1->setValue1('bar');
$child2 = clone $object;
$child2->setValue1('baz');
$child1->setChild($child2);
$object->setChild($child1);
$configuration = [
'testData' => [
'_recursive' => ['child']
]
];
$expected = [
'child' => [
'child' => [
'child' => null,
'value1' => 'baz'
],
'value1' => 'bar',
],
'value1' => 'foo',
];
$output[] = [$object, $configuration, $expected, 'testData', 'Recursive rendering of defined property should be possible.'];
$object = new class('foo') {
private $value1 = '';
private $children = [];
private $secret = 'secret';
public function __construct($value1)
{
$this->value1 = $value1;
}
public function getValue1()
{
return $this->value1;
}
public function setValue1(string $value1)
{
$this->value1 = $value1;
}
public function getChildren()
{
return $this->children;
}
public function addChild($child)
{
$this->children[] = $child;
}
public function getSecret()
{
return $this->secret;
}
};
$child1 = clone $object;
$child1->setValue1('bar');
$child1->addChild(clone $object);
$child1->addChild(clone $object);
$child2 = clone $object;
$child2->setValue1('baz');
$child2->addChild(clone $object);
$child2->addChild(clone $object);
$object->addChild($child1);
$object->addChild($child2);
$children = [
clone $object,
clone $object
];
$configuration = [
'testData' => [
'_descendAll' => [
'_exclude' => ['secret'],
'_recursive' => ['children']
],
]
];
$expected = [
[
'children' => [
[
'children' => [
['children' => [], 'value1' => 'foo'],
['children' => [], 'value1' => 'foo']
],
'value1' => 'bar'
],
[
'children' => [
['children' => [], 'value1' => 'foo'],
['children' => [], 'value1' => 'foo']
],
'value1' => 'baz'
]
],
'value1' => 'foo'
],
[
'children' => [
[
'children' => [
['children' => [], 'value1' => 'foo'],
['children' => [], 'value1' => 'foo']
],
'value1' => 'bar'
],
[
'children' => [
['children' => [], 'value1' => 'foo'],
['children' => [], 'value1' => 'foo']
],
'value1' => 'baz'
]
],
'value1' => 'foo'
]
];
$output[] = [$children, $configuration, $expected, 'testData', 'Recursive rendering of lists of defined property should be possible.'];
return $output;
}
/**
* @test
* @param object|array $object
* @param array $configuration
* @param array|string $expected
* @param string $variableToRender
* @param string $description
* @dataProvider jsonViewTestDataRecursive
*/
public function testRecursive($object, array $configuration, $expected, string $variableToRender, string $description): void
{
$jsonView = $this->getAccessibleMock(JsonView::class, ['dummy'], [], '', false);
$jsonView->_set('configuration', $configuration);
$jsonView->_set('variablesToRender', [$variableToRender]);
$jsonView->_call('assign', $variableToRender, $object);
$actual = $jsonView->_call('renderArray');
self::assertSame($expected, $actual, $description);
}
/** /**
* data provider for testTransformValueWithObjectIdentifierExposure() * data provider for testTransformValueWithObjectIdentifierExposure()
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment