From 478dbb5df9db8f0d8af6baeb0fe3f3ccae852137 Mon Sep 17 00:00:00 2001 From: Philipp Kerling Date: Wed, 12 Sep 2018 23:05:20 +0200 Subject: [PATCH] [FEATURE] Support nesting in v:variable.set SetViewHelper could only set direct child properties of arrays or objects before, which is inconvenient when working with nested structures. This change adds code to access properties nested at arbitrary levels inside mixed array/object hierarchies. --- .../ViewHelpers/Variable/SetViewHelper.php | 90 ++++++++++++++++--- .../Variable/SetViewHelperTest.php | 89 ++++++++++++++++++ 2 files changed, 169 insertions(+), 10 deletions(-) diff --git a/Classes/ViewHelpers/Variable/SetViewHelper.php b/Classes/ViewHelpers/Variable/SetViewHelper.php index dadf25ac2..9623a7c40 100644 --- a/Classes/ViewHelpers/Variable/SetViewHelper.php +++ b/Classes/ViewHelpers/Variable/SetViewHelper.php @@ -91,21 +91,91 @@ public static function renderStatic(array $arguments, \Closure $renderChildrenCl $variableProvider->remove($name); } $variableProvider->add($name, $value); - } elseif (1 === mb_substr_count($name, '.')) { + } else { $parts = explode('.', $name); $objectName = array_shift($parts); - $path = implode('.', $parts); if (false === $variableProvider->exists($objectName)) { return null; } - $object = $variableProvider->get($objectName); - try { - ObjectAccess::setProperty($object, $path, $value); - // Note: re-insert the variable to ensure unreferenced values like arrays also get updated - $variableProvider->remove($objectName); - $variableProvider->add($objectName, $object); - } catch (\Exception $error) { - return null; + $rootObject = $variableProvider->get($objectName); + $property = array_pop($parts); + + // Setting deeply nested properties when arrays are involved is a bit involved: + // Since they are not objects, ObjectAccess::getProperty will only return the value + // of the (sub)array. For any value change to actually take effect, the changed array + // would have to be reinjected into its context. + // To do this, we traverse the path looking for the beginning of the last nested array + // we encounter. If in the end we still are inside that array (and not in an object), + // we must modify the value inside that array and then inject the modified form into + // its parent element. The parent element might be the variable container directly or + // another object encountered while traversing. The modification of the value is done + // by traversing the array again, but this time by reference, so that the desired + // property can be overriden. + + // Reference to outermost array encountered inside the array currently being traversed + $outermostArray = null; + // Parent object of $outermostArray (null if variable container) + $outermostArrayParent = null; + // Property of $outermostArrayParent that holds $outermostArray (for reinjection) + $outermostArrayParentProperty = null; + if (is_array($rootObject)) { + // If root is an array, use as starting point + $outermostArray = &$rootObject; + } + // Path traversed inside the current array + $arrayPath = []; + // Object/array updated during traversal + $subject = $rootObject; + foreach ($parts as $part) { + // Remember current subject as parent to use below if we encounter the start of an array + $parent = $subject; + // Traverse one level + $subject = ObjectAccess::getProperty($subject, $part); + + if ($subject === null) { + return null; + } else if (is_array($subject)) { + if ($outermostArray === null) { + // Nested array has beguin + $outermostArray = &$subject; + $outermostArrayParent = $parent; + $outermostArrayParentProperty = $part; + } else { + // Nested array continues + $arrayPath[] = $part; + } + } else { + // Not in an array any more, forget everything + // $outermostArray is a reference, so destroy it before setting to null + unset($outermostArray); + $outermostArray = null; + $arrayPath = []; + } + } + + if ($outermostArray !== null) { + // Actually set property in array + $subject = &$outermostArray; + foreach ($arrayPath as $path) { + $subject = &$subject[$path]; + } + $subject[$property] = $value; + + if ($outermostArray === $rootObject) { + // Re-insert array in variable container since it is unreferenced + $variableProvider->remove($objectName); + $variableProvider->add($objectName, $rootObject); + } else { + // Re-insert in structure + ObjectAccess::setProperty($outermostArrayParent, $outermostArrayParentProperty, $outermostArray); + } + } else { + // Final value is an object, just set property and do not re-inject + try { + ObjectAccess::setProperty($subject, $property, $value); + } catch (\Exception $error) { + return null; + } } } return null; diff --git a/Tests/Unit/ViewHelpers/Variable/SetViewHelperTest.php b/Tests/Unit/ViewHelpers/Variable/SetViewHelperTest.php index e6279f83d..094dccdb3 100644 --- a/Tests/Unit/ViewHelpers/Variable/SetViewHelperTest.php +++ b/Tests/Unit/ViewHelpers/Variable/SetViewHelperTest.php @@ -37,6 +37,86 @@ public function canSetVariableInExistingArrayValue() $this->assertFalse($variables['test']['test']); } + /** + * @test + */ + public function canSetVariableNestedOneLevelInArrayValue() + { + $variables = new \ArrayObject(['test' => ['test1' => ['test2' => true]]]); + $this->executeViewHelper(['name' => 'test.test1.test2', 'value' => false], $variables); + $this->assertFalse($variables['test']['test1']['test2']); + } + + /** + * @test + */ + public function canSetVariableNestedTwoLevelsInArrayValue() + { + $variables = new \ArrayObject(['test' => ['test1' => ['test2' => ['test3' => true]]]]); + $this->executeViewHelper(['name' => 'test.test1.test2.test3', 'value' => false], $variables); + $this->assertFalse($variables['test']['test1']['test2']['test3']); + } + + /** + * @test + */ + public function canSetVariableInObject() + { + $variables = new \ArrayObject(['test' => (object) ['test' => true]]); + $this->executeViewHelper(['name' => 'test.test', 'value' => false], $variables); + $this->assertFalse($variables['test']->test); + } + + /** + * @test + */ + public function canSetVariableInArrayNestedInObject() + { + $variables = new \ArrayObject(['test' => (object) ['test1' => ['test2' => true]]]); + $this->executeViewHelper(['name' => 'test.test1.test2', 'value' => false], $variables); + $this->assertFalse($variables['test']->test1['test2']); + } + + /** + * @test + */ + public function canSetVariableNestedInArrayNestedInObject() + { + $variables = new \ArrayObject(['test' => (object) ['test1' => ['test2' => ['test3' => true]]]]); + $this->executeViewHelper(['name' => 'test.test1.test2.test3', 'value' => false], $variables); + $this->assertFalse($variables['test']->test1['test2']['test3']); + } + + /** + * @test + */ + public function canSetVariableInObjectNestedInArrayNestedInObject() + { + $variables = new \ArrayObject(['test' => (object) ['test1' => ['test2' => (object) ['test3' => true]]]]); + $this->executeViewHelper(['name' => 'test.test1.test2.test3', 'value' => false], $variables); + $this->assertFalse($variables['test']->test1['test2']->test3); + } + + /** + * @test + */ + public function canSetVariableInArrayNestedInObjectNestedInArray() + { + $variables = new \ArrayObject(['test' => ['test1' => (object) ['test2' => ['test3' => true]]]]); + $this->executeViewHelper(['name' => 'test.test1.test2.test3', 'value' => false], $variables); + $this->assertFalse($variables['test']['test1']->test2['test3']); + } + + /** + * @test + */ + public function canSetVariableInNestedArrayNestedInObjectNestedInArray() + { + $variables = new \ArrayObject(['test' => ['test1' => (object) ['test2' => ['test3' => ['test4' => true]]]]]); + $this->executeViewHelper(['name' => 'test.test1.test2.test3.test4', 'value' => false], $variables); + $this->assertFalse($variables['test']['test1']->test2['test3']['test4']); + } + /** * @test */ @@ -58,6 +138,15 @@ public function ignoresNestedVariableIfRootDoesNotAllowSetting() $this->assertNull($result); } + /** + * @test + */ + public function ignoresNestedVariableIfPathDoesNotExist() { + $variables = new \ArrayObject(['test' => ['test' => ['test' => true]]]); + $result = $this->executeViewHelper(['name' => 'test.doesnotexist.test.test', 'value' => false], $variables); + $this->assertNull($result); + } + /** * @test */