mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-08-30 22:37:42 +02:00
Merge pull request #10484 from creative-commoners/pulls/5/rescue-master-orm-generators
API rescue master-branch PR: Use Generators for ORM
This commit is contained in:
commit
71dca0118d
@ -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) {
|
||||
|
@ -109,13 +109,13 @@ class ArrayList extends ViewableData implements SS_List, Filterable, Sortable, L
|
||||
#[\ReturnTypeWillChange]
|
||||
public function getIterator()
|
||||
{
|
||||
$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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -378,7 +378,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);
|
||||
}
|
||||
}
|
||||
|
@ -45,16 +45,30 @@ class MySQLQuery extends Query
|
||||
}
|
||||
}
|
||||
|
||||
public function seek($row)
|
||||
#[\ReturnTypeWillChange]
|
||||
public function getIterator()
|
||||
{
|
||||
$floatTypes = [MYSQLI_TYPE_FLOAT, MYSQLI_TYPE_DOUBLE, MYSQLI_TYPE_DECIMAL, MYSQLI_TYPE_NEWDECIMAL];
|
||||
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 ($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;
|
||||
}
|
||||
yield $data;
|
||||
}
|
||||
// Check for the method first since $this->handle is a mixed type
|
||||
if (method_exists($this->handle, 'data_seek')) {
|
||||
// Reset so the query can be iterated over again
|
||||
$this->handle->data_seek(0);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function numRecords()
|
||||
@ -62,27 +76,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -56,6 +56,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 +102,25 @@ 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)
|
||||
#[\ReturnTypeWillChange]
|
||||
public function getIterator()
|
||||
{
|
||||
$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) {
|
||||
$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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -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,11 @@ 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();
|
||||
#[\ReturnTypeWillChange]
|
||||
abstract public function getIterator();
|
||||
|
||||
/**
|
||||
* Return the total number of items in the query result.
|
||||
@ -259,12 +165,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);
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ 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;
|
||||
@ -49,6 +50,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 +87,7 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
|
||||
public function __clone()
|
||||
{
|
||||
$this->dataQuery = clone $this->dataQuery;
|
||||
$this->finalisedQuery = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -781,20 +790,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>";
|
||||
@ -864,12 +859,34 @@ 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
|
||||
* @return Generator
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function getIterator()
|
||||
{
|
||||
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 +897,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,6 +1048,11 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
|
||||
*/
|
||||
public function column($colName = "ID")
|
||||
{
|
||||
if ($this->finalisedQuery) {
|
||||
$finalisedQuery = clone $this->finalisedQuery;
|
||||
return $finalisedQuery->distinct(false)->column($colName);
|
||||
}
|
||||
|
||||
$dataQuery = clone $this->dataQuery;
|
||||
return $dataQuery->distinct(false)->column($colName);
|
||||
}
|
||||
@ -1174,7 +1200,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 +1343,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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -260,13 +260,48 @@ class Map implements ArrayAccess, Countable, IteratorAggregate
|
||||
#[\ReturnTypeWillChange]
|
||||
public function getIterator()
|
||||
{
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
@ -225,6 +225,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 +359,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([
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -42,7 +42,6 @@ class DataListTest extends SapphireTest
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public function testFilterDataObjectByCreatedDate()
|
||||
{
|
||||
// create an object to test with
|
||||
|
@ -7,6 +7,7 @@ use SilverStripe\ORM\Connect\MySQLDatabase;
|
||||
use SilverStripe\MSSQL\MSSQLDatabase;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use Exception;
|
||||
use SilverStripe\ORM\Queries\SQLSelect;
|
||||
use SilverStripe\ORM\Tests\DatabaseTest\MyObject;
|
||||
|
||||
/**
|
||||
@ -74,7 +75,7 @@ class DatabaseTest extends SapphireTest
|
||||
'SHOW TABLE STATUS WHERE "Name" = \'%s\'',
|
||||
'DatabaseTest_MyObject'
|
||||
)
|
||||
)->first();
|
||||
)->record();
|
||||
$this->assertEquals(
|
||||
$ret['Engine'],
|
||||
'InnoDB',
|
||||
@ -293,4 +294,58 @@ class DatabaseTest extends SapphireTest
|
||||
$this->assertEquals($inputData, $query->map());
|
||||
$this->assertEquals($inputData, $query->map());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that repeated abstracted iteration of a query returns all records.
|
||||
*/
|
||||
public function testRepeatedIterationUsingAbstraction()
|
||||
{
|
||||
$inputData = ['one', 'two', 'three', 'four'];
|
||||
|
||||
foreach ($inputData as $i => $text) {
|
||||
$x = new MyObject();
|
||||
$x->MyField = $text;
|
||||
$x->MyInt = $i;
|
||||
$x->write();
|
||||
}
|
||||
|
||||
$select = SQLSelect::create(['"MyInt"', '"MyField"'], '"DatabaseTest_MyObject"', orderby: ['"MyInt"']);
|
||||
|
||||
$this->assertEquals($inputData, $select->execute()->map());
|
||||
$this->assertEquals($inputData, $select->execute()->map());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that stopping iteration part-way through produces predictable results
|
||||
* on a subsequent iteration.
|
||||
* This test is here to ensure consistency between implementations (e.g. mysql vs postgres, etc)
|
||||
*/
|
||||
public function testRepeatedPartialIteration()
|
||||
{
|
||||
$inputData = ['one', 'two', 'three', 'four'];
|
||||
|
||||
foreach ($inputData as $i => $text) {
|
||||
$x = new MyObject();
|
||||
$x->MyField = $text;
|
||||
$x->MyInt = $i;
|
||||
$x->write();
|
||||
}
|
||||
|
||||
$query = DB::query('SELECT "MyInt", "MyField" FROM "DatabaseTest_MyObject" ORDER BY "MyInt"');
|
||||
|
||||
$i = 0;
|
||||
foreach ($query as $record) {
|
||||
$this->assertEquals($inputData[$i], $record['MyField']);
|
||||
$i++;
|
||||
if ($i > 1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Continue from where we left off, since we're using a Generator
|
||||
foreach ($query as $record) {
|
||||
$this->assertEquals($inputData[$i], $record['MyField']);
|
||||
$i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -45,58 +45,50 @@ 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 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
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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,14 @@ 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();
|
||||
$list;
|
||||
$this->assertEquals(
|
||||
4,
|
||||
$list->getTotalItems(),
|
||||
@ -223,10 +220,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()
|
||||
|
Loading…
Reference in New Issue
Block a user