mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 12:05:37 +00:00
ENH Looping through arrays in templates (#11244)
This commit is contained in:
parent
57886b76f5
commit
3f30da5155
@ -127,7 +127,7 @@ class ArrayList extends ViewableData implements SS_List, Filterable, Sortable, L
|
||||
public function getIterator(): Traversable
|
||||
{
|
||||
foreach ($this->items as $i => $item) {
|
||||
if (is_array($item)) {
|
||||
if (is_array($item) && !array_is_list($item)) {
|
||||
yield new ArrayData($item);
|
||||
} else {
|
||||
yield $item;
|
||||
|
@ -77,7 +77,7 @@ class ArrayData extends ViewableData
|
||||
public function getField($field)
|
||||
{
|
||||
$value = $this->array[$field];
|
||||
if (is_object($value) && !$value instanceof ViewableData) {
|
||||
if (is_object($value) && !($value instanceof ViewableData) && !is_iterable($value)) {
|
||||
return new ArrayData($value);
|
||||
} elseif (ArrayLib::is_associative($value)) {
|
||||
return new ArrayData($value);
|
||||
|
@ -1886,8 +1886,6 @@ class SSTemplateParser extends Parser implements TemplateParser
|
||||
$res['php'] .= '((bool)'.$sub['php'].')';
|
||||
} else {
|
||||
$php = ($sub['ArgumentMode'] == 'default' ? $sub['lookup_php'] : $sub['php']);
|
||||
// TODO: kinda hacky - maybe we need a way to pass state down the parse chain so
|
||||
// Lookup_LastLookupStep and Argument_BareWord can produce hasValue instead of XML_val
|
||||
$res['php'] .= str_replace('$$FINAL', 'hasValue', $php ?? '');
|
||||
}
|
||||
}
|
||||
@ -5292,8 +5290,6 @@ class SSTemplateParser extends Parser implements TemplateParser
|
||||
$text = stripslashes($text ?? '');
|
||||
$text = addcslashes($text ?? '', '\'\\');
|
||||
|
||||
// TODO: This is pretty ugly & gets applied on all files not just html. I wonder if we can make this
|
||||
// non-dynamically calculated
|
||||
$code = <<<'EOC'
|
||||
(\SilverStripe\View\SSViewer::getRewriteHashLinksDefault()
|
||||
? \SilverStripe\Core\Convert::raw2att( preg_replace("/^(\\/)+/", "/", $_SERVER['REQUEST_URI'] ) )
|
||||
@ -5332,8 +5328,7 @@ EOC;
|
||||
|
||||
$this->includeDebuggingComments = $includeDebuggingComments;
|
||||
|
||||
// Ignore UTF8 BOM at beginning of string. TODO: Confirm this is needed, make sure SSViewer handles UTF
|
||||
// (and other encodings) properly
|
||||
// Ignore UTF8 BOM at beginning of string.
|
||||
if (substr($string ?? '', 0, 3) == pack("CCC", 0xef, 0xbb, 0xbf)) {
|
||||
$this->pos = 3;
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ namespace SilverStripe\View;
|
||||
use ArrayIterator;
|
||||
use Countable;
|
||||
use Iterator;
|
||||
use SilverStripe\ORM\ArrayList;
|
||||
use SilverStripe\ORM\FieldType\DBBoolean;
|
||||
use SilverStripe\ORM\FieldType\DBText;
|
||||
use SilverStripe\ORM\FieldType\DBFloat;
|
||||
@ -130,6 +131,12 @@ class SSViewer_Scope
|
||||
if (is_scalar($item)) {
|
||||
$item = $this->convertScalarToDBField($item);
|
||||
}
|
||||
|
||||
// Wrap list arrays in ViewableData so templates can handle them
|
||||
if (is_array($item) && array_is_list($item)) {
|
||||
$item = ArrayList::create($item);
|
||||
}
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
@ -308,6 +315,8 @@ class SSViewer_Scope
|
||||
// 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();
|
||||
|
||||
|
@ -20,6 +20,7 @@ use SilverStripe\Core\Injector\Injector;
|
||||
use SilverStripe\Dev\Debug;
|
||||
use SilverStripe\Dev\Deprecation;
|
||||
use SilverStripe\ORM\ArrayLib;
|
||||
use SilverStripe\ORM\ArrayList;
|
||||
use SilverStripe\ORM\FieldType\DBField;
|
||||
use SilverStripe\ORM\FieldType\DBHTMLText;
|
||||
use SilverStripe\View\SSViewer;
|
||||
@ -537,7 +538,7 @@ class ViewableData implements IteratorAggregate
|
||||
* @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 +559,11 @@ class ViewableData implements IteratorAggregate
|
||||
$value = $this->$fieldName;
|
||||
}
|
||||
|
||||
// Wrap list arrays in ViewableData so templates can handle them
|
||||
if (is_array($value) && array_is_list($value)) {
|
||||
$value = ArrayList::create($value);
|
||||
}
|
||||
|
||||
// Cast object
|
||||
if (!is_object($value)) {
|
||||
// Force cast
|
||||
@ -601,8 +607,11 @@ class ViewableData implements IteratorAggregate
|
||||
public function hasValue($field, $arguments = [], $cache = true)
|
||||
{
|
||||
$result = $this->obj($field, $arguments, $cache);
|
||||
if ($result instanceof ViewableData) {
|
||||
return $result->exists();
|
||||
}
|
||||
return (bool) $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the string value of a field on this object that has been suitable escaped to be inserted directly into a
|
||||
@ -671,10 +680,8 @@ class ViewableData implements IteratorAggregate
|
||||
/**
|
||||
* 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()
|
||||
public function Me(): static
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
@ -1025,6 +1025,44 @@ after'
|
||||
);
|
||||
}
|
||||
|
||||
public function provideIfBlockWithIterable(): array
|
||||
{
|
||||
$scenarios = [
|
||||
'empty array' => [
|
||||
'iterable' => [],
|
||||
'inScope' => false,
|
||||
],
|
||||
'array' => [
|
||||
'iterable' => [1, 2, 3],
|
||||
'inScope' => false,
|
||||
],
|
||||
'ArrayList' => [
|
||||
'iterable' => new ArrayList([['Val' => 1], ['Val' => 2], ['Val' => 3]]),
|
||||
'inScope' => false,
|
||||
],
|
||||
];
|
||||
foreach ($scenarios as $name => $scenario) {
|
||||
$scenario['inScope'] = true;
|
||||
$scenarios[$name . ' in scope'] = $scenario;
|
||||
}
|
||||
return $scenarios;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideIfBlockWithIterable
|
||||
*/
|
||||
public function testIfBlockWithIterable(iterable $iterable, bool $inScope): void
|
||||
{
|
||||
$expected = count($iterable) ? 'has value' : 'no value';
|
||||
$data = new ArrayData(['Iterable' => $iterable]);
|
||||
if ($inScope) {
|
||||
$template = '<% with $Iterable %><% if $Me %>has value<% else %>no value<% end_if %><% end_with %>';
|
||||
} else {
|
||||
$template = '<% if $Iterable %>has value<% else %>no value<% end_if %>';
|
||||
}
|
||||
$this->assertEqualIgnoringWhitespace($expected, $this->render($template, $data));
|
||||
}
|
||||
|
||||
public function testBaseTagGeneration()
|
||||
{
|
||||
// XHTML will have a closed base tag
|
||||
@ -1340,6 +1378,88 @@ after'
|
||||
);
|
||||
}
|
||||
|
||||
public function provideLoop(): array
|
||||
{
|
||||
return [
|
||||
'nested array and iterator' => [
|
||||
'iterable' => [
|
||||
[
|
||||
'value 1',
|
||||
'value 2',
|
||||
],
|
||||
new ArrayList([
|
||||
'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 provideCountIterable(): array
|
||||
{
|
||||
$scenarios = [
|
||||
'empty array' => [
|
||||
'iterable' => [],
|
||||
'inScope' => false,
|
||||
],
|
||||
'array' => [
|
||||
'iterable' => [1, 2, 3],
|
||||
'inScope' => false,
|
||||
],
|
||||
'ArrayList' => [
|
||||
'iterable' => new ArrayList([['Val' => 1], ['Val' => 2], ['Val' => 3]]),
|
||||
'inScope' => false,
|
||||
],
|
||||
];
|
||||
foreach ($scenarios as $name => $scenario) {
|
||||
$scenario['inScope'] = true;
|
||||
$scenarios[$name . ' in scope'] = $scenario;
|
||||
}
|
||||
return $scenarios;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideCountIterable
|
||||
*/
|
||||
public function testCountIterable(iterable $iterable, bool $inScope): void
|
||||
{
|
||||
$expected = count($iterable);
|
||||
$data = new ArrayData(['Iterable' => $iterable]);
|
||||
if ($inScope) {
|
||||
$template = '<% with $Iterable %>$Count<% end_with %>';
|
||||
} else {
|
||||
$template = '$Iterable.Count';
|
||||
}
|
||||
$this->assertEqualIgnoringWhitespace($expected, $this->render($template, $data));
|
||||
}
|
||||
|
||||
public function testSSViewerBasicIteratorSupport()
|
||||
{
|
||||
$data = new ArrayData(
|
||||
@ -2239,4 +2359,11 @@ EOC;
|
||||
$this->render('<% loop $Foo %>$Me<% end_loop %>', $data)
|
||||
);
|
||||
}
|
||||
|
||||
public function testMe(): void
|
||||
{
|
||||
$mockArrayData = $this->getMockBuilder(ArrayData::class)->addMethods(['forTemplate'])->getMock();
|
||||
$mockArrayData->expects($this->once())->method('forTemplate');
|
||||
$this->render('$Me', $mockArrayData);
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ namespace SilverStripe\View\Tests;
|
||||
use ReflectionMethod;
|
||||
use SilverStripe\ORM\FieldType\DBField;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\ORM\ArrayList;
|
||||
use SilverStripe\View\ArrayData;
|
||||
use SilverStripe\View\SSViewer;
|
||||
use SilverStripe\View\Tests\ViewableDataTest\ViewableDataTestExtension;
|
||||
@ -278,4 +279,31 @@ class ViewableDataTest extends SapphireTest
|
||||
$this->assertSame($obj, $viewableData->getDynamicData('abc'));
|
||||
$this->assertSame($obj, $viewableData->abc);
|
||||
}
|
||||
|
||||
public function provideWrapArrayInObj(): array
|
||||
{
|
||||
return [
|
||||
'empty array' => [
|
||||
'arr' => [],
|
||||
'expectedClass' => ArrayList::class,
|
||||
],
|
||||
'fully indexed array' => [
|
||||
'arr' => [
|
||||
'value1',
|
||||
'value2',
|
||||
],
|
||||
'expectedClass' => ArrayList::class,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideWrapArrayInObj
|
||||
*/
|
||||
public function testWrapArrayInObj(array $arr, string $expectedClass): void
|
||||
{
|
||||
$viewableData = new ViewableData();
|
||||
$viewableData->arr = $arr;
|
||||
$this->assertInstanceOf($expectedClass, $viewableData->obj('arr'));
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user