Building conditions
Qb builds parameterised WHERE fragments you can compose, nest, and reuse.
Every value becomes a named bind, so values are injection-safe by
construction. This guide covers the patterns you reach for daily; the full
operator list is in the Qb operators reference.
Column names are injected verbatim
Only values are bound. The column name argument is interpolated into the
SQL as-is, without quoting or escaping. Never pass user input as a column name —
Qb::eq('status', $userInput) is safe, but Qb::eq($userInput, 'active') is a
SQL-injection vector.
Compose with and / or
Start from leaf conditions and join them:
use Flytachi\Winter\Cdo\Qb;
$where = Qb::and(
Qb::eq('status', 'active'),
Qb::gte('age', 18),
Qb::isNull('banned_at'),
);
// status = :iqb0 AND age >= :iqb1 AND banned_at IS NULLand / or / xor take any number of conditions and skip null or empty ones
silently — which is exactly what makes optional filters clean.
Optional (dynamic) filters
Because null conditions are dropped, you can inline them with a ternary. Empty
in() lists must be guarded, since in() throws on an empty array.
$where = Qb::and(
Qb::eq('status', 'active'),
$minAge !== null ? Qb::gte('age', $minAge) : null,
$country !== null ? Qb::eq('country', $country) : null,
$tagIds ? Qb::in('tag_id', $tagIds) : null, // guard empty array
);Guard against an all-empty condition
If every optional filter is skipped, Qb::and(...) yields an empty fragment.
Passing that to update() or delete() produces a statement with no WHERE.
Check $where->getQuery() !== '' before running a mutation.
Control precedence with clip
SQL binds AND tighter than OR. When you mix them, wrap the OR group in
clip() to force the grouping you mean:
// Wrong: reads as (published AND role='editor') OR role='admin'
Qb::and(
Qb::eq('published', true),
Qb::or(Qb::eq('role', 'editor'), Qb::eq('role', 'admin')),
);
// Right: clip makes the OR a single grouped term
Qb::and(
Qb::eq('published', true),
Qb::clip(
Qb::or(Qb::eq('role', 'editor'), Qb::eq('role', 'admin')),
),
);
// published IS TRUE AND (role = :iqb0 OR role = :iqb1)Build incrementally (mutable)
Qb is immutable by default, but the addAnd / addOr / addXor methods mutate
in place — handy when you accumulate conditions in a loop:
$where = Qb::eq('status', 'active');
foreach ($filters as $column => $value) {
$where->addAnd(Qb::eq($column, $value));
}Reuse a value with a named bind
By default each value gets an opaque placeholder (:iqb0). To reference the
same value across several conditions, build a CDOBind once and pass it in —
it binds a single :name shared by every condition that uses it:
use Flytachi\Winter\Cdo\CDOBind;
$uid = new CDOBind('uid', $currentUserId);
$where = Qb::or(
Qb::eq('author_id', $uid),
Qb::eq('reviewer_id', $uid),
Qb::eq('assignee_id', $uid),
);
// author_id = :uid OR reviewer_id = :uid OR assignee_id = :uidEscape hatch: raw
When no operator fits (vendor functions, subqueries), Qb::raw() injects SQL
verbatim — but still lets you bind values through placeholders:
Qb::raw('JSON_CONTAINS(tags, :tag)', ['tag' => '"php"']);
// JSON_CONTAINS(tags, :tag) — :tag is bound, the function text is verbatimraw does not sanitise the query string
The SQL string in raw() is emitted exactly as written. Never interpolate user
input into it — pass values through the $binds argument instead.
Using the fragment
update() and delete() consume a Qb directly. For reads, pull the pieces
apart and bind them onto a PDO statement:
$stmt = $cdo->prepare("SELECT * FROM users WHERE " . $where->getQuery());
foreach ($where->getBinds() as $bind) {
$stmt->bindValue($bind->getName(), $bind->getValue());
}
$stmt->execute();Related
- Qb operators — every operator and its SQL
- Updating & deleting — conditions as
WHERE - Configuration —
CDOBinddetails - Mental model — why fragments carry their own binds