diff --git a/docs/en/topics/datamodel.md b/docs/en/topics/datamodel.md index 479ca6815..dbdad0389 100644 --- a/docs/en/topics/datamodel.md +++ b/docs/en/topics/datamodel.md @@ -471,6 +471,28 @@ the described relations). } } +## Maps + +A map is an array where the array indexes contain data as well as the values. You can build a map +from any DataList like this: + + :::php + $members = DataList::create('Member')->map('ID', 'FirstName'); + +This will return a map where the keys are Member IDs, and the values are the corresponding FirstName +values. Like everything else in the ORM, these maps are lazy loaded, so the following code will only +query a single record from the database: + + :::php + $members = DataList::create('Member')->map('ID', 'FirstName'); + echo $member[5]; + +This functionality is provided by the `SS_Map` class, which can be used to build a map around any `SS_List`. + + :::php + $members = DataList::create('Member'); + $map = new SS_Map($members, 'ID', 'FirstName'); + ## Data Handling When saving data through the object model, you don't have to manually escape strings to create SQL-safe commands. diff --git a/model/DataList.php b/model/DataList.php index ff45d4d9a..42cf14cbe 100644 --- a/model/DataList.php +++ b/model/DataList.php @@ -151,14 +151,8 @@ class DataList extends ViewableData implements SS_List { return $result; } - public function map($keyfield = 'ID', $titlefield = 'Title') { - $map = array(); - - foreach ($this as $item) { - $map[$item->$keyfield] = $item->$titlefield; - } - - return $map; + public function map($keyField = 'ID', $titleField = 'Title') { + return new SS_Map($this, $keyField, $titleField); } /** diff --git a/model/Map.php b/model/Map.php new file mode 100644 index 000000000..cf8c18536 --- /dev/null +++ b/model/Map.php @@ -0,0 +1,206 @@ +list = $list; + $this->keyField = $keyField; + $this->valueField = $valueField; + } + + /** + * Set the key field for this map + */ + function setKeyField($keyField) { + $this->keyField = $keyField; + } + + /** + * Set the value field for this map + */ + function setValueField($valueField) { + $this->valueField = $valueField; + } + + /** + * Return an array equivalent to this map + */ + function toArray() { + $array = array(); + foreach($this as $k => $v) { + $array[$k] = $v; + } + return $array; + } + + /** + * Return all the keys of this map + */ + function keys() { + $array = array(); + foreach($this as $k => $v) { + $array[] = $v; + } + return $array; + } + /** + * Return all the values of this map + */ + function values() { + $array = array(); + foreach($this as $k => $v) { + $array[] = $v; + } + return $array; + } + + /** + * Unshift an item onto the start of the map + */ + function unshift($key, $value) { + $oldItems = $this->firstItems; + $this->firstItems = array($key => $value); + if($oldItems) $this->firstItems = $this->firstItems + $oldItems; + } + + // ArrayAccess + + function offsetExists($key) { + if(isset($this->firstItems[$key])) return true; + + $record = $this->list->find($this->keyField, $key); + return $record != null; + } + function offsetGet($key) { + if(isset($this->firstItems[$key])) return $this->firstItems[$key]; + + $record = $this->list->find($this->keyField, $key); + if($record) { + $col = $this->valueField; + return $record->$col; + } else { + return null; + } + } + function offsetSet($key, $value) { + if(isset($this->firstItems[$key])) return $this->firstItems[$key] = $value; + + user_error("SS_Map is read-only", E_USER_ERROR); + } + function offsetUnset($key) { + if(isset($this->firstItems[$key])) { + unset($this->firstItems[$key]); + return; + } + + user_error("SS_Map is read-only", E_USER_ERROR); + } + + // IteratorAggreagte + + function getIterator() { + return new SS_Map_Iterator($this->list->getIterator(), $this->keyField, $this->valueField, $this->firstItems); + } + + // Countable + + function count() { + return $this->list->count(); + } +} + +/** + * Builds a map iterator around an Iterator. Called by SS_Map + * @package sapphire + * @subpackage model + */ +class SS_Map_Iterator implements Iterator { + protected $items; + protected $keyField, $titleField; + + protected $firstItemIdx = 0; + protected $firstItems = array(); + protected $excludedItems = array(); + + /** + * @param $items The iterator to build this map from + * @param $keyField The field to use for the keys + * @param $titleField The field to use for the values + * @param $fistItems An optional map of items to show first + */ + function __construct(Iterator $items, $keyField, $titleField, $firstItems = null) { + $this->items = $items; + $this->keyField = $keyField; + $this->titleField = $titleField; + + foreach($firstItems as $k => $v) { + $this->firstItems[] = array($k,$v); + $this->excludedItems[] = $k; + } + + } + + // Iterator functions + + public function rewind() { + $this->firstItemIdx = 0; + $rewoundItem = $this->items->rewind(); + + if(isset($this->firstItems[$this->firstItemIdx])) { + return $this->firstItems[$this->firstItemIdx][1]; + } else { + if($rewoundItem) return $rewoundItem->{$this->titleField}; + } + + } + + public function current() { + if(isset($this->firstItems[$this->firstItemIdx])) { + return $this->firstItems[$this->firstItemIdx][1]; + } else { + return $this->items->current()->{$this->titleField}; + } + } + + public function key() { + if(isset($this->firstItems[$this->firstItemIdx])) { + return $this->firstItems[$this->firstItemIdx][0]; + + } else { + return $this->items->current()->{$this->keyField}; + } + } + + public function next() { + $this->firstItemIdx++; + if(isset($this->firstItems[$this->firstItemIdx])) { + return $this->firstItems[$this->firstItemIdx][1]; + + } else { + if(!isset($this->firstItems[$this->firstItemIdx-1])) $this->items->next(); + + if($this->excludedItems) while(($c = $this->items->current()) && in_array($c->{$this->keyField}, $this->excludedItems, true)) { + $this->items->next(); + } + } + } + + public function valid() { + return $this->items->valid(); + } +} diff --git a/model/SQLMap.php b/model/SQLMap.php index cbeda009f..24bc96ee0 100644 --- a/model/SQLMap.php +++ b/model/SQLMap.php @@ -19,6 +19,8 @@ class SQLMap extends Object implements IteratorAggregate { * @param SQLQuery $query The query to generate this map. THis isn't executed until it's needed. */ public function __construct(SQLQuery $query, $keyField = "ID", $titleField = "Title") { + Deprecation::notice('3.0', 'Use SS_Map or DataList::map() instead.'); + if(!$query) { user_error('SQLMap constructed with null query.', E_USER_ERROR); } @@ -52,7 +54,7 @@ class SQLMap extends Object implements IteratorAggregate { public function getIterator() { $this->genItems(); - return new SQLMap_Iterator($this->items->getIterator(), $this->keyField, $this->titleField); + return new SS_Map_Iterator($this->items->getIterator(), $this->keyField, $this->titleField); } /** @@ -85,45 +87,3 @@ class SQLMap extends Object implements IteratorAggregate { } } } - -/** - * @package sapphire - * @subpackage model - */ -class SQLMap_Iterator extends Object implements Iterator { - protected $items; - protected $keyField, $titleField; - - function __construct(Iterator $items, $keyField, $titleField) { - $this->items = $items; - $this->keyField = $keyField; - $this->titleField = $titleField; - } - - - /* - * Iterator functions - necessary for foreach to work - */ - public function rewind() { - return $this->items->rewind() ? $this->items->rewind()->{$this->titleField} : null; - } - - public function current() { - return $this->items->current()->{$this->titleField}; - } - - public function key() { - return $this->items->current()->{$this->keyField}; - } - - public function next() { - $next = $this->items->next(); - return isset($next->{$this->titleField}) ? $next->{$this->titleField} : null; - } - - public function valid() { - return $this->items->valid(); - } -} - -?> \ No newline at end of file diff --git a/tests/model/MapTest.php b/tests/model/MapTest.php new file mode 100644 index 000000000..fae6d1e06 --- /dev/null +++ b/tests/model/MapTest.php @@ -0,0 +1,110 @@ +assertEquals('This is a team comment by Joe', $map['Joe']); + $this->assertNull($map['DoesntExist']); + } + + function testIteration() { + $list = DataList::create("DataObjectTest_TeamComment"); + $map = new SS_Map($list, 'Name', 'Comment'); + $text = ""; + foreach($map as $k => $v) { + $text .= "$k: $v\n"; + } + $this->assertEquals("Joe: This is a team comment by Joe\n" + . "Bob: This is a team comment by Bob\n" + . "Phil: Phil is a unique guy, and comments on team2\n", $text); + } + + function testDefaultConfigIsIDAndTitle() { + $list = DataList::create("DataObjectTest_Team"); + $map = new SS_Map($list); + $this->assertEquals('Team 1', $map[$this->idFromFixture('DataObjectTest_Team', 'team1')]); + } + + function testSetKeyFieldAndValueField() { + $list = DataList::create("DataObjectTest_TeamComment"); + $map = new SS_Map($list); + $map->setKeyField('Name'); + $map->setValueField('Comment'); + $this->assertEquals('This is a team comment by Joe', $map['Joe']); + } + + function testToArray() { + $list = DataList::create("DataObjectTest_TeamComment"); + $map = new SS_Map($list, 'Name', 'Comment'); + $this->assertEquals(array("Joe" => "This is a team comment by Joe", + "Bob" => "This is a team comment by Bob", + "Phil" => "Phil is a unique guy, and comments on team2"), $map->toArray()); + } + + function testUnshift() { + $list = DataList::create("DataObjectTest_TeamComment"); + $map = new SS_Map($list, 'Name', 'Comment'); + + $map->unshift(-1, '(All)'); + + $this->assertEquals(array( + -1 => "(All)", + "Joe" => "This is a team comment by Joe", + "Bob" => "This is a team comment by Bob", + "Phil" => "Phil is a unique guy, and comments on team2"), $map->toArray()); + + $map->unshift(0, '(Select)'); + + $this->assertEquals('(All)', $map[-1]); + $this->assertEquals('(Select)', $map[0]); + + $this->assertEquals(array( + 0 => "(Select)", + -1 => "(All)", + "Joe" => "This is a team comment by Joe", + "Bob" => "This is a team comment by Bob", + "Phil" => "Phil is a unique guy, and comments on team2"), $map->toArray()); + + $map->unshift("Bob","Replaced"); + $this->assertEquals(array( + "Bob" => "Replaced", + 0 => "(Select)", + -1 => "(All)", + "Joe" => "This is a team comment by Joe", + "Phil" => "Phil is a unique guy, and comments on team2"), $map->toArray()); + + $map->unshift("Phil","Replaced as well"); + $this->assertEquals(array( + "Phil" => "Replaced as well", + "Bob" => "Replaced", + 0 => "(Select)", + -1 => "(All)", + "Joe" => "This is a team comment by Joe"), $map->toArray()); + + $map->unshift("Joe","Replaced the last one"); + $this->assertEquals(array( + "Joe" => "Replaced the last one", + "Phil" => "Replaced as well", + "Bob" => "Replaced", + 0 => "(Select)", + -1 => "(All)"), $map->toArray()); + + } + +} \ No newline at end of file