From f7b853ebd61441b919cd5cb0f27a652f336aea1d Mon Sep 17 00:00:00 2001 From: Guy Sartorelli Date: Thu, 16 May 2024 16:33:38 +1200 Subject: [PATCH] ENH Looping through arrays in templates --- src/View/SSViewer_DataPresenter.php | 4 +++ src/View/SSViewer_Scope.php | 12 ++++++++ src/View/ViewableData.php | 24 +++++++-------- tests/php/View/SSViewerTest.php | 45 +++++++++++++++++++++++++++++ tests/php/View/ViewableDataTest.php | 42 +++++++++++++++++++++++++++ 5 files changed, 115 insertions(+), 12 deletions(-) diff --git a/src/View/SSViewer_DataPresenter.php b/src/View/SSViewer_DataPresenter.php index 4735bba01a2..e5364936d73 100644 --- a/src/View/SSViewer_DataPresenter.php +++ b/src/View/SSViewer_DataPresenter.php @@ -172,6 +172,10 @@ public function getInjectedValue($property, array $params, $cast = true) // Get source for this value $result = $this->getValueSource($property); if (!array_key_exists('source', $result)) { + // $Me is a special property. If nothing is providing an override, return the current item. + if ($property === 'Me') { + return ['obj' => $this->getItem()]; + } return null; } diff --git a/src/View/SSViewer_Scope.php b/src/View/SSViewer_Scope.php index 3b0fe1a5efe..6f4af08c308 100644 --- a/src/View/SSViewer_Scope.php +++ b/src/View/SSViewer_Scope.php @@ -130,6 +130,16 @@ public function getItem() if (is_scalar($item)) { $item = $this->convertScalarToDBField($item); } + // Wrap arrays + if (is_array($item)) { + if (array_is_list($item)) { + // Wrap in ArrayIterator to respect method signature + $item = new ArrayIterator($item); + } else { + // Wrap in ArrayData so values can be accessed by key in templates + $item = ArrayData::create($item); + } + } return $item; } @@ -308,6 +318,8 @@ public function next() // Item may be an array or a regular IteratorAggregate if (is_array($this->item)) { $this->itemIterator = new ArrayIterator($this->item); + } elseif ($this->item instanceof Iterator) { + $this->itemIterator = $this->item; } else { $this->itemIterator = $this->item->getIterator(); diff --git a/src/View/ViewableData.php b/src/View/ViewableData.php index f5f19ec8c0f..dd5ec4cbd7f 100644 --- a/src/View/ViewableData.php +++ b/src/View/ViewableData.php @@ -537,7 +537,7 @@ protected function objCacheClear() * @param array $arguments * @param bool $cache Cache this object * @param string $cacheName a custom cache name - * @return Object|DBField + * @return object|DBField */ public function obj($fieldName, $arguments = [], $cache = false, $cacheName = null) { @@ -558,6 +558,17 @@ public function obj($fieldName, $arguments = [], $cache = false, $cacheName = nu $value = $this->$fieldName; } + // Wrap arrays + if (is_array($value)) { + if (array_is_list($value)) { + // Wrap in ArrayIterator to respect method signature + $value = new ArrayIterator($value); + } else { + // Wrap in ArrayData so values can be accessed by key in templates + $value = ArrayData::create($value); + } + } + // Cast object if (!is_object($value)) { // Force cast @@ -668,17 +679,6 @@ public function getViewerTemplates($suffix = '') return SSViewer::get_templates_by_class(static::class, $suffix, self::class); } - /** - * When rendering some objects it is necessary to iterate over the object being rendered, to do this, you need - * access to itself. - * - * @return ViewableData - */ - public function Me() - { - return $this; - } - /** * Get part of the current classes ancestry to be used as a CSS class. * diff --git a/tests/php/View/SSViewerTest.php b/tests/php/View/SSViewerTest.php index e5307a7c13e..fd0a0ca9b4e 100644 --- a/tests/php/View/SSViewerTest.php +++ b/tests/php/View/SSViewerTest.php @@ -2,6 +2,7 @@ namespace SilverStripe\View\Tests; +use ArrayIterator; use Exception; use InvalidArgumentException; use LogicException; @@ -1331,6 +1332,50 @@ public function testCastingHelpers() ); } + public function provideLoop(): array + { + return [ + 'nested array and iterator' => [ + 'iterable' => [ + [ + 'value 1', + 'value 2', + ], + new ArrayIterator([ + 'value 3', + 'value 4', + ]), + ], + 'template' => '<% loop $Iterable %><% loop $Me %>$Me<% end_loop %><% end_loop %>', + 'expected' => 'value 1 value 2 value 3 value 4', + ], + 'nested associative arrays' => [ + 'iterable' => [ + [ + 'Foo' => 'one', + ], + [ + 'Foo' => 'two', + ], + [ + 'Foo' => 'three', + ], + ], + 'template' => '<% loop $Iterable %>$Foo<% end_loop %>', + 'expected' => 'one two three', + ], + ]; + } + + /** + * @dataProvider provideLoop + */ + public function testLoop(iterable $iterable, string $template, string $expected): void + { + $data = new ArrayData(['Iterable' => $iterable]); + $this->assertEqualIgnoringWhitespace($expected, $this->render($template, $data)); + } + public function testSSViewerBasicIteratorSupport() { $data = new ArrayData( diff --git a/tests/php/View/ViewableDataTest.php b/tests/php/View/ViewableDataTest.php index 15ff7425290..3403fb2b9e2 100644 --- a/tests/php/View/ViewableDataTest.php +++ b/tests/php/View/ViewableDataTest.php @@ -2,6 +2,7 @@ namespace SilverStripe\View\Tests; +use ArrayIterator; use ReflectionMethod; use SilverStripe\ORM\FieldType\DBField; use SilverStripe\Dev\SapphireTest; @@ -278,4 +279,45 @@ public function testDynamicData() $this->assertSame($obj, $viewableData->getDynamicData('abc')); $this->assertSame($obj, $viewableData->abc); } + + public function provideWrapArrayInObj(): array + { + return [ + 'empty array' => [ + 'arr' => [], + 'expectedClass' => ArrayIterator::class, + ], + 'fully indexed array' => [ + 'arr' => [ + 'value1', + 'value2', + ], + 'expectedClass' => ArrayIterator::class, + ], + 'fully associative array' => [ + 'arr' => [ + 'v1' => 'value1', + 'v2' => 'value2', + ], + 'expectedClass' => ArrayData::class, + ], + 'partially associative array' => [ + 'arr' => [ + 'value1', + 'v2' => 'value2', + ], + 'expectedClass' => ArrayData::class, + ], + ]; + } + + /** + * @dataProvider provideWrapArrayInObj + */ + public function testWrapArrayInObj(array $arr, string $expectedClass): void + { + $viewableData = new ViewableData(); + $viewableData->arr = $arr; + $this->assertInstanceOf($expectedClass, $viewableData->obj('arr')); + } }