CakePHP(tm) : Rapid Development Framework (https://cakephp.org) Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
Licensed under The MIT License For full copyright and license information, please see the LICENSE.txt Redistributions of files must retain the above copyright notice.
ResultSet | Represents the results obtained after executing a query for a specific table This object is responsible for correctly nesting result keys reported from the query, casting each field to the correct type and executing the extra queries required for eager loading external associations. |
<?php
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\ORM;
use Cake\Collection\Collection;
use Cake\Collection\CollectionTrait;
use Cake\Database\Exception;
use Cake\Database\Type;
use Cake\Datasource\EntityInterface;
use Cake\Datasource\ResultSetInterface;
use SplFixedArray;
/**
* Represents the results obtained after executing a query for a specific table
* This object is responsible for correctly nesting result keys reported from
* the query, casting each field to the correct type and executing the extra
* queries required for eager loading external associations.
*/
class ResultSet implements ResultSetInterface
{
use CollectionTrait;
/**
* Original query from where results were generated
*
* @var \Cake\ORM\Query
* @deprecated 3.1.6 Due to a memory leak, this property cannot be used anymore
*/
protected $_query;
/**
* Database statement holding the results
*
* @var \Cake\Database\StatementInterface
*/
protected $_statement;
/**
* Points to the next record number that should be fetched
*
* @var int
*/
protected $_index = 0;
/**
* Last record fetched from the statement
*
* @var array
*/
protected $_current;
/**
* Default table instance
*
* @var \Cake\ORM\Table|\Cake\Datasource\RepositoryInterface
*/
protected $_defaultTable;
/**
* The default table alias
*
* @var string
*/
protected $_defaultAlias;
/**
* List of associations that should be placed under the `_matchingData`
* result key.
*
* @var array
*/
protected $_matchingMap = [];
/**
* List of associations that should be eager loaded.
*
* @var array
*/
protected $_containMap = [];
/**
* Map of fields that are fetched from the statement with
* their type and the table they belong to
*
* @var array
*/
protected $_map = [];
/**
* List of matching associations and the column keys to expect
* from each of them.
*
* @var array
*/
protected $_matchingMapColumns = [];
/**
* Results that have been fetched or hydrated into the results.
*
* @var array|\ArrayAccess
*/
protected $_results = [];
/**
* Whether to hydrate results into objects or not
*
* @var bool
*/
protected $_hydrate = true;
/**
* Tracks value of $_autoFields property of $query passed to constructor.
*
* @var bool
*/
protected $_autoFields;
/**
* The fully namespaced name of the class to use for hydrating results
*
* @var string
*/
protected $_entityClass;
/**
* Whether or not to buffer results fetched from the statement
*
* @var bool
*/
protected $_useBuffering = true;
/**
* Holds the count of records in this result set
*
* @var int
*/
protected $_count;
/**
* Type cache for type converters.
*
* Converters are indexed by alias and column name.
*
* @var array
*/
protected $_types = [];
/**
* The Database driver object.
*
* Cached in a property to avoid multiple calls to the same function.
*
* @var \Cake\Database\Driver
*/
protected $_driver;
/**
* Constructor
*
* @param \Cake\ORM\Query $query Query from where results come
* @param \Cake\Database\StatementInterface $statement The statement to fetch from
*/
public function __construct($query, $statement)
{
/** @var \Cake\ORM\Table $repository */
$repository = $query->getRepository();
$this->_statement = $statement;
$this->_driver = $query->getConnection()->getDriver();
$this->_defaultTable = $query->getRepository();
$this->_calculateAssociationMap($query);
$this->_hydrate = $query->isHydrationEnabled();
$this->_entityClass = $repository->getEntityClass();
$this->_useBuffering = $query->isBufferedResultsEnabled();
$this->_defaultAlias = $this->_defaultTable->getAlias();
$this->_calculateColumnMap($query);
$this->_autoFields = $query->isAutoFieldsEnabled();
if ($this->_useBuffering) {
$count = $this->count();
$this->_results = new SplFixedArray($count);
}
}
/**
* Returns the current record in the result iterator
*
* Part of Iterator interface.
*
* @return array|object
*/
public function current()
{
return $this->_current;
}
/**
* Returns the key of the current record in the iterator
*
* Part of Iterator interface.
*
* @return int
*/
public function key()
{
return $this->_index;
}
/**
* Advances the iterator pointer to the next record
*
* Part of Iterator interface.
*
* @return void
*/
public function next()
{
$this->_index++;
}
/**
* Rewinds a ResultSet.
*
* Part of Iterator interface.
*
* @throws \Cake\Database\Exception
* @return void
*/
public function rewind()
{
if ($this->_index == 0) {
return;
}
if (!$this->_useBuffering) {
$msg = 'You cannot rewind an un-buffered ResultSet. Use Query::bufferResults() to get a buffered ResultSet.';
throw new Exception($msg);
}
$this->_index = 0;
}
/**
* Whether there are more results to be fetched from the iterator
*
* Part of Iterator interface.
*
* @return bool
*/
public function valid()
{
if ($this->_useBuffering) {
$valid = $this->_index < $this->_count;
if ($valid && $this->_results[$this->_index] !== null) {
$this->_current = $this->_results[$this->_index];
return true;
}
if (!$valid) {
return $valid;
}
}
$this->_current = $this->_fetchResult();
$valid = $this->_current !== false;
if ($valid && $this->_useBuffering) {
$this->_results[$this->_index] = $this->_current;
}
if (!$valid && $this->_statement !== null) {
$this->_statement->closeCursor();
}
return $valid;
}
/**
* Get the first record from a result set.
*
* This method will also close the underlying statement cursor.
*
* @return array|object
*/
public function first()
{
foreach ($this as $result) {
if ($this->_statement && !$this->_useBuffering) {
$this->_statement->closeCursor();
}
return $result;
}
}
/**
* Serializes a resultset.
*
* Part of Serializable interface.
*
* @return string Serialized object
*/
public function serialize()
{
if (!$this->_useBuffering) {
$msg = 'You cannot serialize an un-buffered ResultSet. Use Query::bufferResults() to get a buffered ResultSet.';
throw new Exception($msg);
}
while ($this->valid()) {
$this->next();
}
if ($this->_results instanceof SplFixedArray) {
return serialize($this->_results->toArray());
}
return serialize($this->_results);
}
/**
* Unserializes a resultset.
*
* Part of Serializable interface.
*
* @param string $serialized Serialized object
* @return void
*/
public function unserialize($serialized)
{
$results = (array)(unserialize($serialized) ?: []);
$this->_results = SplFixedArray::fromArray($results);
$this->_useBuffering = true;
$this->_count = $this->_results->count();
}
/**
* Gives the number of rows in the result set.
*
* Part of the Countable interface.
*
* @return int
*/
public function count()
{
if ($this->_count !== null) {
return $this->_count;
}
if ($this->_statement) {
return $this->_count = $this->_statement->rowCount();
}
if ($this->_results instanceof SplFixedArray) {
$this->_count = $this->_results->count();
} else {
$this->_count = count($this->_results);
}
return $this->_count;
}
/**
* Calculates the list of associations that should get eager loaded
* when fetching each record
*
* @param \Cake\ORM\Query $query The query from where to derive the associations
* @return void
*/
protected function _calculateAssociationMap($query)
{
$map = $query->getEagerLoader()->associationsMap($this->_defaultTable);
$this->_matchingMap = (new Collection($map))
->match(['matching' => true])
->indexBy('alias')
->toArray();
$this->_containMap = (new Collection(array_reverse($map)))
->match(['matching' => false])
->indexBy('nestKey')
->toArray();
}
/**
* Creates a map of row keys out of the query select clause that can be
* used to hydrate nested result sets more quickly.
*
* @param \Cake\ORM\Query $query The query from where to derive the column map
* @return void
*/
protected function _calculateColumnMap($query)
{
$map = [];
foreach ($query->clause('select') as $key => $field) {
$key = trim($key, '"`[]');
if (strpos($key, '__') <= 0) {
$map[$this->_defaultAlias][$key] = $key;
continue;
}
$parts = explode('__', $key, 2);
$map[$parts[0]][$key] = $parts[1];
}
foreach ($this->_matchingMap as $alias => $assoc) {
if (!isset($map[$alias])) {
continue;
}
$this->_matchingMapColumns[$alias] = $map[$alias];
unset($map[$alias]);
}
$this->_map = $map;
}
/**
* Creates a map of Type converter classes for each of the columns that should
* be fetched by this object.
*
* @deprecated 3.2.0 Not used anymore. Type casting is done at the statement level
* @return void
*/
protected function _calculateTypeMap()
{
deprecationWarning('ResultSet::_calculateTypeMap() is deprecated, and will be removed in 4.0.0.');
}
/**
* Returns the Type classes for each of the passed fields belonging to the
* table.
*
* @param \Cake\ORM\Table $table The table from which to get the schema
* @param array $fields The fields whitelist to use for fields in the schema.
* @return array
*/
protected function _getTypes($table, $fields)
{
$types = [];
$schema = $table->getSchema();
$map = array_keys((array)Type::getMap() + ['string' => 1, 'text' => 1, 'boolean' => 1]);
$typeMap = array_combine(
$map,
array_map(['Cake\Database\Type', 'build'], $map)
);
foreach (['string', 'text'] as $t) {
if (get_class($typeMap[$t]) === 'Cake\Database\Type') {
unset($typeMap[$t]);
}
}
foreach (array_intersect($fields, $schema->columns()) as $col) {
$typeName = $schema->getColumnType($col);
if (isset($typeMap[$typeName])) {
$types[$col] = $typeMap[$typeName];
}
}
return $types;
}
/**
* Helper function to fetch the next result from the statement or
* seeded results.
*
* @return mixed
*/
protected function _fetchResult()
{
if (!$this->_statement) {
return false;
}
$row = $this->_statement->fetch('assoc');
if ($row === false) {
return $row;
}
return $this->_groupResult($row);
}
/**
* Correctly nests results keys including those coming from associations
*
* @param array $row Array containing columns and values or false if there is no results
* @return array Results
*/
protected function _groupResult($row)
{
$defaultAlias = $this->_defaultAlias;
$results = $presentAliases = [];
$options = [
'useSetters' => false,
'markClean' => true,
'markNew' => false,
'guard' => false
];
foreach ($this->_matchingMapColumns as $alias => $keys) {
$matching = $this->_matchingMap[$alias];
$results['_matchingData'][$alias] = array_combine(
$keys,
array_intersect_key($row, $keys)
);
if ($this->_hydrate) {
/* @var \Cake\ORM\Table $table */
$table = $matching['instance'];
$options['source'] = $table->getRegistryAlias();
/* @var \Cake\Datasource\EntityInterface $entity */
$entity = new $matching['entityClass']($results['_matchingData'][$alias], $options);
$results['_matchingData'][$alias] = $entity;
}
}
foreach ($this->_map as $table => $keys) {
$results[$table] = array_combine($keys, array_intersect_key($row, $keys));
$presentAliases[$table] = true;
}
// If the default table is not in the results, set
// it to an empty array so that any contained
// associations hydrate correctly.
if (!isset($results[$defaultAlias])) {
$results[$defaultAlias] = [];
}
unset($presentAliases[$defaultAlias]);
foreach ($this->_containMap as $assoc) {
$alias = $assoc['nestKey'];
if ($assoc['canBeJoined'] && empty($this->_map[$alias])) {
continue;
}
/* @var \Cake\ORM\Association $instance */
$instance = $assoc['instance'];
if (!$assoc['canBeJoined'] && !isset($row[$alias])) {
$results = $instance->defaultRowValue($results, $assoc['canBeJoined']);
continue;
}
if (!$assoc['canBeJoined']) {
$results[$alias] = $row[$alias];
}
$target = $instance->getTarget();
$options['source'] = $target->getRegistryAlias();
unset($presentAliases[$alias]);
if ($assoc['canBeJoined'] && $this->_autoFields !== false) {
$hasData = false;
foreach ($results[$alias] as $v) {
if ($v !== null && $v !== []) {
$hasData = true;
break;
}
}
if (!$hasData) {
$results[$alias] = null;
}
}
if ($this->_hydrate && $results[$alias] !== null && $assoc['canBeJoined']) {
$entity = new $assoc['entityClass']($results[$alias], $options);
$results[$alias] = $entity;
}
$results = $instance->transformRow($results, $alias, $assoc['canBeJoined'], $assoc['targetProperty']);
}
foreach ($presentAliases as $alias => $present) {
if (!isset($results[$alias])) {
continue;
}
$results[$defaultAlias][$alias] = $results[$alias];
}
if (isset($results['_matchingData'])) {
$results[$defaultAlias]['_matchingData'] = $results['_matchingData'];
}
$options['source'] = $this->_defaultTable->getRegistryAlias();
if (isset($results[$defaultAlias])) {
$results = $results[$defaultAlias];
}
if ($this->_hydrate && !($results instanceof EntityInterface)) {
$results = new $this->_entityClass($results, $options);
}
return $results;
}
/**
* Casts all values from a row brought from a table to the correct
* PHP type.
*
* @param string $alias The table object alias
* @param array $values The values to cast
* @deprecated 3.2.0 Not used anymore. Type casting is done at the statement level
* @return array
*/
protected function _castValues($alias, $values)
{
deprecationWarning('ResultSet::_castValues() is deprecated, and will be removed in 4.0.0.');
return $values;
}
/**
* Returns an array that can be used to describe the internal state of this
* object.
*
* @return array
*/
public function __debugInfo()
{
return [
'items' => $this->toArray(),
];
}
}