Parameter binding
Every value CDO writes goes through CDOStatement::bindTypedValue(), which picks
a PDO::PARAM_* constant from the value’s PHP type. This is what lets you pass a
bool, an int, an array, or a DateTime and get correct SQL without casting —
here’s exactly how it decides.
The type switch
bindTypedValue($parameter, $value) reads gettype($value) and dispatches:
| PHP type | PDO type | Transform |
|---|---|---|
null |
PARAM_NULL |
— |
bool |
PARAM_BOOL |
— |
int |
PARAM_INT |
— |
array |
PARAM_STR |
json_encode($value) first |
object |
PARAM_STR |
serialised via valObject() (below) |
float, string, other |
PARAM_STR |
bound as-is |
This runs for insert/upsert column values, update SET values, and every Qb
bind — a single, consistent path.
Why booleans and ints matter
Binding a bool as PARAM_BOOL and an int as PARAM_INT (rather than letting
everything default to string) keeps PostgreSQL happy with boolean columns and
lets numeric comparisons use indexes correctly. It’s the difference between
WHERE active = true and the driver choking on '1'.
Arrays become JSON
An array value is json_encoded before binding, so a JSON/JSONB column gets valid
JSON with no extra work:
$cdo->update('products',
['attributes' => ['color' => 'red', 'size' => 'M']],
Qb::eq('id', 42)
);
// attributes bound as '{"color":"red","size":"M"}'Object serialisation (valObject)
When a value is an object, valObject() converts it to a scalar by checking
interfaces in this order — first match wins:
| Priority | Interface / type | Conversion |
|---|---|---|
| 1 | JsonSerializable |
$value->jsonSerialize() |
| 2 | Stringable |
(string) $value |
| 3 | DateTimeInterface |
$value->format('Y-m-d H:i:s') |
| 4 | BackedEnum |
$value->value |
| 5 | anything else | serialize($value) (PHP serialisation) |
$cdo->insert('events', [
'occurred_at' => new DateTimeImmutable('2026-07-01 12:00:00'), // → '2026-07-01 12:00:00'
'status' => Status::Active, // BackedEnum → its ->value
'payload' => $dtoImplementingJsonSerializable, // → jsonSerialize()
]);Ordering has consequences
Because JsonSerializable is checked first, an object that is both
JsonSerializable and DateTimeInterface is JSON-encoded, not formatted as a
datetime. And the priority-5 fallback is PHP serialize() — rarely what a column
wants. Implement Stringable or JsonSerializable on your value objects so they
land on an intentional branch, not the serialize fallback.
Explicit binding and replay
CDOStatement also exposes lower-level control:
bindValue($param, $value, $type = PDO::PARAM_STR)— bind with an explicit PDO type, bypassing detection. Every bind (typed or explicit) is recorded.getBindings()— the recorded[param, value, type]triples.updateStm(PDOStatement $stmt)— swap in a freshly prepared statement and replay every recorded bind onto it. This is what makes a statement survivable across a reconnect: the values are re-applied verbatim to the new handle.
Reading it back
Binding is a write-path concern. When you read with inherited PDO methods, values
come back as the driver returns them — a JSON column is a string you json_decode
yourself, a datetime is a string. CDO does not reverse valObject() on read.
Related
- Configuration —
CDOStatementmethod list - Inserting records — where values enter
- Mental model — values travel beside the SQL, not in it