mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Merge pull request #6518 from sminnee/generators
Use Generators for ORM Query, Map, ArrayList
This commit is contained in:
parent
470753194c
commit
595835bb15
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -863,13 +873,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 +909,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 +1060,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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
@ -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([
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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]);
|
||||
}
|
||||
|
@ -74,7 +74,7 @@ class DatabaseTest extends SapphireTest
|
||||
'SHOW TABLE STATUS WHERE "Name" = \'%s\'',
|
||||
'DatabaseTest_MyObject'
|
||||
)
|
||||
)->first();
|
||||
)->record();
|
||||
$this->assertEquals(
|
||||
$ret['Engine'],
|
||||
'InnoDB',
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user