API: <% loop %> and <% with %> only ever create one new scope level

This commit is contained in:
Loz Calver 2018-06-07 15:01:17 +01:00 committed by Guy Sartorelli
parent 3a6c48cddb
commit 47337782a2
No known key found for this signature in database
GPG Key ID: F313E3B9504D496A
3 changed files with 56 additions and 17 deletions

View File

@ -38,6 +38,14 @@ class SSViewer_DataPresenter extends SSViewer_Scope
*/ */
protected $overlay; protected $overlay;
/**
* Flag for whether overlay should be preserved when pushing a new scope
*
* @see SSViewer_DataPresenter::pushScope()
* @var bool
*/
protected $preserveOverlay = false;
/** /**
* Underlay variables. Concede precedence to overlay variables or anything from the current scope * Underlay variables. Concede precedence to overlay variables or anything from the current scope
* *
@ -200,13 +208,16 @@ class SSViewer_DataPresenter extends SSViewer_Scope
public function pushScope() public function pushScope()
{ {
$scope = parent::pushScope(); $scope = parent::pushScope();
$upIndex = $this->getUpIndex(); $upIndex = $this->getUpIndex() ?: 0;
if ($upIndex !== null) {
$itemStack = $this->getItemStack(); $itemStack = $this->getItemStack();
$itemStack[$upIndex][SSViewer_Scope::ITEM_OVERLAY] = $this->overlay; $itemStack[$upIndex][SSViewer_Scope::ITEM_OVERLAY] = $this->overlay;
$this->setItemStack($itemStack); $this->setItemStack($itemStack);
// Remove the overlay when we're changing to a new scope, as values in
// that scope take priority. The exceptions that set this flag are $Up
// and $Top as they require that the new scope inherits the overlay
if (!$this->preserveOverlay) {
$this->overlay = []; $this->overlay = [];
} }
@ -226,7 +237,7 @@ class SSViewer_DataPresenter extends SSViewer_Scope
if ($upIndex !== null) { if ($upIndex !== null) {
$itemStack = $this->getItemStack(); $itemStack = $this->getItemStack();
$this->overlay = $itemStack[$this->getUpIndex()][SSViewer_Scope::ITEM_OVERLAY]; $this->overlay = $itemStack[$upIndex][SSViewer_Scope::ITEM_OVERLAY];
} }
return parent::popScope(); return parent::popScope();
@ -252,11 +263,15 @@ class SSViewer_DataPresenter extends SSViewer_Scope
if ($upIndex === null) { if ($upIndex === null) {
throw new \LogicException('Up called when we\'re already at the top of the scope'); throw new \LogicException('Up called when we\'re already at the top of the scope');
} }
$overlayIndex = $upIndex; // Parent scope $overlayIndex = $upIndex; // Parent scope
$this->preserveOverlay = true; // Preserve overlay
break; break;
case 'Top': case 'Top':
$overlayIndex = 0; // Top-level scope $overlayIndex = 0; // Top-level scope
$this->preserveOverlay = true; // Preserve overlay
break;
default:
$this->preserveOverlay = false;
break; break;
} }

View File

@ -257,6 +257,9 @@ class SSViewer_Scope
$this->popIndex = $this->itemStack[$newLocalIndex][SSViewer_Scope::POP_INDEX] = $this->localIndex; $this->popIndex = $this->itemStack[$newLocalIndex][SSViewer_Scope::POP_INDEX] = $this->localIndex;
$this->localIndex = $newLocalIndex; $this->localIndex = $newLocalIndex;
// $Up now becomes the parent scope - the parent of the current <% loop %> or <% with %>
$this->upIndex = $this->itemStack[$newLocalIndex][SSViewer_Scope::UP_INDEX] = $this->popIndex;
// We normally keep any previous itemIterator around, so local $Up calls reference the right element. But // We normally keep any previous itemIterator around, so local $Up calls reference the right element. But
// once we enter a new global scope, we need to make sure we use a new one // once we enter a new global scope, we need to make sure we use a new one
$this->itemIterator = $this->itemStack[$newLocalIndex][SSViewer_Scope::ITEM_ITERATOR] = null; $this->itemIterator = $this->itemStack[$newLocalIndex][SSViewer_Scope::ITEM_ITERATOR] = null;

View File

@ -1510,21 +1510,21 @@ after'
// Two level with block, up refers to internally referenced Bar // Two level with block, up refers to internally referenced Bar
$this->assertEquals( $this->assertEquals(
'BarFoo', 'BarTop',
$this->render('<% with Foo.Bar %>{$Name}{$Up.Name}<% end_with %>', $data) $this->render('<% with Foo.Bar %>{$Name}{$Up.Name}<% end_with %>', $data)
); );
// Stepping up & back down the scope tree // Stepping up & back down the scope tree
$this->assertEquals( $this->assertEquals(
'BazBarQux', 'BazFooBar',
$this->render('<% with Foo.Bar.Baz %>{$Name}{$Up.Name}{$Up.Qux.Name}<% end_with %>', $data) $this->render('<% with Foo.Bar.Baz %>{$Name}{$Up.Foo.Name}{$Up.Foo.Bar.Name}<% end_with %>', $data)
); );
// Using $Up in a with block // Using $Up in a with block
$this->assertEquals( $this->assertEquals(
'BazBarQux', 'BazTopBar',
$this->render( $this->render(
'<% with Foo.Bar.Baz %>{$Name}<% with $Up %>{$Name}{$Qux.Name}<% end_with %>' '<% with Foo.Bar.Baz %>{$Name}<% with $Up %>{$Name}{$Foo.Bar.Name}<% end_with %>'
. '<% end_with %>', . '<% end_with %>',
$data $data
) )
@ -1532,9 +1532,9 @@ after'
// Stepping up & back down the scope tree with with blocks // Stepping up & back down the scope tree with with blocks
$this->assertEquals( $this->assertEquals(
'BazBarQuxBarBaz', 'BazTopBarTopBaz',
$this->render( $this->render(
'<% with Foo.Bar.Baz %>{$Name}<% with $Up %>{$Name}<% with Qux %>{$Name}<% end_with %>' '<% with Foo.Bar.Baz %>{$Name}<% with $Up %>{$Name}<% with Foo.Bar %>{$Name}<% end_with %>'
. '{$Name}<% end_with %>{$Name}<% end_with %>', . '{$Name}<% end_with %>{$Name}<% end_with %>',
$data $data
) )
@ -1544,16 +1544,37 @@ after'
$this->assertEquals( $this->assertEquals(
'Foo', 'Foo',
$this->render( $this->render(
'<% with Foo.Bar.Baz %><% with Up %><% with Qux %>{$Up.Up.Name}<% end_with %><% end_with %>' '<% with Foo %><% with Bar %><% with Baz %>{$Up.Up.Name}<% end_with %><% end_with %>'
. '<% end_with %>', . '<% end_with %>',
$data $data
) )
); );
// Using $Up.Up, where first $Up points to an Up used in a local scope lookup, should still skip to Foo // Using $Up as part of a lookup chain in <% with %>
$this->assertEquals(
'Top',
$this->render('<% with Foo.Bar.Baz.Up.Qux %>{$Up.Name}<% end_with %>', $data)
);
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Up called when we're already at the top of the scope
*/
public function testTooManyUps()
{
$data = new ArrayData([
'Foo' => new ArrayData([
'Name' => 'Foo',
'Bar' => new ArrayData([
'Name' => 'Bar'
])
])
]);
$this->assertEquals( $this->assertEquals(
'Foo', 'Foo',
$this->render('<% with Foo.Bar.Baz.Up.Qux %>{$Up.Up.Name}<% end_with %>', $data) $this->render('<% with Foo.Bar %>{$Up.Up.Name}<% end_with %>', $data)
); );
} }