Mental model
CDO is not an ORM and it is not a new database layer. It is a PDO that already knows how to bind values safely and how to speak your specific driver’s dialect. Hold that idea and every method falls into place.
The one idea
A value never touches the SQL string. It travels beside it, as a bind.
Everything CDO adds is a consequence of that. Qb doesn’t build a full query —
it builds a fragment plus its binds. The DML methods don’t concatenate your
data — they turn keys into :placeholders and hand the values to a typed binder.
You compose fragments; the library keeps the values separate the whole way down.
Two halves that meet at execute
CDO has two independent halves that only join when a statement runs:
Qb (immutable) CDO (extends PDO)
───────────── ────────────────
Qb::eq('id', 5) → insert / update / delete / upsert
├─ query: "id = :iqb0" │
└─ binds: [:iqb0 => 5] └─ CDOStatement.bindTypedValue()
└─ picks PDO::PARAM_* by PHP typeQbproduces agetQuery()string and agetBinds()list. It is pure — no connection, no execution, fully testable on its own.CDOowns the connection and the driver dialect. When you callupdate()ordelete(), it stitches yourQbfragment into a statement and binds each value with the right type viaCDOStatement.
Consequences (what follows from the idea)
Qbis immutable. Every factory returns a new instance;and/or/clipcombine without mutating. (TheaddAnd/addOrfamily are the explicit mutable exception — see mutable building.)- Placeholders are auto-named.
Qbmints:iqb0,:iqb1, … so you rarely name anything. When you want a shared or readable name, pass aCDOBindinstead of a raw scalar. nullmeans different things by method. InQb::eqit becomesIS NULL; ininsert/upsertanullvalue is dropped from the column list entirely.- Types are inferred, not assumed. An
intbinds asPARAM_INT, aboolasPARAM_BOOL, an array as JSON, aDateTimeInterfaceasY-m-d H:i:s. You don’t cast; the binder readsgettype(). See Parameter binding.
CDO is still PDO
class CDO extends PDO. That inheritance is deliberate: the convenience methods
cover writes, but reads are plain PDO. There is no select() or
findAll() — you prepare() / query() and fetch() yourself, using a Qb
fragment for the WHERE. Transactions, cursors, and every PDO attribute work
unchanged.
When you reach for what
Writing rows → CDO’s insert / upsert / update / delete. Reading rows →
PDO’s prepare + Qb::…->getQuery() + getBinds(). Building a condition in
isolation (even to unit-test it) → Qb alone, no connection needed.
What CDO deliberately is not
- Not an ORM — no entities, no identity map, no lazy relations. Arrays and plain objects in, arrays out.
- Not a schema tool — no migrations or DDL helpers. You create tables yourself.
- Not a full SELECT builder —
QbbuildsWHERE-shaped fragments (andCASE), not joins, projections, or ordering.
Knowing the boundary is the point: CDO removes the risky, repetitive part of data access — binding and dialect — and leaves the rest to PDO and SQL you already know.
Next steps
- Building conditions — compose
Qbin practice - Driver detection — how the dialect half works
- Parameter binding — how the value half works