diff --git a/src/Forms/GridField/GridFieldExportButton.php b/src/Forms/GridField/GridFieldExportButton.php
index 9e56b76aad2..9fc3ad3f36a 100644
--- a/src/Forms/GridField/GridFieldExportButton.php
+++ b/src/Forms/GridField/GridFieldExportButton.php
@@ -223,10 +223,6 @@ public function generateExportFileData($gridField)
// 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) {
diff --git a/src/ORM/ArrayList.php b/src/ORM/ArrayList.php
index 74a7cd600ea..d8dca3195fe 100644
--- a/src/ORM/ArrayList.php
+++ b/src/ORM/ArrayList.php
@@ -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 @@ public function exists()
/**
* Returns an Iterator for this ArrayList.
* This function allows you to use ArrayList in foreach loops
- *
- * @return ArrayIterator
*/
- #[\ReturnTypeWillChange]
- public function getIterator()
- {
- $items = array_map(
- function ($item) {
- return is_array($item) ? new ArrayData($item) : $item;
- },
- $this->items ?? []
- );
- return new ArrayIterator($items);
+ public function getIterator(): Iterator
+ {
+ foreach ($this->items as $i => $item) {
+ if (is_array($item)) {
+ yield new ArrayData($item);
+ } else {
+ yield $item;
+ }
+ }
}
/**
diff --git a/src/ORM/Connect/DBSchemaManager.php b/src/ORM/Connect/DBSchemaManager.php
index 585fd8187c9..7e3eb3e63ac 100644
--- a/src/ORM/Connect/DBSchemaManager.php
+++ b/src/ORM/Connect/DBSchemaManager.php
@@ -380,7 +380,7 @@ public function requireTable(
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);
}
}
diff --git a/src/ORM/Connect/MySQLQuery.php b/src/ORM/Connect/MySQLQuery.php
index c811386430a..d019167014b 100644
--- a/src/ORM/Connect/MySQLQuery.php
+++ b/src/ORM/Connect/MySQLQuery.php
@@ -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 @@ public function __destruct()
}
}
- 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 @@ public function numRecords()
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;
- }
+ return null;
}
}
diff --git a/src/ORM/Connect/MySQLStatement.php b/src/ORM/Connect/MySQLStatement.php
index 7bc54765fcd..eada8ad407b 100644
--- a/src/ORM/Connect/MySQLStatement.php
+++ b/src/ORM/Connect/MySQLStatement.php
@@ -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 @@ protected function bind()
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)
- {
- $this->statement = $statement;
- $this->metadata = $metadata;
-
- // Immediately bind and buffer
- $this->bind();
- }
-
- public function __destruct()
+ public function getIterator(): Iterator
{
- $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;
- }
}
diff --git a/src/ORM/Connect/PDOQuery.php b/src/ORM/Connect/PDOQuery.php
index 7180de28d0e..24f9712eb8a 100644
--- a/src/ORM/Connect/PDOQuery.php
+++ b/src/ORM/Connect/PDOQuery.php
@@ -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 @@ public function __construct(PDOStatementHandle $statement)
$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);
}
}
diff --git a/src/ORM/Connect/Query.php b/src/ORM/Connect/Query.php
index af9e0c3a540..8aac231b2b5 100644
--- a/src/ORM/Connect/Query.php
+++ b/src/ORM/Connect/Query.php
@@ -27,30 +27,9 @@
* 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 @@ public function column($column = null)
{
$result = [];
- while ($record = $this->next()) {
+ foreach ($this as $record) {
if ($column) {
$result[] = $record[$column];
} else {
@@ -82,6 +61,7 @@ public function column($column = null)
public function keyedColumn()
{
$column = [];
+
foreach ($this as $record) {
$val = $record[key($record)];
$column[$val] = $val;
@@ -106,13 +86,22 @@ public function map()
}
/**
- * 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 @@ public function record()
*/
public function value()
{
- $record = $this->next();
+ $record = $this->record();
if ($record) {
return $record[key($record)];
}
@@ -164,94 +153,10 @@ public function table()
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 public function nextRecord();
* @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);
}
diff --git a/src/ORM/DataList.php b/src/ORM/DataList.php
index 72dfaf02ef2..8dffc1dfe2b 100644
--- a/src/ORM/DataList.php
+++ b/src/ORM/DataList.php
@@ -6,10 +6,12 @@
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 @@ public function dataClass()
public function __clone()
{
$this->dataQuery = clone $this->dataQuery;
+ $this->finalisedQuery = null;
}
/**
@@ -781,20 +791,6 @@ public function each($callback)
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 = "
" . static::class . "
";
@@ -863,13 +859,32 @@ public function getQueryParams()
/**
* Returns an Iterator for this DataList.
* This function allows you to use DataLists in foreach loops
+ */
+ public function getIterator(): Iterator
+ {
+ 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 ArrayIterator
+ * @return SilverStripe\ORM\Connect\Query
+ * @internal This API may change in minor releases
*/
- #[\ReturnTypeWillChange]
- public function getIterator()
+ protected function getFinalisedQuery()
{
- return new ArrayIterator($this->toArray());
+ if (!$this->finalisedQuery) {
+ $this->finalisedQuery = $this->dataQuery->query()->execute();
+ }
+
+ return $this->finalisedQuery;
}
/**
@@ -880,6 +895,10 @@ public function getIterator()
#[\ReturnTypeWillChange]
public function count()
{
+ if ($this->finalisedQuery) {
+ return $this->finalisedQuery->numRecords();
+ }
+
return $this->dataQuery->count();
}
@@ -1027,8 +1046,7 @@ public function byID($id)
*/
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 @@ public function shuffle()
*/
public function removeAll()
{
- foreach ($this->getGenerator() as $item) {
+ foreach ($this as $item) {
$this->remove($item);
}
return $this;
@@ -1317,14 +1335,15 @@ public function chunkedFetch(int $chunkSize = 1000): iterable
$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;
}
diff --git a/src/ORM/ListDecorator.php b/src/ORM/ListDecorator.php
index d3f006e64b5..f34e5ea5a91 100644
--- a/src/ORM/ListDecorator.php
+++ b/src/ORM/ListDecorator.php
@@ -2,6 +2,7 @@
namespace SilverStripe\ORM;
+use Iterator;
use SilverStripe\View\ViewableData;
use LogicException;
@@ -96,8 +97,7 @@ public function remove($itemObject)
$this->list->remove($itemObject);
}
- #[\ReturnTypeWillChange]
- public function getIterator()
+ public function getIterator(): Iterator
{
return $this->list->getIterator();
}
diff --git a/src/ORM/ManyManyList.php b/src/ORM/ManyManyList.php
index 5ad69cd8f46..ff841a9dc65 100644
--- a/src/ORM/ManyManyList.php
+++ b/src/ORM/ManyManyList.php
@@ -521,7 +521,7 @@ public function getExtraData($componentName, $itemID)
$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;
diff --git a/src/ORM/Map.php b/src/ORM/Map.php
index 6bcd68494db..a29116c539b 100644
--- a/src/ORM/Map.php
+++ b/src/ORM/Map.php
@@ -4,6 +4,7 @@
use ArrayAccess;
use Countable;
+use Iterator;
use IteratorAggregate;
/**
@@ -250,23 +251,55 @@ public function offsetUnset($key)
}
/**
- * 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.
+ * Satisfies the IteratorAggregate interface.
+ */
+ public function getIterator(): Iterator
+ {
+ $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.
*
- * @return Map_Iterator
+ * @param array|object $item
+ * @param string $key
+ * @return mixed
*/
- #[\ReturnTypeWillChange]
- public function getIterator()
+ protected function extractValue($item, $key)
{
- return new Map_Iterator(
- $this->list->getIterator(),
- $this->keyField,
- $this->valueField,
- $this->firstItems,
- $this->lastItems
- );
+ 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];
+ }
+ }
}
/**
diff --git a/src/ORM/Map_Iterator.php b/src/ORM/Map_Iterator.php
deleted file mode 100644
index d645ca07df8..00000000000
--- a/src/ORM/Map_Iterator.php
+++ /dev/null
@@ -1,200 +0,0 @@
-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()
- );
- }
-}
diff --git a/src/ORM/PaginatedList.php b/src/ORM/PaginatedList.php
index f7289e9a8bd..b962c0e546a 100644
--- a/src/ORM/PaginatedList.php
+++ b/src/ORM/PaginatedList.php
@@ -8,6 +8,7 @@
use SilverStripe\View\ArrayData;
use ArrayAccess;
use Exception;
+use Iterator;
use IteratorIterator;
/**
@@ -208,11 +209,7 @@ public function setLimitItems($limit)
return $this;
}
- /**
- * @return IteratorIterator
- */
- #[\ReturnTypeWillChange]
- public function getIterator()
+ public function getIterator(): Iterator
{
$pageLength = $this->getPageLength();
if ($this->limitItems && $pageLength) {
@@ -225,6 +222,21 @@ public function getIterator()
}
}
+ /**
+ * @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 @@ public function PaginationSummary($context = 4)
$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([
diff --git a/src/ORM/UnsavedRelationList.php b/src/ORM/UnsavedRelationList.php
index 51cde920fd9..0206ef19edb 100644
--- a/src/ORM/UnsavedRelationList.php
+++ b/src/ORM/UnsavedRelationList.php
@@ -4,6 +4,7 @@
use InvalidArgumentException;
use ArrayIterator;
+use Iterator;
use SilverStripe\ORM\FieldType\DBField;
/**
@@ -120,11 +121,8 @@ public function dataClass()
/**
* Returns an Iterator for this relation.
- *
- * @return ArrayIterator
*/
- #[\ReturnTypeWillChange]
- public function getIterator()
+ public function getIterator(): Iterator
{
return new ArrayIterator($this->toArray());
}
diff --git a/src/View/SSViewer_Scope.php b/src/View/SSViewer_Scope.php
index 224fd07530a..28d1b541f0d 100644
--- a/src/View/SSViewer_Scope.php
+++ b/src/View/SSViewer_Scope.php
@@ -3,6 +3,7 @@
namespace SilverStripe\View;
use ArrayIterator;
+use Countable;
use Iterator;
/**
@@ -289,16 +290,31 @@ public function next()
}
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();
}
diff --git a/src/View/ViewableData.php b/src/View/ViewableData.php
index 2755b1da4a8..4fd3051c206 100644
--- a/src/View/ViewableData.php
+++ b/src/View/ViewableData.php
@@ -5,6 +5,7 @@
use ArrayIterator;
use Exception;
use InvalidArgumentException;
+use Iterator;
use IteratorAggregate;
use LogicException;
use SilverStripe\Core\ClassInfo;
@@ -573,11 +574,8 @@ public function getXMLValues($fields)
*
* 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]);
}
diff --git a/tests/php/ORM/DatabaseTest.php b/tests/php/ORM/DatabaseTest.php
index 25fa3deed8e..33a45caaf38 100644
--- a/tests/php/ORM/DatabaseTest.php
+++ b/tests/php/ORM/DatabaseTest.php
@@ -74,7 +74,7 @@ public function testMySQLCreateTableOptions()
'SHOW TABLE STATUS WHERE "Name" = \'%s\'',
'DatabaseTest_MyObject'
)
- )->first();
+ )->record();
$this->assertEquals(
$ret['Engine'],
'InnoDB',
diff --git a/tests/php/ORM/ListDecoratorTest.php b/tests/php/ORM/ListDecoratorTest.php
index 2e85f50c970..0410bba7653 100644
--- a/tests/php/ORM/ListDecoratorTest.php
+++ b/tests/php/ORM/ListDecoratorTest.php
@@ -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 @@ protected function setUp(): void
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()
diff --git a/tests/php/ORM/MySQLDatabaseTest.php b/tests/php/ORM/MySQLDatabaseTest.php
index 7b084b04522..e4ffdfd2fd6 100644
--- a/tests/php/ORM/MySQLDatabaseTest.php
+++ b/tests/php/ORM/MySQLDatabaseTest.php
@@ -45,58 +45,47 @@ public function testPreparedStatements()
$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'
- ],
- $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'
+ [ 'Sort' => 1, 'Title' => 'First Item' ],
+ [ 'Sort' => 2, 'Title' => 'Second Item' ],
+ [ 'Sort' => 3, 'Title' => 'Third Item' ],
+ [ 'Sort' => 4, 'Title' => 'Last 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
);
}
diff --git a/tests/php/ORM/PDODatabaseTest.php b/tests/php/ORM/PDODatabaseTest.php
index fa91676c536..5462056711a 100644
--- a/tests/php/ORM/PDODatabaseTest.php
+++ b/tests/php/ORM/PDODatabaseTest.php
@@ -43,58 +43,47 @@ public function testPreparedStatements()
$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'
- ],
- $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'
+ [ 'Sort' => 1, 'Title' => 'First Item' ],
+ [ 'Sort' => 2, 'Title' => 'Second Item' ],
+ [ 'Sort' => 3, 'Title' => 'Third Item' ],
+ [ 'Sort' => 4, 'Title' => 'Last 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
);
}
diff --git a/tests/php/ORM/PaginatedListTest.php b/tests/php/ORM/PaginatedListTest.php
index fb63403d6ec..f8a1e2c9772 100644
--- a/tests/php/ORM/PaginatedListTest.php
+++ b/tests/php/ORM/PaginatedListTest.php
@@ -90,7 +90,7 @@ public function testSetCurrentPage()
$this->assertEquals(1, $list->CurrentPage());
}
- public function testGetIterator()
+ public function testIteration()
{
$list = new PaginatedList(
new ArrayList([
@@ -105,26 +105,23 @@ public function testGetIterator()
$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 @@ public function testGetIterator()
['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 @@ public function testLimitItems()
$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()