API-CHANGE: moving iterator support from ViewableData to SSViewer. New set of unit tests for iterator support functions.

This commit is contained in:
Hamish Friedlander 2012-02-11 15:26:26 +13:00
parent 927dbbe717
commit 28bb83552a
9 changed files with 393 additions and 193 deletions

View File

@ -532,9 +532,6 @@ class LeftAndMain extends Controller {
} }
} }
// TODO Remove once ViewableData->First()/Last() is fixed
foreach($items as $i => $item) $item->iteratorProperties($i, $items->Count());
return $items; return $items;
} }

View File

@ -319,8 +319,9 @@ class GridField extends FormField {
} }
} }
$total = $list->count();
foreach($list as $idx => $record) { foreach($list as $idx => $record) {
$record->iteratorProperties($idx, $list->count());
$rowContent = ''; $rowContent = '';
foreach($columns as $column) { foreach($columns as $column) {
$colContent = $this->getColumnContent($record, $column); $colContent = $this->getColumnContent($record, $column);
@ -329,10 +330,16 @@ class GridField extends FormField {
$colAttributes = $this->getColumnAttributes($record, $column); $colAttributes = $this->getColumnAttributes($record, $column);
$rowContent .= $this->createTag('td', $colAttributes, $colContent); $rowContent .= $this->createTag('td', $colAttributes, $colContent);
} }
$classes = array('ss-gridfield-item');
if ($idx == 0) $classes[] = 'first';
if ($idx == $total-1) $classes[] = 'last';
$classes[] = ($idx % 2) ? 'even' : 'odd';
$row = $this->createTag( $row = $this->createTag(
'tr', 'tr',
array( array(
"class" => 'ss-gridfield-item ' . $record->FirstLast() . " " . $record->EvenOdd(), "class" => implode(' ', $classes),
'data-id' => $record->ID, 'data-id' => $record->ID,
// TODO Allow per-row customization similar to GridFieldDefaultColumns // TODO Allow per-row customization similar to GridFieldDefaultColumns
'data-class' => $record->ClassName, 'data-class' => $record->ClassName,

View File

@ -85,7 +85,7 @@ class GridFieldFilter implements GridField_HTMLProvider, GridField_DataManipulat
$field = new LiteralField('', ''); $field = new LiteralField('', '');
} }
$field->iteratorProperties($currentColumn-1, count($columns));
$forTemplate->Fields->push($field); $forTemplate->Fields->push($field);
} }
@ -93,4 +93,4 @@ class GridFieldFilter implements GridField_HTMLProvider, GridField_DataManipulat
'header' => $forTemplate->renderWith('GridFieldFilter_Row'), 'header' => $forTemplate->renderWith('GridFieldFilter_Row'),
); );
} }
} }

View File

@ -23,15 +23,6 @@ class ArrayDataTest extends SapphireTest {
$this->assertFalse($arrayData->hasField('c')); $this->assertFalse($arrayData->hasField('c'));
} }
function testWrappingAnEmptyObjectWorks() {
$object = (object) array();
$this->assertTrue(is_object($object));
$arrayData = new ArrayData($object);
$this->assertEquals(null, $arrayData->TotalItems()); // (tobych) Shouldn't we get 0?
}
function testWrappingAnAssociativeArrayWorks() { function testWrappingAnAssociativeArrayWorks() {
$array = array("A" => "Alpha", "B" => "Beta"); $array = array("A" => "Alpha", "B" => "Beta");
$this->assertTrue(ArrayLib::is_associative($array)); $this->assertTrue(ArrayLib::is_associative($array));
@ -43,12 +34,6 @@ class ArrayDataTest extends SapphireTest {
$this->assertEquals("Beta", $arrayData->getField("B")); $this->assertEquals("Beta", $arrayData->getField("B"));
} }
function testWrappingAnEmptyArrayWorks() {
$arrayData = new ArrayData(array());
$this->assertEquals(null, $arrayData->TotalItems()); // (tobych) Shouldn't we get 0?
}
function testRefusesToWrapAnIndexedArray() { function testRefusesToWrapAnIndexedArray() {
$array = array(0 => "One", 1 => "Two"); $array = array(0 => "One", 1 => "Two");
$this->assertFalse(ArrayLib::is_associative($array)); $this->assertFalse(ArrayLib::is_associative($array));

View File

@ -97,32 +97,45 @@ SS
$this->assertEquals(i18n::get_locale(), $this->render('{$i18nLocale}'), 'i18n template functions result correct result'); $this->assertEquals(i18n::get_locale(), $this->render('{$i18nLocale}'), 'i18n template functions result correct result');
$this->assertEquals(i18n::get_locale(), $this->render('{$get_locale}'), 'i18n template functions result correct result'); $this->assertEquals(i18n::get_locale(), $this->render('{$get_locale}'), 'i18n template functions result correct result');
$this->assertEquals((string)Controller::curr(), $this->render('{$CurrentPage}'), 'i18n template functions result correct result'); //only run this test if we have a controller - i.e. if we are running this test from a web-browser
$this->assertEquals((string)Controller::curr(), $this->render('{$currentPage}'), 'i18n template functions result correct result'); if (Controller::has_curr()) $this->assertEquals((string)Controller::curr(), $this->render('{$CurrentPage}'), 'i18n template functions result correct result');
if (Controller::has_curr()) $this->assertEquals((string)Controller::curr(), $this->render('{$currentPage}'), 'i18n template functions result correct result');
$this->assertEquals(Member::currentUser(), $this->render('{$CurrentMember}'), 'Member template functions result correct result'); $this->assertEquals((string)Member::currentUser(), $this->render('{$CurrentMember}'), 'Member template functions result correct result');
$this->assertEquals(Member::currentUser(), $this->render('{$CurrentUser}'), 'Member template functions result correct result'); $this->assertEquals((string)Member::currentUser(), $this->render('{$CurrentUser}'), 'Member template functions result correct result');
$this->assertEquals(Member::currentUser(), $this->render('{$currentMember}'), 'Member template functions result correct result'); $this->assertEquals((string)Member::currentUser(), $this->render('{$currentMember}'), 'Member template functions result correct result');
$this->assertEquals(Member::currentUser(), $this->render('{$currentUser}'), 'Member template functions result correct result'); $this->assertEquals((string)Member::currentUser(), $this->render('{$currentUser}'), 'Member template functions result correct result');
$this->assertEquals(SecurityToken::getSecurityID(), $this->render('{$getSecurityID}'), 'SecurityToken template functions result correct result'); $this->assertEquals(SecurityToken::getSecurityID(), $this->render('{$getSecurityID}'), 'SecurityToken template functions result correct result');
$this->assertEquals(SecurityToken::getSecurityID(), $this->render('{$SecurityID}'), 'SecurityToken template functions result correct result'); $this->assertEquals(SecurityToken::getSecurityID(), $this->render('{$SecurityID}'), 'SecurityToken template functions result correct result');
} }
function testGlobalVariableCallsWithArguments() { function testGlobalVariableCallsWithArguments() {
$this->assertEquals(Permission::check("ADMIN"), $this->render('{$HasPerm(\'ADMIN\')}'), 'Permissions template functions result correct result'); $this->assertEquals(Permission::check("ADMIN"), (bool)$this->render('{$HasPerm(\'ADMIN\')}'), 'Permissions template functions result correct result');
$this->assertEquals(Permission::check("ADMIN"), $this->render('{$hasPerm(\'ADMIN\')}'), 'Permissions template functions result correct result'); $this->assertEquals(Permission::check("ADMIN"), (bool)$this->render('{$hasPerm(\'ADMIN\')}'), 'Permissions template functions result correct result');
} }
/* //TODO: enable this test function testLocalFunctionsTakePriorityOverGlobals() {
function testLocalFunctionsTakePriorityOverGlobals() {
$data = new ArrayData(array( $data = new ArrayData(array(
'Page' => new SSViewerTest_Page() 'Page' => new SSViewerTest_Page()
)); ));
//call method with lots of arguments
$result = $this->render('<% control Page %>$lotsOfArguments11("a","b","c","d","e","f","g","h","i","j","k")<% end_control %>',$data);
$this->assertEquals("abcdefghijk",$result, "Function can accept up to 11 arguments");
//call method that does not exist
$result = $this->render('<% control Page %><% if IDoNotExist %>hello<% end_if %><% end_control %>',$data);
$this->assertEquals("",$result, "Method does not exist - empty result");
//call if that does not exist
$result = $this->render('<% control Page %>$IDoNotExist("hello")<% end_control %>',$data);
$this->assertEquals("",$result, "Method does not exist - empty result");
//call method with same name as a global method (local call should take priority)
$result = $this->render('<% control Page %>$absoluteBaseURL<% end_control %>',$data); $result = $this->render('<% control Page %>$absoluteBaseURL<% end_control %>',$data);
$this->assertEquals("testPageCalled",$result, "Local Object's function called. Did not return the actual baseURL of the current site"); $this->assertEquals("testLocalFunctionPriorityCalled",$result, "Local Object's function called. Did not return the actual baseURL of the current site");
}*/ }
function testObjectDotArguments() { function testObjectDotArguments() {
$this->assertEquals( $this->assertEquals(
@ -376,7 +389,7 @@ after')
function testRecursiveInclude() { function testRecursiveInclude() {
$view = new SSViewer(array('SSViewerTestRecursiveInclude')); $view = new SSViewer(array('SSViewerTestRecursiveInclude'));
$data = new ArrayData(array( $data = new ArrayData(array(
'Title' => 'A', 'Title' => 'A',
'Children' => new ArrayList(array( 'Children' => new ArrayList(array(
@ -462,6 +475,107 @@ after')
); );
} }
function testSSViewerBasicIteratorSupport() {
$data = new ArrayData(array(
'Set' => new DataObjectSet(array(
new SSViewerTest_Page("1"),
new SSViewerTest_Page("2"),
new SSViewerTest_Page("3"),
new SSViewerTest_Page("4"),
new SSViewerTest_Page("5"),
new SSViewerTest_Page("6"),
new SSViewerTest_Page("7"),
new SSViewerTest_Page("8"),
new SSViewerTest_Page("9"),
new SSViewerTest_Page("10"),
))
));
//base test
$result = $this->render('<% loop Set %>$Number<% end_loop %>',$data);
$this->assertEquals("12345678910",$result,"Numbers rendered in order");
//test First
$result = $this->render('<% loop Set %><% if First %>$Number<% end_if %><% end_loop %>',$data);
$this->assertEquals("1",$result,"Only the first number is rendered");
//test Last
$result = $this->render('<% loop Set %><% if Last %>$Number<% end_if %><% end_loop %>',$data);
$this->assertEquals("10",$result,"Only the last number is rendered");
//test Even
$result = $this->render('<% loop Set %><% if Even() %>$Number<% end_if %><% end_loop %>',$data);
$this->assertEquals("246810",$result,"Even numbers rendered in order");
//test Even with quotes
$result = $this->render('<% loop Set %><% if Even("1") %>$Number<% end_if %><% end_loop %>',$data);
$this->assertEquals("246810",$result,"Even numbers rendered in order");
//test Even without quotes
$result = $this->render('<% loop Set %><% if Even(1) %>$Number<% end_if %><% end_loop %>',$data);
$this->assertEquals("246810",$result,"Even numbers rendered in order");
//test Even with zero-based start index
$result = $this->render('<% loop Set %><% if Even("0") %>$Number<% end_if %><% end_loop %>',$data);
$this->assertEquals("13579",$result,"Even (with zero-based index) numbers rendered in order");
//test Odd
$result = $this->render('<% loop Set %><% if Odd %>$Number<% end_if %><% end_loop %>',$data);
$this->assertEquals("13579",$result,"Odd numbers rendered in order");
//test FirstLast
$result = $this->render('<% loop Set %><% if FirstLast %>$Number$FirstLast<% end_if %><% end_loop %>',$data);
$this->assertEquals("1first10last",$result,"First and last numbers rendered in order");
//test Middle
$result = $this->render('<% loop Set %><% if Middle %>$Number<% end_if %><% end_loop %>',$data);
$this->assertEquals("23456789",$result,"Middle numbers rendered in order");
//test MiddleString
$result = $this->render('<% loop Set %><% if MiddleString == "middle" %>$Number$MiddleString<% end_if %><% end_loop %>',$data);
$this->assertEquals("2middle3middle4middle5middle6middle7middle8middle9middle",$result,"Middle numbers rendered in order");
//test EvenOdd
$result = $this->render('<% loop Set %>$EvenOdd<% end_loop %>',$data);
$this->assertEquals("oddevenoddevenoddevenoddevenoddeven",$result,"Even and Odd is returned in sequence numbers rendered in order");
//test Pos
$result = $this->render('<% loop Set %>$Pos<% end_loop %>',$data);
$this->assertEquals("12345678910",$result,"Even and Odd is returned in sequence numbers rendered in order");
//test Total
$result = $this->render('<% loop Set %>$TotalItems<% end_loop %>',$data);
$this->assertEquals("10101010101010101010",$result,"10 total items X 10 are returned");
//test Modulus
$result = $this->render('<% loop Set %>$Modulus(2,1)<% end_loop %>',$data);
$this->assertEquals("1010101010",$result,"1-indexed pos modular divided by 2 rendered in order");
//test MultipleOf 3
$result = $this->render('<% loop Set %><% if MultipleOf(3) %>$Number<% end_if %><% end_loop %>',$data);
$this->assertEquals("369",$result,"Only numbers that are multiples of 3 are returned");
//test MultipleOf 4
$result = $this->render('<% loop Set %><% if MultipleOf(4) %>$Number<% end_if %><% end_loop %>',$data);
$this->assertEquals("48",$result,"Only numbers that are multiples of 4 are returned");
//test MultipleOf 5
$result = $this->render('<% loop Set %><% if MultipleOf(5) %>$Number<% end_if %><% end_loop %>',$data);
$this->assertEquals("510",$result,"Only numbers that are multiples of 5 are returned");
//test MultipleOf 10
$result = $this->render('<% loop Set %><% if MultipleOf(10,1) %>$Number<% end_if %><% end_loop %>',$data);
$this->assertEquals("10",$result,"Only numbers that are multiples of 10 (with 1-based indexing) are returned");
//test MultipleOf 9 zero-based
$result = $this->render('<% loop Set %><% if MultipleOf(9,0) %>$Number<% end_if %><% end_loop %>',$data);
$this->assertEquals("110",$result,"Only numbers that are multiples of 9 with zero-based indexing are returned. I.e. the first and last item");
//test MultipleOf 11
$result = $this->render('<% loop Set %><% if MultipleOf(11) %>$Number<% end_if %><% end_loop %>',$data);
$this->assertEquals("",$result,"Only numbers that are multiples of 11 are returned. I.e. nothing returned");
}
/** /**
* Test $Up works when the scope $Up refers to was entered with a "with" block * Test $Up works when the scope $Up refers to was entered with a "with" block
*/ */
@ -797,7 +911,22 @@ class SSViewerTest_Controller extends Controller {
class SSViewerTest_Page extends SiteTree { class SSViewerTest_Page extends SiteTree {
public $number = null;
function __construct($number = null) {
parent::__construct();
$this->number = $number;
}
function Number() {
return $this->number;
}
function absoluteBaseURL() { function absoluteBaseURL() {
return "testPageCalled"; return "testLocalFunctionPriorityCalled";
}
function lotsOfArguments11($a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k) {
return $a. $b. $c. $d. $e. $f. $g. $h. $i. $j. $k;
} }
} }

View File

@ -120,23 +120,6 @@ class ViewableDataTest extends SapphireTest {
); );
} }
} }
function testFirstLast() {
$vd = new ViewableData();
$vd->iteratorProperties(0, 3);
$this->assertEquals('first', $vd->FirstLast());
$vd->iteratorProperties(1, 3);
$this->assertEquals(null, $vd->FirstLast());
$vd->iteratorProperties(2, 3);
$this->assertEquals('last', $vd->FirstLast());
$vd->iteratorProperties(0, 1);
$this->assertEquals('first last', $vd->FirstLast());
}
} }
/**#@+ /**#@+

View File

@ -26,15 +26,17 @@ class SSViewer_Scope {
// And array of item, itemIterator, pop_index, up_index, current_index // And array of item, itemIterator, pop_index, up_index, current_index
private $itemStack = array(); private $itemStack = array();
private $item; // The current "global" item (the one any lookup starts from) protected $item; // The current "global" item (the one any lookup starts from)
private $itemIterator; // If we're looping over the current "global" item, here's the iterator that tracks with item we're up to protected $itemIterator; // If we're looping over the current "global" item, here's the iterator that tracks with item we're up to
protected $itemIteratorTotal; //Total number of items in the iterator
private $popIndex; // A pointer into the item stack for which item should be scope on the next pop call private $popIndex; // A pointer into the item stack for which item should be scope on the next pop call
private $upIndex; // A pointer into the item stack for which item is "up" from this one private $upIndex; // A pointer into the item stack for which item is "up" from this one
private $currentIndex; // A pointer into the item stack for which item is this one (or null if not in stack yet) private $currentIndex; // A pointer into the item stack for which item is this one (or null if not in stack yet)
private $localIndex; private $localIndex;
function __construct($item){ function __construct($item){
$this->item = $item; $this->item = $item;
$this->localIndex=0; $this->localIndex=0;
@ -100,11 +102,12 @@ class SSViewer_Scope {
function next(){ function next(){
if (!$this->item) return false; if (!$this->item) return false;
if (!$this->itemIterator) { if (!$this->itemIterator) {
if (is_array($this->item)) $this->itemIterator = new ArrayIterator($this->item); if (is_array($this->item)) $this->itemIterator = new ArrayIterator($this->item);
else $this->itemIterator = $this->item->getIterator(); else $this->itemIterator = $this->item->getIterator();
$this->itemStack[$this->localIndex][1] = $this->itemIterator; $this->itemStack[$this->localIndex][1] = $this->itemIterator;
$this->itemIteratorTotal = iterator_count($this->itemIterator); //count the total number of items
$this->itemIterator->rewind(); $this->itemIterator->rewind();
} }
else { else {
@ -112,7 +115,7 @@ class SSViewer_Scope {
} }
$this->resetLocalScope(); $this->resetLocalScope();
if (!$this->itemIterator->valid()) return false; if (!$this->itemIterator->valid()) return false;
return $this->itemIterator->key(); return $this->itemIterator->key();
} }
@ -126,6 +129,163 @@ class SSViewer_Scope {
} }
} }
class SSViewer_BasicIteratorSupport implements TemplateIteratorProvider {
protected $iteratorPos;
protected $iteratorTotalItems;
public static function getExposedVariables() {
return array(
'First',
'Last',
'FirstLast',
'Middle',
'MiddleString',
'Even',
'Odd',
'EvenOdd',
'Pos',
'TotalItems',
'Modulus',
'MultipleOf',
);
}
/**
* Set the current iterator properties - where we are on the iterator.
*
* @param int $pos position in iterator
* @param int $totalItems total number of items
*/
public function iteratorProperties($pos, $totalItems) {
$this->iteratorPos = $pos;
$this->iteratorTotalItems = $totalItems;
}
/**
* Returns true if this object is the first in a set.
*
* @return bool
*/
public function First() {
return $this->iteratorPos == 0;
}
/**
* Returns true if this object is the last in a set.
*
* @return bool
*/
public function Last() {
return $this->iteratorPos == $this->iteratorTotalItems - 1;
}
/**
* Returns 'first' or 'last' if this is the first or last object in the set.
*
* @return string|null
*/
public function FirstLast() {
if($this->First() && $this->Last()) return 'first last';
if($this->First()) return 'first';
if($this->Last()) return 'last';
}
/**
* Return true if this object is between the first & last objects.
*
* @return bool
*/
public function Middle() {
return !$this->First() && !$this->Last();
}
/**
* Return 'middle' if this object is between the first & last objects.
*
* @return string|null
*/
public function MiddleString() {
if($this->Middle()) return 'middle';
}
/**
* Return true if this object is an even item in the set.
* The count starts from $startIndex, which defaults to 1.
*
* @param int $startIndex Number to start count from.
* @return bool
*/
public function Even($startIndex = 1) {
return !$this->Odd($startIndex);
}
/**
* Return true if this is an odd item in the set.
*
* @param int $startIndex Number to start count from.
* @return bool
*/
public function Odd($startIndex = 1) {
return (bool) (($this->iteratorPos+$startIndex) % 2);
}
/**
* Return 'even' or 'odd' if this object is in an even or odd position in the set respectively.
*
* @param int $startIndex Number to start count from.
* @return string
*/
public function EvenOdd($startIndex = 1) {
return ($this->Even($startIndex)) ? 'even' : 'odd';
}
/**
* Return the numerical position of this object in the container set. The count starts at $startIndex.
* The default is the give the position using a 1-based index.
*
* @param int $startIndex Number to start count from.
* @return int
*/
public function Pos($startIndex = 1) {
return $this->iteratorPos + $startIndex;
}
/**
* Return the total number of "sibling" items in the dataset.
*
* @return int
*/
public function TotalItems() {
return $this->iteratorTotalItems;
}
/**
* Returns the modulus of the numerical position of the item in the data set.
* The count starts from $startIndex, which defaults to 1.
* @param int $Mod The number to perform Mod operation to.
* @param int $startIndex Number to start count from.
* @return int
*/
public function Modulus($mod, $startIndex = 1) {
return ($this->iteratorPos + $startIndex) % $mod;
}
/**
* Returns true or false depending on if the pos of the iterator is a multiple of a specific number.
* So, <% if MultipleOf(3) %> would return true on indexes: 3,6,9,12,15, etc.
* The count starts from $offset, which defaults to 1.
* @param int $factor The multiple of which to return
* @param int $offset Number to start count from.
* @return bool
*/
public function MultipleOf($factor, $offset = 1) {
return (bool) ($this->Modulus($factor, $offset) == 0);
}
}
/** /**
* This extends SSViewer_Scope to mix in data on top of what the item provides. This can be "global" * This extends SSViewer_Scope to mix in data on top of what the item provides. This can be "global"
* data that is scope-independant (like BaseURL), or type-specific data that is layered on top cross-cut like * data that is scope-independant (like BaseURL), or type-specific data that is layered on top cross-cut like
@ -136,27 +296,39 @@ class SSViewer_Scope {
class SSViewer_DataPresenter extends SSViewer_Scope { class SSViewer_DataPresenter extends SSViewer_Scope {
private static $extras = array(); private static $extras = array();
private static $iteratorSupport = array();
function __construct($item){ function __construct($item){
parent::__construct($item); parent::__construct($item);
if (count(self::$iteratorSupport) == 0) { //build up extras array only once per request
$this->createCallableArray(self::$iteratorSupport, "TemplateIteratorProvider", true); //call non-statically
}
if (count(self::$extras) == 0) { //build up extras array only once per request if (count(self::$extras) == 0) { //build up extras array only once per request
//get all the exposed variables from all classes that implement the TemplateGlobalProvider interface //get all the exposed variables from all classes that implement the TemplateGlobalProvider interface
$implementers = ClassInfo::implementorsOf("TemplateGlobalProvider"); $this->createCallableArray(self::$extras, "TemplateGlobalProvider");
}
}
protected function createCallableArray(&$extraArray, $interfaceToQuery, $createObject = false) {
$implementers = ClassInfo::implementorsOf($interfaceToQuery);
if ($implementers && count($implementers) > 0) {
foreach($implementers as $implementer) { foreach($implementers as $implementer) {
if ($createObject) $implementer = new $implementer(); //create a new instance of the object for method calls
$exposedVariables = $implementer::getExposedVariables(); //get the exposed variables $exposedVariables = $implementer::getExposedVariables(); //get the exposed variables
foreach($exposedVariables as $varName => $methodName) { foreach($exposedVariables as $varName => $methodName) {
if (!$varName || is_numeric($varName)) $varName = $methodName; //array has just a single value, use it for both key and value if (!$varName || is_numeric($varName)) $varName = $methodName; //array has just a single value, use it for both key and value
//e.g. "array(Director, absoluteBaseURL)" means call "Director::absoluteBaseURL()" //e.g. "array(Director, absoluteBaseURL)" means call "Director::absoluteBaseURL()"
self::$extras[$varName] = array($implementer, $methodName); $extraArray[$varName] = array($implementer, $methodName);
$firstCharacter = substr($varName, 0, 1); $firstCharacter = substr($varName, 0, 1);
if ((strtoupper($firstCharacter) === $firstCharacter)) { //is uppercase, so save the lowercase version, too if ((strtoupper($firstCharacter) === $firstCharacter)) { //is uppercase, so save the lowercase version, too
self::$extras[lcfirst($varName)] = array($implementer, $methodName); //callable array $extraArray[lcfirst($varName)] = array($implementer, $methodName); //callable array
} else { //is lowercase, save a version so it also works uppercase } else { //is lowercase, save a version so it also works uppercase
self::$extras[ucfirst($varName)] = array($implementer, $methodName); $extraArray[ucfirst($varName)] = array($implementer, $methodName);
} }
} }
} }
@ -164,17 +336,40 @@ class SSViewer_DataPresenter extends SSViewer_Scope {
} }
function __call($name, $arguments) { function __call($name, $arguments) {
//TODO: make local functions take priority over global functions //extract the method name and parameters
$property = $arguments[0]; //the name of the function being called
if ($arguments[1]) $params = $arguments[1]; //the function parameters in an array
else $params = array();
$property = $arguments[0]; //check if the method to-be-called exists on the target object
if (array_key_exists($property, self::$extras)) { $on = $this->itemIterator ? $this->itemIterator->current() : $this->item;
if (method_exists($on, $property)) { //return the result immediately without trying global functions
return parent::__call($name, $arguments);
}
//attempt to call a "global" functions
if (array_key_exists($property, self::$extras) || array_key_exists($property, self::$iteratorSupport)) {
$this->resetLocalScope(); //if we are inside a chain (e.g. $A.B.C.Up.E) break out to the beginning of it $this->resetLocalScope(); //if we are inside a chain (e.g. $A.B.C.Up.E) break out to the beginning of it
$value = self::$extras[$property]; //get the method call //special case for the iterator, which need current index and total number of items
if (array_key_exists($property, self::$iteratorSupport)) {
$value = self::$iteratorSupport[$property];
if ($this->itemIterator) {
// Set the current iterator position and total (the object instance is the first item in the callable array)
$value[0]->iteratorProperties($this->itemIterator->key(), $this->itemIteratorTotal);
} else {
// If we don't actually have an iterator at the moment, act like a list of length 1
$value[0]->iteratorProperties(0, 1);
}
} else { //normal case of extras call
$value = self::$extras[$property]; //get the method call
}
//only call callable functions //only call callable functions
if (is_callable($value)) { if (is_callable($value)) {
$value = call_user_func_array($value, array_slice($arguments, 1)); //$value = call_user_func_array($value, array_slice($arguments, 1));
$value = call_user_func_array($value, $params);
} }
switch ($name) { switch ($name) {

View File

@ -0,0 +1,29 @@
<?php
/**
* Interface that is implemented by any classes that want to expose a method that can be called in a template.
* SSViewer_BasicIteratorSupport is an example of this. See also @TemplateGlobalProvider
* @package sapphire
* @subpackage core
*/
interface TemplateIteratorProvider {
/**
* @abstract
* @return array Returns an array of strings of the method names of methods on the call that should be exposed
* as global variables in the templates. A map (template-variable-name => method-name) can optionally be supplied
* if the template variable name is different from the name of the method to call. The case of the first character
* in the method name called from the template does not matter, although names specified in the map should
* correspond to the actual method name in the relevant class.
* Note that the template renderer must be able to call these methods statically.
*/
public static function getExposedVariables();
/**
* Set the current iterator properties - where we are on the iterator.
* @abstract
* @param int $pos position in iterator
* @param int $totalItems total number of items
*/
public function iteratorProperties($pos, $totalItems);
}
?>

View File

@ -40,12 +40,7 @@ class ViewableData extends Object implements IteratorAggregate {
private static $casting_cache = array(); private static $casting_cache = array();
// ----------------------------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------------------------
/**
* @var int
*/
protected $iteratorPos, $iteratorTotalItems;
/** /**
* A failover object to attempt to get data from if it is not present on this object. * A failover object to attempt to get data from if it is not present on this object.
* *
@ -510,126 +505,6 @@ class ViewableData extends Object implements IteratorAggregate {
return new ArrayIterator(array($this)); return new ArrayIterator(array($this));
} }
/**
* Set the current iterator properties - where we are on the iterator.
*
* @param int $pos position in iterator
* @param int $totalItems total number of items
*/
public function iteratorProperties($pos, $totalItems) {
$this->iteratorPos = $pos;
$this->iteratorTotalItems = $totalItems;
}
/**
* Returns true if this object is the first in a set.
*
* @return bool
*/
public function First() {
return $this->iteratorPos == 0;
}
/**
* Returns true if this object is the last in a set.
*
* @return bool
*/
public function Last() {
return $this->iteratorPos == $this->iteratorTotalItems - 1;
}
/**
* Returns 'first' or 'last' if this is the first or last object in the set.
*
* @return string|null
*/
public function FirstLast() {
if($this->First() && $this->Last()) return 'first last';
if($this->First()) return 'first';
if($this->Last()) return 'last';
}
/**
* Return true if this object is between the first & last objects.
*
* @return bool
*/
public function Middle() {
return !$this->First() && !$this->Last();
}
/**
* Return 'middle' if this object is between the first & last objects.
*
* @return string|null
*/
public function MiddleString() {
if($this->Middle()) return 'middle';
}
/**
* Return true if this object is an even item in the set.
*
* @return bool
*/
public function Even() {
return (bool) ($this->iteratorPos % 2);
}
/**
* Return true if this is an odd item in the set.
*
* @return bool
*/
public function Odd() {
return !$this->Even();
}
/**
* Return 'even' or 'odd' if this object is in an even or odd position in the set respectively.
*
* @return string
*/
public function EvenOdd() {
return ($this->Even()) ? 'even' : 'odd';
}
/**
* Return the numerical position of this object in the container set. The count starts at $startIndex.
*
* @param int $startIndex Number to start count from.
* @return int
*/
public function Pos($startIndex = 1) {
return $this->iteratorPos + $startIndex;
}
/**
* Return the total number of "sibling" items in the dataset.
*
* @return int
*/
public function TotalItems() {
return $this->iteratorTotalItems;
}
/**
* Returns the modulus of the numerical position of the item in the data set.
* The count starts from $startIndex, which defaults to 1.
* @param int $Mod The number to perform Mod operation to.
* @param int $startIndex Number to start count from.
* @return int
*/
public function Modulus($mod, $startIndex = 1) {
return ($this->iteratorPos + $startIndex) % $mod;
}
public function MultipleOf($factor, $offset = 1) {
return ($this->Modulus($factor, $offset) == 0);
}
// UTILITY METHODS ------------------------------------------------------------------------------------------------- // UTILITY METHODS -------------------------------------------------------------------------------------------------
/** /**