class StatementWrapperIterator

Same name in other branches
  1. 10 core/lib/Drupal/Core/Database/StatementWrapperIterator.php \Drupal\Core\Database\StatementWrapperIterator

StatementInterface iterator implementation.

This class is meant to be generic enough for any type of database clients, even if all Drupal core database drivers currently use PDO clients. We implement \Iterator instead of \IteratorAggregate to allow iteration to be kept in sync with the underlying database resultset cursor. PDO is not able to execute a database operation while a cursor is open on the result of an earlier select query, so Drupal by default uses buffered queries setting \PDO::MYSQL_ATTR_USE_BUFFERED_QUERY to TRUE on the connection. This forces the query to return all the results in a buffer local to the client library, potentially leading to memory issues in case of large datasets being returned by a query. Other database clients, however, could allow multithread queries, or developers could disable buffered queries in PDO: in that case, this class prevents the resultset to be entirely fetched in PHP memory (that an \IteratorAggregate implementation would force) and therefore optimize memory usage while iterating the resultset.

Hierarchy

Expanded class hierarchy of StatementWrapperIterator

3 files declare their use of StatementWrapperIterator
Connection.php in core/modules/mysql/src/Driver/Database/mysql/Connection.php
Connection.php in core/modules/pgsql/src/Driver/Database/pgsql/Connection.php
StubConnection.php in core/tests/Drupal/Tests/Core/Database/Stub/StubConnection.php

File

core/lib/Drupal/Core/Database/StatementWrapperIterator.php, line 29

Namespace

Drupal\Core\Database
View source
class StatementWrapperIterator implements \Iterator, StatementInterface {
    use StatementIteratorTrait;
    use FetchModeTrait;
    
    /**
     * The client database Statement object.
     *
     * For a \PDO client connection, this will be a \PDOStatement object.
     */
    protected object $clientStatement;
    
    /**
     * Constructs a StatementWrapperIterator object.
     *
     * @param \Drupal\Core\Database\Connection $connection
     *   Drupal database connection object.
     * @param object $clientConnection
     *   Client database connection object, for example \PDO.
     * @param string $query
     *   The SQL query string.
     * @param array $options
     *   Array of query options.
     * @param bool $rowCountEnabled
     *   (optional) Enables counting the rows matched. Defaults to FALSE.
     */
    public function __construct(Connection $connection, object $clientConnection, string $query, array $options, bool $rowCountEnabled = FALSE) {
        $this->clientStatement = $clientConnection->prepare($query, $options);
        $this->setFetchMode(\PDO::FETCH_OBJ);
    }
    
    /**
     * Returns the client-level database statement object.
     *
     * This method should normally be used only within database driver code.
     *
     * @return object
     *   The client-level database statement, for example \PDOStatement.
     */
    public function getClientStatement() : object {
        return $this->clientStatement;
    }
    
    /**
     * {@inheritdoc}
     */
    public function getConnectionTarget() : string {
        return $this->connection
            ->getTarget();
    }
    
    /**
     * {@inheritdoc}
     */
    public function execute($args = [], $options = []) {
        if (isset($options['fetch'])) {
            if (is_string($options['fetch'])) {
                // \PDO::FETCH_PROPS_LATE tells __construct() to run before properties
                // are added to the object.
                $this->setFetchMode(\PDO::FETCH_CLASS | \PDO::FETCH_PROPS_LATE, $options['fetch']);
            }
            else {
                $this->setFetchMode($options['fetch']);
            }
        }
        if ($this->connection
            ->isEventEnabled(StatementExecutionStartEvent::class)) {
            $startEvent = new StatementExecutionStartEvent(spl_object_id($this), $this->connection
                ->getKey(), $this->connection
                ->getTarget(), $this->getQueryString(), $args ?? [], $this->connection
                ->findCallerFromDebugBacktrace());
            $this->connection
                ->dispatchEvent($startEvent);
        }
        try {
            $return = $this->clientStatement
                ->execute($args);
            $this->markResultsetIterable($return);
        } catch (\Exception $e) {
            if (isset($startEvent) && $this->connection
                ->isEventEnabled(StatementExecutionFailureEvent::class)) {
                $this->connection
                    ->dispatchEvent(new StatementExecutionFailureEvent($startEvent->statementObjectId, $startEvent->key, $startEvent->target, $startEvent->queryString, $startEvent->args, $startEvent->caller, $startEvent->time, get_class($e), $e->getCode(), $e->getMessage()));
            }
            throw $e;
        }
        if (isset($startEvent) && $this->connection
            ->isEventEnabled(StatementExecutionEndEvent::class)) {
            $this->connection
                ->dispatchEvent(new StatementExecutionEndEvent($startEvent->statementObjectId, $startEvent->key, $startEvent->target, $startEvent->queryString, $startEvent->args, $startEvent->caller, $startEvent->time));
        }
        return $return;
    }
    
    /**
     * {@inheritdoc}
     */
    public function getQueryString() {
        return $this->clientStatement->queryString;
    }
    
    /**
     * {@inheritdoc}
     */
    public function fetchCol($index = 0) {
        return $this->fetchAll(\PDO::FETCH_COLUMN, $index);
    }
    
    /**
     * {@inheritdoc}
     */
    public function fetchAllAssoc($key, $fetch = NULL) {
        if (isset($fetch)) {
            if (is_string($fetch)) {
                $this->setFetchMode(\PDO::FETCH_CLASS, $fetch);
            }
            else {
                $this->setFetchMode($fetch);
            }
        }
        // Return early if the statement was already fully traversed.
        if (!$this->isResultsetIterable) {
            return [];
        }
        // Once the while loop is completed, the resultset is marked so not to
        // allow more fetching.
        $return = [];
        while ($record = $this->fetch()) {
            $recordKey = is_object($record) ? $record->{$key} : $record[$key];
            $return[$recordKey] = $record;
        }
        return $return;
    }
    
    /**
     * {@inheritdoc}
     */
    public function fetchAllKeyed($key_index = 0, $value_index = 1) {
        $this->setFetchMode(\PDO::FETCH_NUM);
        // Return early if the statement was already fully traversed.
        if (!$this->isResultsetIterable) {
            return [];
        }
        // Once the while loop is completed, the resultset is marked so not to
        // allow more fetching.
        $return = [];
        while ($record = $this->fetch()) {
            $return[$record[$key_index]] = $record[$value_index];
        }
        return $return;
    }
    
    /**
     * {@inheritdoc}
     */
    public function fetchField($index = 0) {
        // Call \PDOStatement::fetchColumn to fetch the field.
        $column = $this->clientStatement
            ->fetchColumn($index);
        if ($column === FALSE) {
            $this->markResultsetFetchingComplete();
            return FALSE;
        }
        $this->setResultsetCurrentRow($column);
        return $column;
    }
    
    /**
     * {@inheritdoc}
     */
    public function fetchAssoc() {
        return $this->fetch(\PDO::FETCH_ASSOC);
    }
    
    /**
     * {@inheritdoc}
     */
    public function fetchObject(?string $class_name = NULL, array $constructor_arguments = []) {
        if ($class_name) {
            $row = $this->clientStatement
                ->fetchObject($class_name, $constructor_arguments);
        }
        else {
            $row = $this->clientStatement
                ->fetchObject();
        }
        if ($row === FALSE) {
            $this->markResultsetFetchingComplete();
            return FALSE;
        }
        $this->setResultsetCurrentRow($row);
        return $row;
    }
    
    /**
     * {@inheritdoc}
     */
    public function rowCount() {
        // SELECT query should not use the method.
        if ($this->rowCountEnabled) {
            return $this->clientStatement
                ->rowCount();
        }
        else {
            throw new RowCountException();
        }
    }
    
    /**
     * {@inheritdoc}
     */
    public function setFetchMode($mode, $a1 = NULL, $a2 = []) {
        assert(in_array($mode, $this->supportedFetchModes), 'Fetch mode ' . ($this->fetchModeLiterals[$mode] ?? $mode) . ' is not supported. Use supported modes only.');
        // Call \PDOStatement::setFetchMode to set fetch mode.
        // \PDOStatement is picky about the number of arguments in some cases so we
        // need to be pass the exact number of arguments we where given.
        return match (func_num_args()) {    1 => $this->clientStatement
                ->setFetchMode($mode),
            2 => $this->clientStatement
                ->setFetchMode($mode, $a1),
            default => $this->clientStatement
                ->setFetchMode($mode, $a1, $a2),
        
        };
    }
    
    /**
     * {@inheritdoc}
     */
    public function fetch($mode = NULL, $cursor_orientation = NULL, $cursor_offset = NULL) {
        assert(!isset($mode) || in_array($mode, $this->supportedFetchModes), 'Fetch mode ' . ($this->fetchModeLiterals[$mode] ?? $mode) . ' is not supported. Use supported modes only.');
        // Call \PDOStatement::fetchAll to fetch all rows.
        // \PDOStatement is picky about the number of arguments in some cases so we
        // need to pass the exact number of arguments we were given.
        $row = match (func_num_args()) {    0 => $this->clientStatement
                ->fetch(),
            1 => $this->clientStatement
                ->fetch($mode),
            2 => $this->clientStatement
                ->fetch($mode, $cursor_orientation),
            default => $this->clientStatement
                ->fetch($mode, $cursor_orientation, $cursor_offset),
        
        };
        if ($row === FALSE) {
            $this->markResultsetFetchingComplete();
            return FALSE;
        }
        $this->setResultsetCurrentRow($row);
        return $row;
    }
    
    /**
     * {@inheritdoc}
     */
    public function fetchAll($mode = NULL, $column_index = NULL, $constructor_arguments = NULL) {
        assert(!isset($mode) || in_array($mode, $this->supportedFetchModes), 'Fetch mode ' . ($this->fetchModeLiterals[$mode] ?? $mode) . ' is not supported. Use supported modes only.');
        // Call \PDOStatement::fetchAll to fetch all rows.
        // \PDOStatement is picky about the number of arguments in some cases so we
        // need to be pass the exact number of arguments we where given.
        $return = match (func_num_args()) {    0 => $this->clientStatement
                ->fetchAll(),
            1 => $this->clientStatement
                ->fetchAll($mode),
            2 => $this->clientStatement
                ->fetchAll($mode, $column_index),
            default => $this->clientStatement
                ->fetchAll($mode, $column_index, $constructor_arguments),
        
        };
        $this->markResultsetFetchingComplete();
        return $return;
    }

}

Members

Title Sort descending Modifiers Object type Summary Overriden Title
FetchModeTrait::$fetchModeLiterals protected property Map FETCH_* modes to their literal for inclusion in messages.
FetchModeTrait::$supportedFetchModes protected property The fetch modes supported.
FetchModeTrait::assocToClass protected function Converts a row of data in FETCH_ASSOC format to FETCH_CLASS.
FetchModeTrait::assocToColumn protected function Converts a row of data in FETCH_ASSOC format to FETCH_COLUMN.
FetchModeTrait::assocToNum protected function Converts a row of data in FETCH_ASSOC format to FETCH_NUM.
FetchModeTrait::assocToObj protected function Converts a row of data in FETCH_ASSOC format to FETCH_OBJ.
StatementIteratorTrait::$isResultsetIterable private property Traces if rows can be fetched from the resultset.
StatementIteratorTrait::$resultsetKey private property The key of the current row.
StatementIteratorTrait::$resultsetRow private property The current row, retrieved in the current fetch format.
StatementIteratorTrait::current public function Returns the current element.
StatementIteratorTrait::getResultsetCurrentRowIndex protected function Returns the row index of the current element in the resultset.
StatementIteratorTrait::key public function Returns the key of the current element.
StatementIteratorTrait::markResultsetFetchingComplete protected function Informs the iterator that no more rows can be fetched from the resultset.
StatementIteratorTrait::markResultsetIterable protected function Informs the iterator whether rows can be fetched from the resultset.
StatementIteratorTrait::next public function Moves the current position to the next element.
StatementIteratorTrait::rewind public function Rewinds back to the first element of the Iterator.
StatementIteratorTrait::setResultsetCurrentRow protected function Sets the current resultset row for the iterator, and increments the key.
StatementIteratorTrait::valid public function Checks if current position is valid.
StatementWrapperIterator::$clientStatement protected property The client database Statement object.
StatementWrapperIterator::execute public function Executes a prepared statement. Overrides StatementInterface::execute
StatementWrapperIterator::fetch public function Fetches the next row from a result set. Overrides StatementInterface::fetch
StatementWrapperIterator::fetchAll public function Returns an array containing all of the result set rows. Overrides StatementInterface::fetchAll
StatementWrapperIterator::fetchAllAssoc public function Returns the result set as an associative array keyed by the given field. Overrides StatementInterface::fetchAllAssoc
StatementWrapperIterator::fetchAllKeyed public function Returns the entire result set as a single associative array. Overrides StatementInterface::fetchAllKeyed
StatementWrapperIterator::fetchAssoc public function Fetches the next row and returns it as an associative array. Overrides StatementInterface::fetchAssoc
StatementWrapperIterator::fetchCol public function Returns an entire single column of a result set as an indexed array. Overrides StatementInterface::fetchCol
StatementWrapperIterator::fetchField public function Returns a single field from the next record of a result set. Overrides StatementInterface::fetchField
StatementWrapperIterator::fetchObject public function Fetches the next row and returns it as an object. Overrides StatementInterface::fetchObject
StatementWrapperIterator::getClientStatement public function Returns the client-level database statement object.
StatementWrapperIterator::getConnectionTarget public function Returns the target connection this statement is associated with. Overrides StatementInterface::getConnectionTarget
StatementWrapperIterator::getQueryString public function Gets the query string of this statement. Overrides StatementInterface::getQueryString
StatementWrapperIterator::rowCount public function Returns the number of rows matched by the last SQL statement. Overrides StatementInterface::rowCount
StatementWrapperIterator::setFetchMode public function Sets the default fetch mode for this statement. Overrides StatementInterface::setFetchMode
StatementWrapperIterator::__construct public function Constructs a StatementWrapperIterator object.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.