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;
}

View File

@ -319,8 +319,9 @@ class GridField extends FormField {
}
}
$total = $list->count();
foreach($list as $idx => $record) {
$record->iteratorProperties($idx, $list->count());
$rowContent = '';
foreach($columns as $column) {
$colContent = $this->getColumnContent($record, $column);
@ -329,10 +330,16 @@ class GridField extends FormField {
$colAttributes = $this->getColumnAttributes($record, $column);
$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(
'tr',
array(
"class" => 'ss-gridfield-item ' . $record->FirstLast() . " " . $record->EvenOdd(),
"class" => implode(' ', $classes),
'data-id' => $record->ID,
// TODO Allow per-row customization similar to GridFieldDefaultColumns
'data-class' => $record->ClassName,

View File

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

View File

@ -23,15 +23,6 @@ class ArrayDataTest extends SapphireTest {
$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() {
$array = array("A" => "Alpha", "B" => "Beta");
$this->assertTrue(ArrayLib::is_associative($array));
@ -43,12 +34,6 @@ class ArrayDataTest extends SapphireTest {
$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() {
$array = array(0 => "One", 1 => "Two");
$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('{$get_locale}'), 'i18n template functions result correct result');
$this->assertEquals((string)Controller::curr(), $this->render('{$CurrentPage}'), '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
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(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(Member::currentUser(), $this->render('{$currentUser}'), 'Member template functions result correct result');
$this->assertEquals((string)Member::currentUser(), $this->render('{$CurrentMember}'), 'Member template functions result correct result');
$this->assertEquals((string)Member::currentUser(), $this->render('{$CurrentUser}'), 'Member template functions result correct result');
$this->assertEquals((string)Member::currentUser(), $this->render('{$currentMember}'), '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('{$SecurityID}'), 'SecurityToken template functions result correct result');
}
function testGlobalVariableCallsWithArguments() {
$this->assertEquals(Permission::check("ADMIN"), $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');
$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(
'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);
$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() {
$this->assertEquals(
@ -376,7 +389,7 @@ after')
function testRecursiveInclude() {
$view = new SSViewer(array('SSViewerTestRecursiveInclude'));
$data = new ArrayData(array(
'Title' => 'A',
'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
*/
@ -797,7 +911,22 @@ class SSViewerTest_Controller extends Controller {
class SSViewerTest_Page extends SiteTree {
public $number = null;
function __construct($number = null) {
parent::__construct();
$this->number = $number;
}
function Number() {
return $this->number;
}
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
private $itemStack = array();
private $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 $item; // The current "global" item (the one any lookup starts from)
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 $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 $localIndex;
function __construct($item){
$this->item = $item;
$this->localIndex=0;
@ -100,11 +102,12 @@ class SSViewer_Scope {
function next(){
if (!$this->item) return false;
if (!$this->itemIterator) {
if (!$this->itemIterator) {
if (is_array($this->item)) $this->itemIterator = new ArrayIterator($this->item);
else $this->itemIterator = $this->item->getIterator();
$this->itemStack[$this->localIndex][1] = $this->itemIterator;
$this->itemIteratorTotal = iterator_count($this->itemIterator); //count the total number of items
$this->itemIterator->rewind();
}
else {
@ -112,7 +115,7 @@ class SSViewer_Scope {
}
$this->resetLocalScope();
if (!$this->itemIterator->valid()) return false;
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"
* 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 {
private static $extras = array();
private static $iteratorSupport = array();
function __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
//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) {
if ($createObject) $implementer = new $implementer(); //create a new instance of the object for method calls
$exposedVariables = $implementer::getExposedVariables(); //get the exposed variables
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
//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);
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
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) {
//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];
if (array_key_exists($property, self::$extras)) {
//check if the method to-be-called exists on the target object
$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
$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
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) {

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();
// -----------------------------------------------------------------------------------------------------------------
/**
* @var int
*/
protected $iteratorPos, $iteratorTotalItems;
/**
* 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));
}
/**
* 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 -------------------------------------------------------------------------------------------------
/**