Binding time matters
A fictional importer receives two amounts, 10 and 20, and prepares statements for later execution. bindParam retains a reference to the variable; the value used depends on what it contains when execute is called. If both statements point to the same variable reused in the loop, both can receive 20. bindValue captures the value supplied when binding occurs. Draw the preparation, binding, mutation, and execution sequence before choosing the method. A transaction groups database effects but does not repair a misused PHP reference. PDO::PARAM_INT declares the parameter type and likewise does not turn a reference into a copy.
Every execution has complete inputs
In a worker that reuses a statement, treat each task as a new operation. Supply the parameters required for that execution instead of depending on values left by a previous task. When binding type matters, choose an explicit strategy and test it on the driver being used. The execute parameter array is not equivalent to declaring each parameter as integer with bindValue. A SQLite typeof test can expose that difference locally; it does not establish every conversion in another engine. Parameters represent values, not table or column names. For configurable ordering, map allowed choices to SQL fragments defined by the application.
Reading a row advances the cursor
A query returns two columns per task. Calling fetchColumn for the first column and then for the second does not read two fields from one row: each call obtains a column from a new row. Read one row with fetch and extract fields from that structure. If the query joins two tables with an id column, assign aliases such as task_id and owner_id. FETCH_ASSOC does not provide two distinct entries under the same key. FETCH_BOTH adds numeric positions and does not replace an API contract. The test should verify names and relationships between values, not merely the number of returned results.
Zero, empty, and failure differ
A COUNT can return zero in a valid row; the next read can return false because the cursor ended. Use comparisons that respect these contracts. fetchAll with no rows returns an empty array and, after an earlier read, collects only remaining rows. An UPDATE can execute successfully and affect zero rows because its predicate matches no record. PDO::exec can also return zero without representing an error. Do not use SELECT rowCount as a portable count: behavior depends on the driver. If only existence matters, fetch a row; if the count matters, define the query and the time represented by that observation.
Laboratory: preserve two values
The code creates an in-memory SQLite database, prepares two INSERT statements, and delays execution until the loop ends. Predict output before running it: rows should retain amounts 10 and 20. Then replace only bindValue with bindParam while retaining the loop variable and observe the change. Restore the correct version before continuing. Reading uses an explicit identifier alias and FETCH_ASSOC to obtain a small structure. Add a read after the end of the cursor and confirm false using strict comparison. This laboratory is local and uses neither customer data nor a connection to a production database.
Summary for integration diagnosis
When a batch ends without exception but contains wrong values, separate three questions: what were the parameters at execution time, which rows did the query return, and what effects did the write actually produce? Record a synthetic sample with values and call order. A success counter does not prove the contract was met. Check aliases, cursor position, and empty-result handling before changing infrastructure. Connect this lesson to JSON contracts: an ambiguous PDO structure can become syntactically valid but semantically wrong JSON. The next lesson adds transaction ownership and concurrency decisions, which require guarantees beyond correct reading.
<?php
declare(strict_types=1);
$db = new PDO('sqlite::memory:', null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$db->exec('CREATE TABLE entries(id INTEGER PRIMARY KEY, amount INTEGER NOT NULL)');
$queue = [];
foreach ([10, 20] as $amount) {
$statement = $db->prepare('INSERT INTO entries(amount) VALUES(:amount)');
$statement->bindValue(':amount', $amount, PDO::PARAM_INT);
$queue[] = $statement;
}
foreach ($queue as $statement) {
$statement->execute;
}
$rows = $db->query('SELECT id AS entry_id, amount FROM entries ORDER BY id')->fetchAll(PDO::FETCH_ASSOC);
echo json_encode($rows, JSON_THROW_ON_ERROR)Two entries prepared using one variable retain their original values when bindValue captures each amount.
Common pitfalls
Do not confuse references with copies, fields in one row with successive reads, or zero affected rows with SQL failure.
Related topics: PDO transactions and partial failures · JSON, contracts, and explicit failures · Streams, CSV, and controlled imports
Define parameters, result shape, and the meaning of zero and false as explicit parts of the contract.
Reference: PHP manual: pdostatement.fetch · PHP 8.5 reference; DR PHP 2026.3; new fixtures executed on PHP 8.4.4 / SQLite 3.51.2