Skip to main content

Reading results

execute() returns a Result. It wraps the executed statement and reads rows from it.

interface Result
{
public function first(): ?array;

public function all(): array;

public function column(int|string $column = 0): array;

public function affectedRows(): int;

public function iterate(): iterable;
}

The forward-only contract

caution

A result reads the rows once, moving forward only. Read one result with one method.

There is no rewind. The rows a method consumes are gone, so a second call sees only what is left:

$result = $connection->execute('SELECT * FROM users');

$first = $result->first(); // the first row
$rest = $result->all(); // every row *except* the first

That is occasionally what you want, and much more often a bug. Nothing throws, because it is a legal thing to do with a cursor. If you need the rows twice, call all() once and keep the array.

The reason for the contract is iterate(): a result that buffered everything up front could not stream a large table. The cost is that all five methods share one cursor.

Rows

first() returns the next row as a string-keyed array, or null when there is none. With a LIMIT 1 query it is the row you asked for:

$user = $connection->execute('SELECT * FROM users WHERE id = ?', [$id])->first();

if ($user === null) {
throw new UserNotFound($id);
}

all() returns the remaining rows as a list. On an unread result, that is every row:

$users = $connection->execute('SELECT * FROM users')->all();

An empty result is an empty array, never null, so it is safe to iterate without checking.

Streaming

iterate() yields rows one at a time without buffering them, which is how you walk a table that does not fit in memory:

foreach ($connection->execute('SELECT * FROM events')->iterate() as $event) {
$this->process($event);
}

It is a generator, so nothing is fetched until the loop asks. Breaking out early leaves the rest of the rows unread on the connection; if you plan to reuse the connection immediately, finish the loop or read the rest.

A single column

column() fetches one column across all remaining rows, flattening the rows away:

$ids = $connection->execute('SELECT id FROM users WHERE role = ?', ['admin'])->column();

$names = $connection->execute('SELECT id, name FROM users')->column('name');

The argument is a position or a name, and it defaults to position 0 — the first column of the select. Positions start at zero.

Selecting by name asks the driver for column metadata, which not every driver exposes. Both failure modes throw a ResultException:

SituationMessage
The position is out of range, or the name matches no columnThe result set has no such column. Context lists the columns it does have.
The driver exposes no column metadata at allA column cannot be selected by name on this driver. Use a position instead.

An out-of-range position is caught before the fetch, so a typo is an exception rather than an empty array.

Affected rows

affectedRows() is the statement's row count. For a write, that is the number of rows changed:

$deleted = $connection->execute('DELETE FROM sessions WHERE expires_at < ?', [$cutoff])->affectedRows();
note

Do not use it to count rows in a select. PDO does not promise a row count for a SELECT, and what you get varies by driver — often zero, sometimes the number fetched so far. To count rows, either count($result->all()) or ask the database with SELECT COUNT(*).

Unlike the other methods, affectedRows() reads no rows, so it can be called alongside one of them.

Choosing a method

You wantMethod
At most one rowfirst()
Every row, as an arrayall()
Every row, without loading them alliterate()
One column across rowscolumn()
How many rows a write changedaffectedRows()