Package · cdo

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:

text
  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 type
  • Qb produces a getQuery() string and a getBinds() list. It is pure — no connection, no execution, fully testable on its own.
  • CDO owns the connection and the driver dialect. When you call update() or delete(), it stitches your Qb fragment into a statement and binds each value with the right type via CDOStatement.

Consequences (what follows from the idea)

  • Qb is immutable. Every factory returns a new instance; and / or / clip combine without mutating. (The addAnd / addOr family are the explicit mutable exception — see mutable building.)
  • Placeholders are auto-named. Qb mints :iqb0, :iqb1, … so you rarely name anything. When you want a shared or readable name, pass a CDOBind instead of a raw scalar.
  • null means different things by method. In Qb::eq it becomes IS NULL; in insert/upsert a null value is dropped from the column list entirely.
  • Types are inferred, not assumed. An int binds as PARAM_INT, a bool as PARAM_BOOL, an array as JSON, a DateTimeInterface as Y-m-d H:i:s. You don’t cast; the binder reads gettype(). 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 builderQb builds WHERE-shaped fragments (and CASE), 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