Skip to content
This repository
Browse code

API-CHANGE: moving iterator support from ViewableData to SSViewer. Ne…

…w set of unit tests for iterator support functions.
  • Loading branch information...
commit 28bb83552a2b252056390abcb3df0ceae045f291 1 parent 927dbbe
Hamish Friedlander authored February 11, 2012
3  admin/code/LeftAndMain.php
@@ -532,9 +532,6 @@ public function Breadcrumbs($unlinked = false) {
532 532
 			}
533 533
 		}
534 534
 
535  
-		// TODO Remove once ViewableData->First()/Last() is fixed
536  
-		foreach($items as $i => $item) $item->iteratorProperties($i, $items->Count());
537  
-
538 535
 		return $items;
539 536
 	}
540 537
 	
11  forms/gridfield/GridField.php
@@ -319,8 +319,9 @@ public function FieldHolder() {
319 319
 			}
320 320
 		}
321 321
 
  322
+		$total = $list->count();
  323
+
322 324
 		foreach($list as $idx => $record) {
323  
-			$record->iteratorProperties($idx, $list->count());
324 325
 			$rowContent = '';
325 326
 			foreach($columns as $column) {
326 327
 				$colContent = $this->getColumnContent($record, $column);
@@ -329,10 +330,16 @@ public function FieldHolder() {
329 330
 				$colAttributes = $this->getColumnAttributes($record, $column);
330 331
 				$rowContent .= $this->createTag('td', $colAttributes, $colContent);
331 332
 			}
  333
+
  334
+			$classes = array('ss-gridfield-item');
  335
+			if ($idx == 0) $classes[] = 'first';
  336
+			if ($idx == $total-1) $classes[] = 'last';
  337
+			$classes[] = ($idx % 2) ? 'even' : 'odd';
  338
+
332 339
 			$row = $this->createTag(
333 340
 				'tr', 
334 341
 				array(
335  
-					"class" => 'ss-gridfield-item ' . $record->FirstLast() . " " . $record->EvenOdd(),
  342
+					"class" => implode(' ', $classes),
336 343
 					'data-id' => $record->ID,
337 344
 					// TODO Allow per-row customization similar to GridFieldDefaultColumns
338 345
 					'data-class' => $record->ClassName,
4  forms/gridfield/GridFieldFilter.php
@@ -85,7 +85,7 @@ public function getHTMLFragments($gridField) {
85 85
 				$field = new LiteralField('', '');
86 86
 			}
87 87
 
88  
-			$field->iteratorProperties($currentColumn-1, count($columns));
  88
+			
89 89
 			$forTemplate->Fields->push($field);
90 90
 		}
91 91
 		
@@ -93,4 +93,4 @@ public function getHTMLFragments($gridField) {
93 93
 			'header' => $forTemplate->renderWith('GridFieldFilter_Row'),
94 94
 		);
95 95
 	}
96  
-}
  96
+}
15  tests/core/ArrayDataTest.php
@@ -23,15 +23,6 @@ function testWrappingANonEmptyObjectWorks() {
23 23
 		$this->assertFalse($arrayData->hasField('c'));
24 24
 	}
25 25
 
26  
-	function testWrappingAnEmptyObjectWorks() {
27  
-		$object = (object) array();
28  
-		$this->assertTrue(is_object($object));
29  
-
30  
-		$arrayData = new ArrayData($object);
31  
-
32  
-		$this->assertEquals(null, $arrayData->TotalItems()); // (tobych) Shouldn't we get 0?
33  
-	}
34  
-
35 26
 	function testWrappingAnAssociativeArrayWorks() {
36 27
 		$array = array("A" => "Alpha", "B" => "Beta");
37 28
 		$this->assertTrue(ArrayLib::is_associative($array));
@@ -43,12 +34,6 @@ function testWrappingAnAssociativeArrayWorks() {
43 34
 		$this->assertEquals("Beta", $arrayData->getField("B"));
44 35
 	}
45 36
 
46  
-	function testWrappingAnEmptyArrayWorks() {
47  
-		$arrayData = new ArrayData(array());
48  
-
49  
-		$this->assertEquals(null, $arrayData->TotalItems()); // (tobych) Shouldn't we get 0?
50  
-	}
51  
-
52 37
 	function testRefusesToWrapAnIndexedArray() {
53 38
 		$array = array(0 => "One", 1 => "Two");
54 39
 		$this->assertFalse(ArrayLib::is_associative($array));
157  tests/view/SSViewerTest.php
@@ -97,32 +97,45 @@ function testGlobalVariableCalls() {
97 97
 		$this->assertEquals(i18n::get_locale(), $this->render('{$i18nLocale}'), 'i18n template functions result correct result');
98 98
 		$this->assertEquals(i18n::get_locale(), $this->render('{$get_locale}'), 'i18n template functions result correct result');
99 99
 
100  
-		$this->assertEquals((string)Controller::curr(), $this->render('{$CurrentPage}'), 'i18n template functions result correct result');
101  
-		$this->assertEquals((string)Controller::curr(), $this->render('{$currentPage}'), 'i18n template functions result correct result');
  100
+		//only run this test if we have a controller - i.e. if we are running this test from a web-browser
  101
+		if (Controller::has_curr()) $this->assertEquals((string)Controller::curr(), $this->render('{$CurrentPage}'), 'i18n template functions result correct result');
  102
+		if (Controller::has_curr()) $this->assertEquals((string)Controller::curr(), $this->render('{$currentPage}'), 'i18n template functions result correct result');
102 103
 
103  
-		$this->assertEquals(Member::currentUser(), $this->render('{$CurrentMember}'), 'Member template functions result correct result');
104  
-		$this->assertEquals(Member::currentUser(), $this->render('{$CurrentUser}'), 'Member template functions result correct result');
105  
-		$this->assertEquals(Member::currentUser(), $this->render('{$currentMember}'), 'Member template functions result correct result');
106  
-		$this->assertEquals(Member::currentUser(), $this->render('{$currentUser}'), 'Member template functions result correct result');
  104
+		$this->assertEquals((string)Member::currentUser(), $this->render('{$CurrentMember}'), 'Member template functions result correct result');
  105
+		$this->assertEquals((string)Member::currentUser(), $this->render('{$CurrentUser}'), 'Member template functions result correct result');
  106
+		$this->assertEquals((string)Member::currentUser(), $this->render('{$currentMember}'), 'Member template functions result correct result');
  107
+		$this->assertEquals((string)Member::currentUser(), $this->render('{$currentUser}'), 'Member template functions result correct result');
107 108
 
108 109
 		$this->assertEquals(SecurityToken::getSecurityID(), $this->render('{$getSecurityID}'), 'SecurityToken template functions result correct result');
109 110
 		$this->assertEquals(SecurityToken::getSecurityID(), $this->render('{$SecurityID}'), 'SecurityToken template functions result correct result');
110 111
 	}
111 112
 
112 113
 	function testGlobalVariableCallsWithArguments() {
113  
-		$this->assertEquals(Permission::check("ADMIN"), $this->render('{$HasPerm(\'ADMIN\')}'), 'Permissions template functions result correct result');
114  
-		$this->assertEquals(Permission::check("ADMIN"), $this->render('{$hasPerm(\'ADMIN\')}'), 'Permissions template functions result correct result');
  114
+		$this->assertEquals(Permission::check("ADMIN"), (bool)$this->render('{$HasPerm(\'ADMIN\')}'), 'Permissions template functions result correct result');
  115
+		$this->assertEquals(Permission::check("ADMIN"), (bool)$this->render('{$hasPerm(\'ADMIN\')}'), 'Permissions template functions result correct result');
115 116
 	}
116 117
 
117  
-	/* //TODO: enable this test
118  
-	  function testLocalFunctionsTakePriorityOverGlobals() {
  118
+	function testLocalFunctionsTakePriorityOverGlobals() {
119 119
 		$data = new ArrayData(array(
120 120
 			'Page' => new SSViewerTest_Page()
121 121
 		));
122 122
 
  123
+		//call method with lots of arguments
  124
+		$result = $this->render('<% control Page %>$lotsOfArguments11("a","b","c","d","e","f","g","h","i","j","k")<% end_control %>',$data);
  125
+		$this->assertEquals("abcdefghijk",$result, "Function can accept up to 11 arguments");
  126
+
  127
+		//call method that does not exist
  128
+		$result = $this->render('<% control Page %><% if IDoNotExist %>hello<% end_if %><% end_control %>',$data);
  129
+		$this->assertEquals("",$result, "Method does not exist - empty result");
  130
+
  131
+		//call if that does not exist
  132
+		$result = $this->render('<% control Page %>$IDoNotExist("hello")<% end_control %>',$data);
  133
+		$this->assertEquals("",$result, "Method does not exist - empty result");
  134
+
  135
+		//call method with same name as a global method (local call should take priority)
123 136
 		$result = $this->render('<% control Page %>$absoluteBaseURL<% end_control %>',$data);
124  
-		$this->assertEquals("testPageCalled",$result, "Local Object's function called. Did not return the actual baseURL of the current site");
125  
-	}*/
  137
+		$this->assertEquals("testLocalFunctionPriorityCalled",$result, "Local Object's function called. Did not return the actual baseURL of the current site");
  138
+	}
126 139
 	
127 140
 	function testObjectDotArguments() {
128 141
 		$this->assertEquals(
@@ -376,7 +389,7 @@ function testBaseTagGeneration() {
376 389
 	
377 390
 	function testRecursiveInclude() {
378 391
 		$view = new SSViewer(array('SSViewerTestRecursiveInclude'));
379  
-		
  392
+
380 393
 		$data = new ArrayData(array(
381 394
 			'Title' => 'A',
382 395
 			'Children' => new ArrayList(array(
@@ -462,6 +475,107 @@ function testCastingHelpers() {
462 475
 		);
463 476
 	}
464 477
 	
  478
+	function testSSViewerBasicIteratorSupport() {
  479
+		$data = new ArrayData(array(
  480
+			'Set' => new DataObjectSet(array(
  481
+				new SSViewerTest_Page("1"),
  482
+				new SSViewerTest_Page("2"),
  483
+				new SSViewerTest_Page("3"),
  484
+				new SSViewerTest_Page("4"),
  485
+				new SSViewerTest_Page("5"),
  486
+				new SSViewerTest_Page("6"),
  487
+				new SSViewerTest_Page("7"),
  488
+				new SSViewerTest_Page("8"),
  489
+				new SSViewerTest_Page("9"),
  490
+				new SSViewerTest_Page("10"),
  491
+			))
  492
+		));
  493
+
  494
+		//base test
  495
+		$result = $this->render('<% loop Set %>$Number<% end_loop %>',$data);
  496
+		$this->assertEquals("12345678910",$result,"Numbers rendered in order");
  497
+
  498
+		//test First
  499
+		$result = $this->render('<% loop Set %><% if First %>$Number<% end_if %><% end_loop %>',$data);
  500
+		$this->assertEquals("1",$result,"Only the first number is rendered");
  501
+
  502
+		//test Last
  503
+		$result = $this->render('<% loop Set %><% if Last %>$Number<% end_if %><% end_loop %>',$data);
  504
+		$this->assertEquals("10",$result,"Only the last number is rendered");
  505
+				
  506
+		//test Even
  507
+		$result = $this->render('<% loop Set %><% if Even() %>$Number<% end_if %><% end_loop %>',$data);
  508
+		$this->assertEquals("246810",$result,"Even numbers rendered in order");
  509
+
  510
+		//test Even with quotes
  511
+		$result = $this->render('<% loop Set %><% if Even("1") %>$Number<% end_if %><% end_loop %>',$data);
  512
+		$this->assertEquals("246810",$result,"Even numbers rendered in order");
  513
+
  514
+		//test Even without quotes
  515
+		$result = $this->render('<% loop Set %><% if Even(1) %>$Number<% end_if %><% end_loop %>',$data);
  516
+		$this->assertEquals("246810",$result,"Even numbers rendered in order");
  517
+
  518
+		//test Even with zero-based start index
  519
+		$result = $this->render('<% loop Set %><% if Even("0") %>$Number<% end_if %><% end_loop %>',$data);
  520
+		$this->assertEquals("13579",$result,"Even (with zero-based index) numbers rendered in order");
  521
+
  522
+		//test Odd
  523
+		$result = $this->render('<% loop Set %><% if Odd %>$Number<% end_if %><% end_loop %>',$data);
  524
+		$this->assertEquals("13579",$result,"Odd numbers rendered in order");
  525
+
  526
+		//test FirstLast
  527
+		$result = $this->render('<% loop Set %><% if FirstLast %>$Number$FirstLast<% end_if %><% end_loop %>',$data);
  528
+		$this->assertEquals("1first10last",$result,"First and last numbers rendered in order");
  529
+
  530
+		//test Middle
  531
+		$result = $this->render('<% loop Set %><% if Middle %>$Number<% end_if %><% end_loop %>',$data);
  532
+		$this->assertEquals("23456789",$result,"Middle numbers rendered in order");
  533
+
  534
+		//test MiddleString
  535
+		$result = $this->render('<% loop Set %><% if MiddleString == "middle" %>$Number$MiddleString<% end_if %><% end_loop %>',$data);
  536
+		$this->assertEquals("2middle3middle4middle5middle6middle7middle8middle9middle",$result,"Middle numbers rendered in order");
  537
+
  538
+		//test EvenOdd
  539
+		$result = $this->render('<% loop Set %>$EvenOdd<% end_loop %>',$data);
  540
+		$this->assertEquals("oddevenoddevenoddevenoddevenoddeven",$result,"Even and Odd is returned in sequence numbers rendered in order");
  541
+
  542
+		//test Pos
  543
+		$result = $this->render('<% loop Set %>$Pos<% end_loop %>',$data);
  544
+		$this->assertEquals("12345678910",$result,"Even and Odd is returned in sequence numbers rendered in order");
  545
+
  546
+		//test Total
  547
+		$result = $this->render('<% loop Set %>$TotalItems<% end_loop %>',$data);
  548
+		$this->assertEquals("10101010101010101010",$result,"10 total items X 10 are returned");
  549
+
  550
+		//test Modulus
  551
+		$result = $this->render('<% loop Set %>$Modulus(2,1)<% end_loop %>',$data);
  552
+		$this->assertEquals("1010101010",$result,"1-indexed pos modular divided by 2 rendered in order");
  553
+
  554
+		//test MultipleOf 3
  555
+		$result = $this->render('<% loop Set %><% if MultipleOf(3) %>$Number<% end_if %><% end_loop %>',$data);
  556
+		$this->assertEquals("369",$result,"Only numbers that are multiples of 3 are returned");
  557
+
  558
+		//test MultipleOf 4
  559
+		$result = $this->render('<% loop Set %><% if MultipleOf(4) %>$Number<% end_if %><% end_loop %>',$data);
  560
+		$this->assertEquals("48",$result,"Only numbers that are multiples of 4 are returned");
  561
+
  562
+		//test MultipleOf 5
  563
+		$result = $this->render('<% loop Set %><% if MultipleOf(5) %>$Number<% end_if %><% end_loop %>',$data);
  564
+		$this->assertEquals("510",$result,"Only numbers that are multiples of 5 are returned");
  565
+
  566
+		//test MultipleOf 10
  567
+		$result = $this->render('<% loop Set %><% if MultipleOf(10,1) %>$Number<% end_if %><% end_loop %>',$data);
  568
+		$this->assertEquals("10",$result,"Only numbers that are multiples of 10 (with 1-based indexing) are returned");
  569
+
  570
+		//test MultipleOf 9 zero-based
  571
+		$result = $this->render('<% loop Set %><% if MultipleOf(9,0) %>$Number<% end_if %><% end_loop %>',$data);
  572
+		$this->assertEquals("110",$result,"Only numbers that are multiples of 9 with zero-based indexing are returned. I.e. the first and last item");
  573
+
  574
+		//test MultipleOf 11
  575
+		$result = $this->render('<% loop Set %><% if MultipleOf(11) %>$Number<% end_if %><% end_loop %>',$data);
  576
+		$this->assertEquals("",$result,"Only numbers that are multiples of 11 are returned. I.e. nothing returned");
  577
+	}
  578
+
465 579
 	/**
466 580
 	 * Test $Up works when the scope $Up refers to was entered with a "with" block
467 581
 	 */
@@ -797,7 +911,22 @@ class SSViewerTest_Controller extends Controller {
797 911
 
798 912
 class SSViewerTest_Page extends SiteTree {
799 913
 
  914
+	public $number = null;
  915
+
  916
+	function __construct($number = null) {
  917
+		parent::__construct();
  918
+		$this->number = $number;
  919
+	}
  920
+
  921
+	function Number() {
  922
+		return $this->number;
  923
+	}
  924
+
800 925
 	function absoluteBaseURL() {
801  
-		return "testPageCalled";
  926
+		return "testLocalFunctionPriorityCalled";
  927
+	}
  928
+
  929
+	function lotsOfArguments11($a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k) {
  930
+		return $a. $b. $c. $d. $e. $f. $g. $h. $i. $j. $k;
802 931
 	}
803 932
 }
17  tests/view/ViewableDataTest.php
@@ -120,23 +120,6 @@ public function testCastingClass() {
120 120
 			);
121 121
 		}
122 122
 	}
123  
-
124  
-	function testFirstLast() {
125  
-		$vd = new ViewableData();
126  
-		
127  
-		$vd->iteratorProperties(0, 3);
128  
-		$this->assertEquals('first', $vd->FirstLast());
129  
-
130  
-		$vd->iteratorProperties(1, 3);
131  
-		$this->assertEquals(null, $vd->FirstLast());
132  
-
133  
-		$vd->iteratorProperties(2, 3);
134  
-		$this->assertEquals('last', $vd->FirstLast());
135  
-
136  
-		$vd->iteratorProperties(0, 1);
137  
-		$this->assertEquals('first last', $vd->FirstLast());
138  
-	}
139  
-
140 123
 }
141 124
 
142 125
 /**#@+
223  view/SSViewer.php
@@ -26,15 +26,17 @@ class SSViewer_Scope {
26 26
 	// And array of item, itemIterator, pop_index, up_index, current_index
27 27
 	private $itemStack = array(); 
28 28
 	
29  
-	private $item; // The current "global" item (the one any lookup starts from)
30  
-	private $itemIterator; // If we're looping over the current "global" item, here's the iterator that tracks with item we're up to
  29
+	protected $item; // The current "global" item (the one any lookup starts from)
  30
+	protected $itemIterator; // If we're looping over the current "global" item, here's the iterator that tracks with item we're up to
  31
+	protected $itemIteratorTotal;   //Total number of items in the iterator
31 32
 	
32 33
 	private $popIndex; // A pointer into the item stack for which item should be scope on the next pop call
33 34
 	private $upIndex; // A pointer into the item stack for which item is "up" from this one
34 35
 	private $currentIndex; // A pointer into the item stack for which item is this one (or null if not in stack yet)
35 36
 	
36 37
 	private $localIndex;
37  
-	
  38
+
  39
+
38 40
 	function __construct($item){
39 41
 		$this->item = $item;
40 42
 		$this->localIndex=0;
@@ -100,11 +102,12 @@ function popScope(){
100 102
 	function next(){
101 103
 		if (!$this->item) return false;
102 104
 		
103  
-		if (!$this->itemIterator) { 
  105
+		if (!$this->itemIterator) {
104 106
 			if (is_array($this->item)) $this->itemIterator = new ArrayIterator($this->item);
105 107
 			else $this->itemIterator = $this->item->getIterator();
106 108
 			
107 109
 			$this->itemStack[$this->localIndex][1] = $this->itemIterator;
  110
+			$this->itemIteratorTotal = iterator_count($this->itemIterator); //count the total number of items
108 111
 			$this->itemIterator->rewind();
109 112
 		}
110 113
 		else {
@@ -112,7 +115,7 @@ function next(){
112 115
 		}
113 116
 		
114 117
 		$this->resetLocalScope();
115  
-		
  118
+
116 119
 		if (!$this->itemIterator->valid()) return false;
117 120
 		return $this->itemIterator->key();
118 121
 	}
@@ -126,6 +129,163 @@ function __call($name, $arguments) {
126 129
 	}
127 130
 }
128 131
 
  132
+class SSViewer_BasicIteratorSupport implements TemplateIteratorProvider {
  133
+
  134
+	protected $iteratorPos;
  135
+	protected $iteratorTotalItems;
  136
+
  137
+	public static function getExposedVariables() {
  138
+		return array(
  139
+			'First',
  140
+			'Last',
  141
+			'FirstLast',
  142
+			'Middle',
  143
+			'MiddleString',
  144
+			'Even',
  145
+			'Odd',
  146
+			'EvenOdd',
  147
+			'Pos',
  148
+			'TotalItems',
  149
+			'Modulus',
  150
+			'MultipleOf',
  151
+		);
  152
+	}
  153
+
  154
+	/**
  155
+	 * Set the current iterator properties - where we are on the iterator.
  156
+	 *
  157
+	 * @param int $pos position in iterator
  158
+	 * @param int $totalItems total number of items
  159
+	 */
  160
+	public function iteratorProperties($pos, $totalItems) {
  161
+		$this->iteratorPos        = $pos;
  162
+		$this->iteratorTotalItems = $totalItems;
  163
+	}
  164
+
  165
+	/**
  166
+	 * Returns true if this object is the first in a set.
  167
+	 *
  168
+	 * @return bool
  169
+	 */
  170
+	public function First() {
  171
+		return $this->iteratorPos == 0;
  172
+	}
  173
+
  174
+	/**
  175
+	 * Returns true if this object is the last in a set.
  176
+	 *
  177
+	 * @return bool
  178
+	 */
  179
+	public function Last() {
  180
+		return $this->iteratorPos == $this->iteratorTotalItems - 1;
  181
+	}
  182
+
  183
+	/**
  184
+	 * Returns 'first' or 'last' if this is the first or last object in the set.
  185
+	 *
  186
+	 * @return string|null
  187
+	 */
  188
+	public function FirstLast() {
  189
+		if($this->First() && $this->Last()) return 'first last';
  190
+		if($this->First()) return 'first';
  191
+		if($this->Last())  return 'last';
  192
+	}
  193
+
  194
+	/**
  195
+	 * Return true if this object is between the first & last objects.
  196
+	 *
  197
+	 * @return bool
  198
+	 */
  199
+	public function Middle() {
  200
+		return !$this->First() && !$this->Last();
  201
+	}
  202
+
  203
+	/**
  204
+	 * Return 'middle' if this object is between the first & last objects.
  205
+	 *
  206
+	 * @return string|null
  207
+	 */
  208
+	public function MiddleString() {
  209
+		if($this->Middle()) return 'middle';
  210
+	}
  211
+
  212
+	/**
  213
+	 * Return true if this object is an even item in the set.
  214
+	 * The count starts from $startIndex, which defaults to 1.
  215
+	 *
  216
+	 * @param int $startIndex Number to start count from.
  217
+	 * @return bool
  218
+	 */
  219
+	public function Even($startIndex = 1) {
  220
+		return !$this->Odd($startIndex);
  221
+	}
  222
+
  223
+	/**
  224
+	 * Return true if this is an odd item in the set.
  225
+	 *
  226
+	 * @param int $startIndex Number to start count from.
  227
+	 * @return bool
  228
+	 */
  229
+	public function Odd($startIndex = 1) {
  230
+		return (bool) (($this->iteratorPos+$startIndex) % 2);
  231
+	}
  232
+
  233
+	/**
  234
+	 * Return 'even' or 'odd' if this object is in an even or odd position in the set respectively.
  235
+	 *
  236
+	 * @param int $startIndex Number to start count from.
  237
+	 * @return string
  238
+	 */
  239
+	public function EvenOdd($startIndex = 1) {
  240
+		return ($this->Even($startIndex)) ? 'even' : 'odd';
  241
+	}
  242
+
  243
+	/**
  244
+	 * Return the numerical position of this object in the container set. The count starts at $startIndex.
  245
+	 * The default is the give the position using a 1-based index.
  246
+	 *
  247
+	 * @param int $startIndex Number to start count from.
  248
+	 * @return int
  249
+	 */
  250
+	public function Pos($startIndex = 1) {
  251
+		return $this->iteratorPos + $startIndex;
  252
+	}
  253
+
  254
+	/**
  255
+	 * Return the total number of "sibling" items in the dataset.
  256
+	 *
  257
+	 * @return int
  258
+	 */
  259
+	public function TotalItems() {
  260
+		return $this->iteratorTotalItems;
  261
+	}
  262
+
  263
+	/**
  264
+	 * Returns the modulus of the numerical position of the item in the data set.
  265
+	 * The count starts from $startIndex, which defaults to 1.
  266
+	 * @param int $Mod The number to perform Mod operation to.
  267
+	 * @param int $startIndex Number to start count from.
  268
+	 * @return int
  269
+	 */
  270
+	public function Modulus($mod, $startIndex = 1) {
  271
+		return ($this->iteratorPos + $startIndex) % $mod;
  272
+	}
  273
+
  274
+	/**
  275
+	 * Returns true or false depending on if the pos of the iterator is a multiple of a specific number.
  276
+	 * So, <% if MultipleOf(3) %> would return true on indexes: 3,6,9,12,15, etc. 
  277
+	 * The count starts from $offset, which defaults to 1.
  278
+	 * @param int $factor The multiple of which to return
  279
+	 * @param int $offset Number to start count from.
  280
+	 * @return bool
  281
+	 */
  282
+	public function MultipleOf($factor, $offset = 1) {
  283
+		return (bool) ($this->Modulus($factor, $offset) == 0);
  284
+	}
  285
+
  286
+
  287
+
  288
+}
129 289
 /**
130 290
  * This extends SSViewer_Scope to mix in data on top of what the item provides. This can be "global"
131 291
  * data that is scope-independant (like BaseURL), or type-specific data that is layered on top cross-cut like
@@ -136,27 +296,39 @@ function __call($name, $arguments) {
136 296
 class SSViewer_DataPresenter extends SSViewer_Scope {
137 297
 	
138 298
 	private static $extras = array();
  299
+	private static $iteratorSupport = array();
139 300
 
140 301
 	function __construct($item){
141 302
 		parent::__construct($item);
142 303
 
  304
+		if (count(self::$iteratorSupport) == 0) {   //build up extras array only once per request
  305
+			$this->createCallableArray(self::$iteratorSupport, "TemplateIteratorProvider", true);   //call non-statically
  306
+		}
  307
+
143 308
 		if (count(self::$extras) == 0) {   //build up extras array only once per request
144 309
 			//get all the exposed variables from all classes that implement the TemplateGlobalProvider interface
145  
-			$implementers = ClassInfo::implementorsOf("TemplateGlobalProvider");
  310
+			$this->createCallableArray(self::$extras, "TemplateGlobalProvider");
  311
+		}
  312
+	}
  313
+
  314
+	protected function createCallableArray(&$extraArray, $interfaceToQuery, $createObject = false) {
  315
+		$implementers = ClassInfo::implementorsOf($interfaceToQuery);
  316
+		if ($implementers && count($implementers) > 0) {
146 317
 			foreach($implementers as $implementer) {
  318
+				if ($createObject) $implementer = new $implementer();   //create a new instance of the object for method calls
147 319
 				$exposedVariables = $implementer::getExposedVariables();    //get the exposed variables
148 320
 
149 321
 				foreach($exposedVariables as $varName => $methodName) {
150 322
 					if (!$varName || is_numeric($varName)) $varName = $methodName;  //array has just a single value, use it for both key and value
151 323
 
152 324
 					//e.g. "array(Director, absoluteBaseURL)" means call "Director::absoluteBaseURL()"
153  
-					self::$extras[$varName] = array($implementer, $methodName);
  325
+					$extraArray[$varName] = array($implementer, $methodName);
154 326
 					$firstCharacter = substr($varName, 0, 1);
155 327
 
156 328
 					if ((strtoupper($firstCharacter) === $firstCharacter)) {    //is uppercase, so save the lowercase version, too
157  
-						self::$extras[lcfirst($varName)] = array($implementer, $methodName);    //callable array
  329
+						$extraArray[lcfirst($varName)] = array($implementer, $methodName);    //callable array
158 330
 					} else {    //is lowercase, save a version so it also works uppercase
159  
-						self::$extras[ucfirst($varName)] = array($implementer, $methodName);
  331
+						$extraArray[ucfirst($varName)] = array($implementer, $methodName);
160 332
 					}
161 333
 				}
162 334
 			}
@@ -164,17 +336,40 @@ function __construct($item){
164 336
 	}
165 337
 	
166 338
 	function __call($name, $arguments) {
167  
-		//TODO: make local functions take priority over global functions
  339
+		//extract the method name and parameters
  340
+		$property = $arguments[0];  //the name of the function being called
  341
+		if ($arguments[1]) $params = $arguments[1]; //the function parameters in an array
  342
+		else $params = array();
  343
+
  344
+		//check if the method to-be-called exists on the target object
  345
+		$on = $this->itemIterator ? $this->itemIterator->current() : $this->item;
  346
+		if (method_exists($on, $property)) {    //return the result immediately without trying global functions
  347
+			return parent::__call($name, $arguments);
  348
+		}
168 349
 
169  
-		$property = $arguments[0];
170  
-		if (array_key_exists($property, self::$extras)) {
  350
+		//attempt to call a "global" functions
  351
+		if (array_key_exists($property, self::$extras) || array_key_exists($property, self::$iteratorSupport)) {
171 352
 			$this->resetLocalScope();   //if we are inside a chain (e.g. $A.B.C.Up.E) break out to the beginning of it
172 353
 
173  
-			$value = self::$extras[$property];  //get the method call
  354
+			//special case for the iterator, which need current index and total number of items
  355
+			if (array_key_exists($property, self::$iteratorSupport)) {
  356
+				$value = self::$iteratorSupport[$property];
  357
+
  358
+				if ($this->itemIterator) {
  359
+					// Set the current iterator position and total (the object instance is the first item in the callable array)
  360
+					$value[0]->iteratorProperties($this->itemIterator->key(), $this->itemIteratorTotal);
  361
+				} else {
  362
+					// If we don't actually have an iterator at the moment, act like a list of length 1
  363
+					$value[0]->iteratorProperties(0, 1);
  364
+				}
  365
+			} else {    //normal case of extras call
  366
+				$value = self::$extras[$property];  //get the method call
  367
+			}
174 368
 
175 369
 			//only call callable functions
176 370
 			if (is_callable($value)) {
177  
-				$value = call_user_func_array($value, array_slice($arguments, 1));
  371
+				//$value = call_user_func_array($value, array_slice($arguments, 1));
  372
+				$value = call_user_func_array($value, $params);
178 373
 			}
179 374
 
180 375
 			switch ($name) {
29  view/TemplateIteratorProvider.php
... ...
@@ -0,0 +1,29 @@
  1
+<?php
  2
+/**
  3
+ * Interface that is implemented by any classes that want to expose a method that can be called in a template.
  4
+ * SSViewer_BasicIteratorSupport is an example of this. See also @TemplateGlobalProvider
  5
+ * @package sapphire
  6
+ * @subpackage core
  7
+ */
  8
+interface TemplateIteratorProvider {
  9
+	/**
  10
+	 * @abstract
  11
+	 * @return array Returns an array of strings of the method names of methods on the call that should be exposed
  12
+	 * as global variables in the templates. A map (template-variable-name => method-name) can optionally be supplied
  13
+	 * if the template variable name is different from the name of the method to call. The case of the first character
  14
+	 * in the method name called from the template does not matter, although names specified in the map should
  15
+	 * correspond to the actual method name in the relevant class.
  16
+	 * Note that the template renderer must be able to call these methods statically.
  17
+	 */
  18
+	public static function getExposedVariables();
  19
+
  20
+	/**
  21
+	 * Set the current iterator properties - where we are on the iterator.
  22
+	 * @abstract
  23
+	 * @param int $pos position in iterator
  24
+	 * @param int $totalItems total number of items
  25
+	 */
  26
+	public function iteratorProperties($pos, $totalItems);
  27
+}
  28
+
  29
+?>
127  view/ViewableData.php
@@ -40,12 +40,7 @@ class ViewableData extends Object implements IteratorAggregate {
40 40
 	private static $casting_cache = array();
41 41
 	
42 42
 	// -----------------------------------------------------------------------------------------------------------------
43  
-	
44  
-	/**
45  
-	 * @var int
46  
-	 */
47  
-	protected $iteratorPos, $iteratorTotalItems;
48  
-	
  43
+
49 44
 	/**
50 45
 	 * A failover object to attempt to get data from if it is not present on this object.
51 46
 	 *
@@ -510,126 +505,6 @@ public function getIterator() {
510 505
 		return new ArrayIterator(array($this));
511 506
 	}
512 507
 	
513  
-	/** 
514  
-	 * Set the current iterator properties - where we are on the iterator.
515  
-	 *
516  
-	 * @param int $pos position in iterator
517  
-	 * @param int $totalItems total number of items
518  
-	 */
519  
-	public function iteratorProperties($pos, $totalItems) {
520  
-		$this->iteratorPos        = $pos;
521  
-		$this->iteratorTotalItems = $totalItems;
522  
-	}
523  
-	
524  
-	/**
525  
-	 * Returns true if this object is the first in a set.
526  
-	 *
527  
-	 * @return bool
528  
-	 */
529  
-	public function First() {
530  
-		return $this->iteratorPos == 0;
531  
-	}
532  
-	
533  
-	/**
534  
-	 * Returns true if this object is the last in a set.
535  
-	 *
536  
-	 * @return bool
537  
-	 */
538  
-	public function Last() {
539  
-		return $this->iteratorPos == $this->iteratorTotalItems - 1;
540  
-	}
541  
-	
542  
-	/**
543  
-	 * Returns 'first' or 'last' if this is the first or last object in the set.
544  
-	 *
545  
-	 * @return string|null
546  
-	 */
547  
-	public function FirstLast() {
548  
-		if($this->First() && $this->Last()) return 'first last';
549  
-		if($this->First()) return 'first';
550  
-		if($this->Last())  return 'last';
551  
-	}
552  
-	
553  
-	/**
554  
-	 * Return true if this object is between the first & last objects.
555  
-	 *
556  
-	 * @return bool
557  
-	 */
558  
-	public function Middle() {
559  
-		return !$this->First() && !$this->Last();
560  
-	}
561  
-	
562  
-	/**
563  
-	 * Return 'middle' if this object is between the first & last objects.
564  
-	 *
565  
-	 * @return string|null
566  
-	 */
567  
-	public function MiddleString() {
568  
-		if($this->Middle()) return 'middle';
569  
-	}
570  
-	
571  
-	/**
572  
-	 * Return true if this object is an even item in the set.
573  
-	 *
574  
-	 * @return bool
575  
-	 */
576  
-	public function Even() {
577  
-		return (bool) ($this->iteratorPos % 2);
578  
-	}
579  
-	
580  
-	/**
581  
-	 * Return true if this is an odd item in the set.
582  
-	 *
583  
-	 * @return bool
584  
-	 */
585  
-	public function Odd() {
586  
-		return !$this->Even();
587  
-	}
588  
-	
589  
-	/**
590  
-	 * Return 'even' or 'odd' if this object is in an even or odd position in the set respectively.
591  
-	 *
592  
-	 * @return string
593  
-	 */
594  
-	public function EvenOdd() {
595  
-		return ($this->Even()) ? 'even' : 'odd';
596  
-	}
597  
-	
598  
-	/**
599  
-	 * Return the numerical position of this object in the container set. The count starts at $startIndex.
600  
-	 *
601  
-	 * @param int $startIndex Number to start count from.
602  
-	 * @return int
603  
-	 */
604  
-	public function Pos($startIndex = 1) {
605  
-		return $this->iteratorPos + $startIndex;
606  
-	}
607  
-	
608  
-	/**
609  
-	 * Return the total number of "sibling" items in the dataset.
610  
-	 *
611  
-	 * @return int
612  
-	 */
613  
-	public function TotalItems() {
614  
-		return $this->iteratorTotalItems;
615  
-	}
616  
-
617  
-	/**
618  
-	 * Returns the modulus of the numerical position of the item in the data set.
619  
-	 * The count starts from $startIndex, which defaults to 1.
620  
-	 * @param int $Mod The number to perform Mod operation to.
621  
-	 * @param int $startIndex Number to start count from.
622  
-	 * @return int
623  
-	 */
624  
-	public function Modulus($mod, $startIndex = 1) {
625  
-		return ($this->iteratorPos + $startIndex) % $mod;
626  
-	}
627  
-	
628  
-	public function MultipleOf($factor, $offset = 1) {
629  
-		return ($this->Modulus($factor, $offset) == 0);
630  
-	}
631  
-
632  
-
633 508
 	// UTILITY METHODS -------------------------------------------------------------------------------------------------
634 509
 	
635 510
 	/**

0 notes on commit 28bb835

Please sign in to comment.
Something went wrong with that request. Please try again.