Merge pull request #10450 from creative-commoners/pulls/5/rescue-master-generators

API rescue master-branch PR: Use Generators for ORM
This commit is contained in:
Steve Boyd 2022-08-29 19:03:47 +12:00 committed by GitHub
commit 9edf3a5ca6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 263 additions and 568 deletions

View File

@ -223,10 +223,6 @@ class GridFieldExportButton extends AbstractGridFieldComponent implements GridFi
// Remove limit as the list may be paginated, we want the full list for the export
$items = $items->limit(null);
// Use Generator in applicable cases to reduce memory consumption
$items = $items instanceof DataList
? $items->getGenerator()
: $items;
/** @var DataObject $item */
foreach ($items as $item) {

View File

@ -2,8 +2,8 @@
namespace SilverStripe\ORM;
use ArrayIterator;
use InvalidArgumentException;
use Iterator;
use LogicException;
use SilverStripe\Dev\Debug;
use SilverStripe\Dev\Deprecation;
@ -103,19 +103,16 @@ class ArrayList extends ViewableData implements SS_List, Filterable, Sortable, L
/**
* Returns an Iterator for this ArrayList.
* This function allows you to use ArrayList in foreach loops
*
* @return ArrayIterator
*/
#[\ReturnTypeWillChange]
public function getIterator()
public function getIterator(): Iterator
{
$items = array_map(
function ($item) {
return is_array($item) ? new ArrayData($item) : $item;
},
$this->items ?? []
);
return new ArrayIterator($items);
foreach ($this->items as $i => $item) {
if (is_array($item)) {
yield new ArrayData($item);
} else {
yield $item;
}
}
}
/**

View File

@ -380,7 +380,7 @@ abstract class DBSchemaManager
if ($dbID && isset($options[$dbID])) {
if (preg_match('/ENGINE=([^\s]*)/', $options[$dbID] ?? '', $alteredEngineMatches)) {
$alteredEngine = $alteredEngineMatches[1];
$tableStatus = $this->query(sprintf('SHOW TABLE STATUS LIKE \'%s\'', $table))->first();
$tableStatus = $this->query(sprintf('SHOW TABLE STATUS LIKE \'%s\'', $table))->record();
$tableOptionsChanged = ($tableStatus['Engine'] != $alteredEngine);
}
}

View File

@ -2,6 +2,8 @@
namespace SilverStripe\ORM\Connect;
use Iterator;
/**
* A result-set from a MySQL database (using MySQLiConnector)
* Note that this class is only used for the results of non-prepared statements
@ -45,16 +47,13 @@ class MySQLQuery extends Query
}
}
public function seek($row)
public function getIterator(): Iterator
{
if (is_object($this->handle)) {
// Fix for https://github.com/silverstripe/silverstripe-framework/issues/9097 without breaking the seek() API
$this->handle->data_seek($row);
$result = $this->nextRecord();
$this->handle->data_seek($row);
return $result;
while ($data = $this->handle->fetch_assoc()) {
yield $data;
}
}
return null;
}
public function numRecords()
@ -62,27 +61,7 @@ class MySQLQuery extends Query
if (is_object($this->handle)) {
return $this->handle->num_rows;
}
return null;
}
public function nextRecord()
{
$floatTypes = [MYSQLI_TYPE_FLOAT, MYSQLI_TYPE_DOUBLE, MYSQLI_TYPE_DECIMAL, MYSQLI_TYPE_NEWDECIMAL];
if (is_object($this->handle) && ($row = $this->handle->fetch_array(MYSQLI_NUM))) {
$data = [];
foreach ($row as $i => $value) {
if (!isset($this->columns[$i])) {
throw new DatabaseException("Can't get metadata for column $i");
}
if (in_array($this->columns[$i]->type, $floatTypes ?? [])) {
$value = (float)$value;
}
$data[$this->columns[$i]->name] = $value;
}
return $data;
} else {
return false;
}
}
}

View File

@ -2,6 +2,7 @@
namespace SilverStripe\ORM\Connect;
use Iterator;
use mysqli_result;
use mysqli_stmt;
@ -56,6 +57,26 @@ class MySQLStatement extends Query
*/
protected $boundValues = [];
/**
* Hook the result-set given into a Query class, suitable for use by SilverStripe.
* @param mysqli_stmt $statement The related statement, if present
* @param mysqli_result $metadata The metadata for this statement
*/
public function __construct($statement, $metadata)
{
$this->statement = $statement;
$this->metadata = $metadata;
// Immediately bind and buffer
$this->bind();
}
public function __destruct()
{
$this->statement->close();
$this->currentRecord = false;
}
/**
* Binds this statement to the variables
*/
@ -82,58 +103,20 @@ class MySQLStatement extends Query
call_user_func_array([$this->statement, 'bind_result'], $variables ?? []);
}
/**
* Hook the result-set given into a Query class, suitable for use by SilverStripe.
* @param mysqli_stmt $statement The related statement, if present
* @param mysqli_result $metadata The metadata for this statement
*/
public function __construct($statement, $metadata)
public function getIterator(): Iterator
{
$this->statement = $statement;
$this->metadata = $metadata;
// Immediately bind and buffer
$this->bind();
}
public function __destruct()
{
$this->statement->close();
$this->currentRecord = false;
}
public function seek($row)
{
$this->rowNum = $row - 1;
// Fix for https://github.com/silverstripe/silverstripe-framework/issues/9097 without breaking the seek() API
$this->statement->data_seek($row);
$result = $this->next();
$this->statement->data_seek($row);
return $result;
while ($this->statement->fetch()) {
// Dereferenced row
$row = [];
foreach ($this->boundValues as $key => $value) {
$row[$key] = $value;
}
yield $row;
}
}
public function numRecords()
{
return $this->statement->num_rows();
}
public function nextRecord()
{
// Skip data if out of data
if (!$this->statement->fetch()) {
return false;
}
// Dereferenced row
$row = [];
foreach ($this->boundValues as $key => $value) {
$floatTypes = [MYSQLI_TYPE_FLOAT, MYSQLI_TYPE_DOUBLE, MYSQLI_TYPE_DECIMAL, MYSQLI_TYPE_NEWDECIMAL];
if (in_array($this->types[$key], $floatTypes ?? [])) {
$value = (float)$value;
}
$row[$key] = $value;
}
return $row;
}
}

View File

@ -2,6 +2,9 @@
namespace SilverStripe\ORM\Connect;
use ArrayIterator;
use Iterator;
/**
* A result-set from a PDO database.
*/
@ -14,7 +17,7 @@ class PDOQuery extends Query
/**
* Hook the result-set given into a Query class, suitable for use by SilverStripe.
* @param PDOStatement $statement The internal PDOStatement containing the results
* @param PDOStatementHandle $statement The internal PDOStatement containing the results
*/
public function __construct(PDOStatementHandle $statement)
{
@ -26,25 +29,13 @@ class PDOQuery extends Query
$statement->closeCursor();
}
public function seek($row)
public function getIterator(): Iterator
{
$this->rowNum = $row - 1;
return $this->nextRecord();
return new ArrayIterator($this->results);
}
public function numRecords()
{
return count($this->results ?? []);
}
public function nextRecord()
{
$index = $this->rowNum + 1;
if (isset($this->results[$index])) {
return $this->results[$index];
} else {
return false;
}
return count($this->results);
}
}

View File

@ -27,30 +27,9 @@ use Iterator;
* on providing the specific data-access methods that are required: {@link nextRecord()}, {@link numRecords()}
* and {@link seek()}
*/
abstract class Query implements Iterator
abstract class Query implements \IteratorAggregate
{
/**
* The current record in the iterator.
*
* @var array
*/
protected $currentRecord = null;
/**
* The number of the current row in the iterator.
*
* @var int
*/
protected $rowNum = -1;
/**
* Flag to keep track of whether iteration has begun, to prevent unnecessary seeks
*
* @var bool
*/
protected $queryHasBegun = false;
/**
* Return an array containing all the values from a specific column. If no column is set, then the first will be
* returned
@ -62,7 +41,7 @@ abstract class Query implements Iterator
{
$result = [];
while ($record = $this->next()) {
foreach ($this as $record) {
if ($column) {
$result[] = $record[$column];
} else {
@ -82,6 +61,7 @@ abstract class Query implements Iterator
public function keyedColumn()
{
$column = [];
foreach ($this as $record) {
$val = $record[key($record)];
$column[$val] = $val;
@ -106,13 +86,22 @@ abstract class Query implements Iterator
}
/**
* Returns the next record in the iterator.
* Returns the first record in the result
*
* @return array
*/
public function record()
{
return $this->next();
return $this->getIterator()->current();
}
/**
* @deprecated Use record() instead
* @return array
*/
public function first()
{
return $this->record();
}
/**
@ -122,7 +111,7 @@ abstract class Query implements Iterator
*/
public function value()
{
$record = $this->next();
$record = $this->record();
if ($record) {
return $record[key($record)];
}
@ -164,94 +153,10 @@ abstract class Query implements Iterator
return $result;
}
/**
* Iterator function implementation. Rewind the iterator to the first item and return it.
* Makes use of {@link seek()} and {@link numRecords()}, takes care of the plumbing.
*
* @return void
*/
#[\ReturnTypeWillChange]
public function rewind()
{
if ($this->queryHasBegun && $this->numRecords() > 0) {
$this->queryHasBegun = false;
$this->currentRecord = null;
$this->seek(0);
}
}
/**
* Iterator function implementation. Return the current item of the iterator.
*
* @return array
*/
#[\ReturnTypeWillChange]
public function current()
{
if (!$this->currentRecord) {
return $this->next();
} else {
return $this->currentRecord;
}
}
/**
* Iterator function implementation. Return the first item of this iterator.
*
* @return array
*/
public function first()
{
$this->rewind();
return $this->current();
}
/**
* Iterator function implementation. Return the row number of the current item.
*
* @return int
*/
#[\ReturnTypeWillChange]
public function key()
{
return $this->rowNum;
}
/**
* Iterator function implementation. Return the next record in the iterator.
* Makes use of {@link nextRecord()}, takes care of the plumbing.
*
* @return array
*/
#[\ReturnTypeWillChange]
public function next()
{
$this->queryHasBegun = true;
$this->currentRecord = $this->nextRecord();
$this->rowNum++;
return $this->currentRecord;
}
/**
* Iterator function implementation. Check if the iterator is pointing to a valid item.
*
* @return bool
*/
#[\ReturnTypeWillChange]
public function valid()
{
if (!$this->queryHasBegun) {
$this->next();
}
return $this->currentRecord !== false;
}
/**
* Return the next record in the query result.
*
* @return array
*/
abstract public function nextRecord();
abstract public function getIterator(): Iterator;
/**
* Return the total number of items in the query result.
@ -259,12 +164,4 @@ abstract class Query implements Iterator
* @return int
*/
abstract public function numRecords();
/**
* Go to a specific row number in the query result and return the record.
*
* @param int $rowNum Row number to go to.
* @return array
*/
abstract public function seek($rowNum);
}

View File

@ -6,10 +6,12 @@ use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\Debug;
use SilverStripe\ORM\Filters\SearchFilter;
use SilverStripe\ORM\Queries\SQLConditionGroup;
use SilverStripe\View\TemplateIterator;
use SilverStripe\View\ViewableData;
use ArrayIterator;
use Exception;
use InvalidArgumentException;
use Iterator;
use LogicException;
/**
@ -49,6 +51,13 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
*/
protected $dataQuery;
/**
* A cached Query to save repeated database calls. {@see DataList::getTemplateIteratorCount()}
*
* @var SilverStripe\ORM\Connect\Query
*/
protected $finalisedQuery;
/**
* Create a new DataList.
* No querying is done on construction, but the initial query schema is set up.
@ -79,6 +88,7 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
public function __clone()
{
$this->dataQuery = clone $this->dataQuery;
$this->finalisedQuery = null;
}
/**
@ -781,20 +791,6 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
return $this;
}
/**
* Returns a generator for this DataList
*
* @return \Generator&DataObject[]
*/
public function getGenerator()
{
$query = $this->dataQuery->query()->execute();
while ($row = $query->record()) {
yield $this->createDataObject($row);
}
}
public function debug()
{
$val = "<h2>" . static::class . "</h2><ul>";
@ -863,13 +859,32 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
/**
* Returns an Iterator for this DataList.
* This function allows you to use DataLists in foreach loops
*
* @return ArrayIterator
*/
#[\ReturnTypeWillChange]
public function getIterator()
public function getIterator(): Iterator
{
return new ArrayIterator($this->toArray());
foreach ($this->getFinalisedQuery() as $row) {
yield $this->createDataObject($row);
}
// Re-set the finaliseQuery so that it can be re-executed
$this->finalisedQuery = null;
}
/**
* Returns the Query result for this DataList. Repeated calls will return
* a cached result, unless the DataQuery underlying this list has been
* modified
*
* @return SilverStripe\ORM\Connect\Query
* @internal This API may change in minor releases
*/
protected function getFinalisedQuery()
{
if (!$this->finalisedQuery) {
$this->finalisedQuery = $this->dataQuery->query()->execute();
}
return $this->finalisedQuery;
}
/**
@ -880,6 +895,10 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
#[\ReturnTypeWillChange]
public function count()
{
if ($this->finalisedQuery) {
return $this->finalisedQuery->numRecords();
}
return $this->dataQuery->count();
}
@ -1027,8 +1046,7 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
*/
public function column($colName = "ID")
{
$dataQuery = clone $this->dataQuery;
return $dataQuery->distinct(false)->column($colName);
return $this->dataQuery->distinct(false)->column($colName);
}
/**
@ -1174,7 +1192,7 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
*/
public function removeAll()
{
foreach ($this->getGenerator() as $item) {
foreach ($this as $item) {
$this->remove($item);
}
return $this;
@ -1317,14 +1335,15 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
$currentChunk = 0;
// Keep looping until we run out of chunks
while ($chunk = $this->limit($chunkSize, $chunkSize * $currentChunk)->getIterator()) {
while ($chunk = $this->limit($chunkSize, $chunkSize * $currentChunk)) {
// Loop over all the item in our chunk
$count = 0;
foreach ($chunk as $item) {
$count++;
yield $item;
}
if ($chunk->count() < $chunkSize) {
if ($count < $chunkSize) {
// If our last chunk had less item than our chunkSize, we've reach the end.
break;
}

View File

@ -2,6 +2,7 @@
namespace SilverStripe\ORM;
use Iterator;
use SilverStripe\View\ViewableData;
use LogicException;
@ -96,8 +97,7 @@ abstract class ListDecorator extends ViewableData implements SS_List, Sortable,
$this->list->remove($itemObject);
}
#[\ReturnTypeWillChange]
public function getIterator()
public function getIterator(): Iterator
{
return $this->list->getIterator();
}

View File

@ -521,7 +521,7 @@ class ManyManyList extends RelationList
$query->addWhere([
"\"{$this->joinTable}\".\"{$this->localKey}\"" => $itemID
]);
$queryResult = $query->execute()->current();
$queryResult = $query->execute()->record();
if ($queryResult) {
foreach ($queryResult as $fieldName => $value) {
$result[$fieldName] = $value;

View File

@ -4,6 +4,7 @@ namespace SilverStripe\ORM;
use ArrayAccess;
use Countable;
use Iterator;
use IteratorAggregate;
/**
@ -250,23 +251,55 @@ class Map implements ArrayAccess, Countable, IteratorAggregate
}
/**
* Returns an Map_Iterator instance for iterating over the complete set
* Returns an Iterator for iterating over the complete set
* of items in the map.
*
* Satisfies the IteratorAggreagte interface.
*
* @return Map_Iterator
* Satisfies the IteratorAggregate interface.
*/
#[\ReturnTypeWillChange]
public function getIterator()
public function getIterator(): Iterator
{
return new Map_Iterator(
$this->list->getIterator(),
$this->keyField,
$this->valueField,
$this->firstItems,
$this->lastItems
);
$keyField = $this->keyField;
$valueField = $this->valueField;
foreach ($this->firstItems as $k => $v) {
yield $k => $v;
}
foreach ($this->list as $record) {
if (isset($this->firstItems[$record->$keyField])) {
continue;
}
if (isset($this->lastItems[$record->$keyField])) {
continue;
}
yield $this->extractValue($record, $this->keyField) => $this->extractValue($record, $this->valueField);
}
foreach ($this->lastItems as $k => $v) {
yield $k => $v;
}
}
/**
* Extracts a value from an item in the list, where the item is either an
* object or array.
*
* @param array|object $item
* @param string $key
* @return mixed
*/
protected function extractValue($item, $key)
{
if (is_object($item)) {
if (method_exists($item, 'hasMethod') && $item->hasMethod($key)) {
return $item->{$key}();
}
return $item->{$key};
} else {
if (array_key_exists($key, $item)) {
return $item[$key];
}
}
}
/**

View File

@ -1,200 +0,0 @@
<?php
namespace SilverStripe\ORM;
use Iterator;
/**
* Builds a map iterator around an Iterator. Called by Map
*/
class Map_Iterator implements Iterator
{
/**
* @var Iterator
**/
protected $items;
protected $keyField, $titleField;
protected $firstItemIdx = 0;
protected $endItemIdx;
protected $firstItems = [];
protected $lastItems = [];
protected $excludedItems = [];
/**
* @param Iterator $items The iterator to build this map from
* @param string $keyField The field to use for the keys
* @param string $titleField The field to use for the values
* @param array $firstItems An optional map of items to show first
* @param array $lastItems An optional map of items to show last
*/
public function __construct(Iterator $items, $keyField, $titleField, $firstItems = null, $lastItems = null)
{
$this->items = $items;
$this->keyField = $keyField;
$this->titleField = $titleField;
$this->endItemIdx = null;
if ($firstItems) {
foreach ($firstItems as $k => $v) {
$this->firstItems[] = [$k, $v];
$this->excludedItems[] = $k;
}
}
if ($lastItems) {
foreach ($lastItems as $k => $v) {
$this->lastItems[] = [$k, $v];
$this->excludedItems[] = $k;
}
}
}
/**
* Rewind the Iterator to the first element.
*
* @return mixed
*/
#[\ReturnTypeWillChange]
public function rewind()
{
$this->firstItemIdx = 0;
$this->endItemIdx = null;
$rewoundItem = $this->items->rewind();
if (isset($this->firstItems[$this->firstItemIdx])) {
return $this->firstItems[$this->firstItemIdx][1];
} else {
if ($rewoundItem) {
return $this->extractValue($rewoundItem, $this->titleField);
} else {
if (!$this->items->valid() && $this->lastItems) {
$this->endItemIdx = 0;
return $this->lastItems[0][1];
}
}
}
}
/**
* Return the current element.
*
* @return mixed
*/
#[\ReturnTypeWillChange]
public function current()
{
if (($this->endItemIdx !== null) && isset($this->lastItems[$this->endItemIdx])) {
return $this->lastItems[$this->endItemIdx][1];
} else {
if (isset($this->firstItems[$this->firstItemIdx])) {
return $this->firstItems[$this->firstItemIdx][1];
}
}
return $this->extractValue($this->items->current(), $this->titleField);
}
/**
* Extracts a value from an item in the list, where the item is either an
* object or array.
*
* @param array|object $item
* @param string $key
* @return mixed
*/
protected function extractValue($item, $key)
{
if (is_object($item)) {
if (method_exists($item, 'hasMethod') && $item->hasMethod($key)) {
return $item->{$key}();
}
return $item->{$key};
} else {
if (array_key_exists($key, $item ?? [])) {
return $item[$key];
}
}
}
/**
* Return the key of the current element.
*
* @return string
*/
#[\ReturnTypeWillChange]
public function key()
{
if (($this->endItemIdx !== null) && isset($this->lastItems[$this->endItemIdx])) {
return $this->lastItems[$this->endItemIdx][0];
} else {
if (isset($this->firstItems[$this->firstItemIdx])) {
return $this->firstItems[$this->firstItemIdx][0];
} else {
return $this->extractValue($this->items->current(), $this->keyField);
}
}
}
/**
* Move forward to next element.
*
* @return mixed
*/
#[\ReturnTypeWillChange]
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();
}
}
}
if (!$this->items->valid()) {
// iterator has passed the preface items, off the end of the items
// list. Track through the end items to go through to the next
if ($this->endItemIdx === null) {
$this->endItemIdx = -1;
}
$this->endItemIdx++;
if (isset($this->lastItems[$this->endItemIdx])) {
return $this->lastItems[$this->endItemIdx];
}
return false;
}
}
/**
* Checks if current position is valid.
*
* @return boolean
*/
#[\ReturnTypeWillChange]
public function valid()
{
return (
(isset($this->firstItems[$this->firstItemIdx])) ||
(($this->endItemIdx !== null) && isset($this->lastItems[$this->endItemIdx])) ||
$this->items->valid()
);
}
}

View File

@ -8,6 +8,7 @@ use SilverStripe\ORM\Queries\SQLSelect;
use SilverStripe\View\ArrayData;
use ArrayAccess;
use Exception;
use Iterator;
use IteratorIterator;
/**
@ -208,11 +209,7 @@ class PaginatedList extends ListDecorator
return $this;
}
/**
* @return IteratorIterator
*/
#[\ReturnTypeWillChange]
public function getIterator()
public function getIterator(): Iterator
{
$pageLength = $this->getPageLength();
if ($this->limitItems && $pageLength) {
@ -225,6 +222,21 @@ class PaginatedList extends ListDecorator
}
}
/**
* @return array
*/
public function toArray()
{
$result = [];
// Use getIterator()
foreach ($this as $record) {
$result[] = $record;
}
return $result;
}
/**
* Returns a set of links to all the pages in the list. This is useful for
* basic pagination.
@ -344,8 +356,8 @@ class PaginatedList extends ListDecorator
$num = $i + 1;
$emptyRange = $num != 1 && $num != $total && (
$num == $left - 1 || $num == $right + 1
);
$num == $left - 1 || $num == $right + 1
);
if ($emptyRange) {
$result->push(new ArrayData([

View File

@ -4,6 +4,7 @@ namespace SilverStripe\ORM;
use InvalidArgumentException;
use ArrayIterator;
use Iterator;
use SilverStripe\ORM\FieldType\DBField;
/**
@ -120,11 +121,8 @@ class UnsavedRelationList extends ArrayList implements Relation
/**
* Returns an Iterator for this relation.
*
* @return ArrayIterator
*/
#[\ReturnTypeWillChange]
public function getIterator()
public function getIterator(): Iterator
{
return new ArrayIterator($this->toArray());
}

View File

@ -3,6 +3,7 @@
namespace SilverStripe\View;
use ArrayIterator;
use Countable;
use Iterator;
/**
@ -289,16 +290,31 @@ class SSViewer_Scope
}
if (!$this->itemIterator) {
// Note: it is important that getIterator() is called before count() as implemenations may rely on
// this to efficiency get both the number of records and an iterator (e.g. DataList does this)
// Item may be an array or a regular IteratorAggregate
if (is_array($this->item)) {
$this->itemIterator = new ArrayIterator($this->item);
} else {
$this->itemIterator = $this->item->getIterator();
// This will execute code in a generator up to the first yield. For example, this ensures that
// DataList::getIterator() is called before Datalist::count()
$this->itemIterator->rewind();
}
// If the item implements Countable, use that to fetch the count, otherwise we have to inspect the
// iterator and then rewind it.
if ($this->item instanceof Countable) {
$this->itemIteratorTotal = count($this->item);
} else {
$this->itemIteratorTotal = iterator_count($this->itemIterator);
$this->itemIterator->rewind();
}
$this->itemStack[$this->localIndex][SSViewer_Scope::ITEM_ITERATOR] = $this->itemIterator;
$this->itemIteratorTotal = iterator_count($this->itemIterator); // Count the total number of items
$this->itemStack[$this->localIndex][SSViewer_Scope::ITEM_ITERATOR_TOTAL] = $this->itemIteratorTotal;
$this->itemIterator->rewind();
} else {
$this->itemIterator->next();
}

View File

@ -5,6 +5,7 @@ namespace SilverStripe\View;
use ArrayIterator;
use Exception;
use InvalidArgumentException;
use Iterator;
use IteratorAggregate;
use LogicException;
use SilverStripe\Core\ClassInfo;
@ -573,11 +574,8 @@ class ViewableData implements IteratorAggregate
*
* This is useful so you can use a single record inside a <% control %> block in a template - and then use
* to access individual fields on this object.
*
* @return ArrayIterator
*/
#[\ReturnTypeWillChange]
public function getIterator()
public function getIterator(): Iterator
{
return new ArrayIterator([$this]);
}

View File

@ -74,7 +74,7 @@ class DatabaseTest extends SapphireTest
'SHOW TABLE STATUS WHERE "Name" = \'%s\'',
'DatabaseTest_MyObject'
)
)->first();
)->record();
$this->assertEquals(
$ret['Engine'],
'InnoDB',

View File

@ -2,6 +2,7 @@
namespace SilverStripe\ORM\Tests;
use ArrayIterator;
use LogicException;
use PHPUnit\Framework\MockObject\MockObject;
use SilverStripe\Dev\SapphireTest;
@ -34,8 +35,9 @@ class ListDecoratorTest extends SapphireTest
public function testGetIterator()
{
$this->list->expects($this->once())->method('getIterator')->willReturn('mock');
$this->assertSame('mock', $this->decorator->getIterator());
$iterator = new ArrayIterator();
$this->list->expects($this->once())->method('getIterator')->willReturn($iterator);
$this->assertSame($iterator, $this->decorator->getIterator());
}
public function testCanSortBy()

View File

@ -45,58 +45,47 @@ class MySQLDatabaseTest extends SapphireTest
$this->assertInstanceOf(MySQLQuery::class, $result3);
// Iterating one level should not buffer, but return the right result
$result1Array = [];
foreach ($result1 as $record) {
$result1Array[] = $record;
}
$this->assertEquals(
[
'Sort' => 1,
'Title' => 'First Item'
[ 'Sort' => 1, 'Title' => 'First Item' ],
[ 'Sort' => 2, 'Title' => 'Second Item' ],
[ 'Sort' => 3, 'Title' => 'Third Item' ],
[ 'Sort' => 4, 'Title' => 'Last Item' ],
],
$result1->next()
);
$this->assertEquals(
[
'Sort' => 2,
'Title' => 'Second Item'
],
$result1->next()
);
// Test first
$this->assertEquals(
[
'Sort' => 1,
'Title' => 'First Item'
],
$result1->first()
);
// Test seek
$this->assertEquals(
[
'Sort' => 2,
'Title' => 'Second Item'
],
$result1->seek(1)
$result1Array
);
// Test count
$this->assertEquals(4, $result1->numRecords());
// Test second statement
$result2Array = [];
foreach ($result2 as $record) {
$result2Array[] = $record;
break;
}
$this->assertEquals(
[
'Sort' => 3,
'Title' => 'Third Item'
[ 'Sort' => 3, 'Title' => 'Third Item' ],
],
$result2->next()
$result2Array
);
// Test non-prepared query
$result3Array = [];
foreach ($result3 as $record) {
$result3Array[] = $record;
break;
}
$this->assertEquals(
[
'Sort' => 1,
'Title' => 'First Item'
[ 'Sort' => 1, 'Title' => 'First Item' ],
],
$result3->next()
$result3Array
);
}

View File

@ -43,58 +43,47 @@ class PDODatabaseTest extends SapphireTest
$this->assertInstanceOf(PDOQuery::class, $result3);
// Iterating one level should not buffer, but return the right result
$result1Array = [];
foreach ($result1 as $record) {
$result1Array[] = $record;
}
$this->assertEquals(
[
'Sort' => 1,
'Title' => 'First Item'
[ 'Sort' => 1, 'Title' => 'First Item' ],
[ 'Sort' => 2, 'Title' => 'Second Item' ],
[ 'Sort' => 3, 'Title' => 'Third Item' ],
[ 'Sort' => 4, 'Title' => 'Last Item' ],
],
$result1->next()
);
$this->assertEquals(
[
'Sort' => 2,
'Title' => 'Second Item'
],
$result1->next()
);
// Test first
$this->assertEquals(
[
'Sort' => 1,
'Title' => 'First Item'
],
$result1->first()
);
// Test seek
$this->assertEquals(
[
'Sort' => 2,
'Title' => 'Second Item'
],
$result1->seek(1)
$result1Array
);
// Test count
$this->assertEquals(4, $result1->numRecords());
// Test second statement
$result2Array = [];
foreach ($result2 as $record) {
$result2Array[] = $record;
break;
}
$this->assertEquals(
[
'Sort' => 3,
'Title' => 'Third Item'
[ 'Sort' => 3, 'Title' => 'Third Item' ],
],
$result2->next()
$result2Array
);
// Test non-prepared query
$result3Array = [];
foreach ($result3 as $record) {
$result3Array[] = $record;
break;
}
$this->assertEquals(
[
'Sort' => 1,
'Title' => 'First Item'
[ 'Sort' => 1, 'Title' => 'First Item' ],
],
$result3->next()
$result3Array
);
}

View File

@ -90,7 +90,7 @@ class PaginatedListTest extends SapphireTest
$this->assertEquals(1, $list->CurrentPage());
}
public function testGetIterator()
public function testIteration()
{
$list = new PaginatedList(
new ArrayList([
@ -105,26 +105,23 @@ class PaginatedListTest extends SapphireTest
$this->assertListEquals(
[['Num' => 1], ['Num' => 2]],
ArrayList::create($list->getIterator()->getInnerIterator()->getArrayCopy())
$list
);
$list->setCurrentPage(2);
$this->assertListEquals(
[['Num' => 3], ['Num' => 4]],
ArrayList::create($list->getIterator()->getInnerIterator()->getArrayCopy())
$list
);
$list->setCurrentPage(3);
$this->assertListEquals(
[['Num' => 5]],
ArrayList::create($list->getIterator()->getInnerIterator()->getArrayCopy())
$list
);
$list->setCurrentPage(999);
$this->assertListEquals(
[],
ArrayList::create($list->getIterator()->getInnerIterator()->getArrayCopy())
);
$this->assertListEquals([], $list);
// Test disabled paging
$list->setPageLength(0);
@ -137,14 +134,13 @@ class PaginatedListTest extends SapphireTest
['Num' => 4],
['Num' => 5],
],
ArrayList::create($list->getIterator()->getInnerIterator()->getArrayCopy())
$list
);
// Test with dataobjectset
$players = Player::get();
$list = new PaginatedList($players);
$list->setPageLength(1);
$list->getIterator();
$this->assertEquals(
4,
$list->getTotalItems(),
@ -223,10 +219,10 @@ class PaginatedListTest extends SapphireTest
$list = new PaginatedList($list);
$list->setCurrentPage(3);
$this->assertCount(10, $list->getIterator()->getInnerIterator());
$this->assertEquals(10, count($list->toArray()));
$list->setLimitItems(false);
$this->assertCount(50, $list->getIterator()->getInnerIterator());
$this->assertEquals(50, count($list->toArray()));
}
public function testCurrentPage()