-
Notifications
You must be signed in to change notification settings - Fork 0
Parameters
The parameter bag is what makes the builder safe to use with PDO without ever concatenating user input. This page covers the API in detail — collision auto-suffixing, the NULL short-circuit, RawQuery key hashing, and the value-inlining decision tree.
ParameterInterface exposes six methods:
| Method | Returns | Purpose |
|---|---|---|
set(string $key, mixed $value) |
self |
overwrite by key |
add(string|RawQuery $key, mixed $value) |
placeholder name | append, auto-suffix on collision |
get(?string $key = null, mixed $default = null) |
value or full map | read |
all() |
array<string, mixed> |
PDO-ready map |
merge(array|ParameterInterface ...$arrays) |
self |
bulk merge |
reset() |
self |
empty the bag |
The default implementation, Parameters, ships with the package.
$qb = new QueryBuilder('mysql');
$qb->from('users')->where('country', 'TR');
$bag = $qb->getParameter();
$bag->all(); // [':country' => 'TR']$bag->set('id', 1);
$bag->set('id', 2);
$bag->all();
// [':id' => 2]Use set() when you control the key and want it to remain stable across
rebinds. The builder's setParameter() delegates here.
$bag->add('id', 1); // returns ':id'
$bag->add('id', 2); // returns ':id_1'
$bag->add('id', 3); // returns ':id_2'This is what the clause builders use internally. Every value bound by
where('id', ...), set('id', ...) etc. goes through add() so a chain
mentioning the same column repeatedly still produces a valid SQL
statement.
Both set() and add() strip non-alphanumeric characters from the key
before prefixing with ::
$bag->add('user.id', 1); // returns ':userid' (dot removed)
$bag->add('user-id', 2); // returns ':userid_1' (dash removed)PDO bind names only accept [A-Za-z0-9_]. The sanitization is silent —
be aware of it if you reach for "exotic" key shapes.
add() does not register a binding when the value is null:
$placeholder = $bag->add('deleted_at', null);
// $placeholder === 'NULL'
$bag->all();
// []This lets the compiler inline the literal NULL into the SQL. A
parameterized :deleted_at bound to PHP null would compile to
= NULL which is not the same as IS NULL. The short-circuit
sidesteps that footgun. Use whereIsNull() / whereIsNotNull() for the
correct SQL form.
When the key passed to add() is itself a RawQuery (used internally by
batch UPDATE when the column reference is a complex expression), the
implementation hashes it with md5() to produce a stable, opaque
placeholder name:
$bag->add(new RawQuery('some expression'), 1);
// returns ':<32 hex chars>'You won't usually trigger this directly — it's plumbing for the batch UPDATE compiler.
get() is multi-purpose:
$bag->set('id', 99);
$bag->get(); // returns the whole map
$bag->get('id'); // 99
$bag->get(':id'); // 99 — leading colon is optional
$bag->get('missing'); // null
$bag->get('missing', 'fallback'); // 'fallback'
$bag->get('missing', fn () => 'lazy'); // 'lazy' — closure invoked lazilyA Closure default is invoked only when the key is missing —
useful for expensive fallbacks.
merge() accepts plain arrays and other ParameterInterface instances:
$other = (new Parameters())->set('c', 3)->set('d', 4);
$bag = new Parameters();
$bag->merge(['a' => 1, 'b' => 2], $other);
$bag->all();
// [':a' => 1, ':b' => 2, ':c' => 3, ':d' => 4]merge() uses set() semantics — colliding keys overwrite. If you need
collision-safe merging, iterate the source manually and call add() for
each entry.
$bag->reset(); // empty the bag
$bag->all(); // []QueryBuilder::resetStructure() does not reset the parameter bag.
They are independent. If you reuse a builder for a fresh query and want
a clean bag too, call both:
$qb->resetStructure();
$qb->getParameter()->reset();Not every value flows through the bag. The internal helper
SqlValueDetector::isSqlParameterOrFunction() returns true (and the
value is inlined instead of bound) for:
-
Integers —
5becomes5in SQL. -
?— positional placeholder. -
:fooshape — pre-formed named placeholder. -
table.columnshape — dotted column reference. -
function()shape — parameterless SQL function call. -
RawQuery— always inlined verbatim.
$qb->where('id', 5);
// WHERE `id` = 5 ← integer inlined
$qb->where('id', '?');
// WHERE `id` = ? ← positional placeholder inlined
$qb->where('id', $qb->raw('NOW()'));
// WHERE `id` = NOW() ← RawQuery inlinedEverything else — strings, booleans, floats, DateTime — goes through
add().
🔐 The
table.columnandfunction()auto-detection is defensible for programmer-supplied strings but can be abused if user input happens to match those shapes. See Security §V1, §V2 — the application MUST coerce user input to a concrete type before passing it to a value slot.
The map returned by all() is already keyed for PDO:
$pdo = new PDO(/* … */);
$qb->select('*')->from('users')->where('country', 'TR');
$stmt = $pdo->prepare($qb->generateSelectQuery());
$stmt->execute($qb->getParameter()->all());PDO ignores the leading : on bind names, so either form (':country'
or 'country') works at execute time. The bag always emits the
colon-prefixed form.
Useful for sub-queries (see
Sub Queries) or hand-rolled
RawQuery:
$qb->setParameter('admin_role', 'admin');
$qb->where($qb->raw('role = :admin_role'));
// WHERE role = :admin_role
// Bag: [':admin_role' => 'admin']setParameter() is a convenience for getParameter()->set(...).
$qb = new QueryBuilder('mysql');
$qb->from('users')
->where('country', 'TR')
->where('country', 'US') // collision → :country_1
->whereIn('role_id', [1, 2, 3]) // integers inlined
->set('updated_at', $qb->raw('NOW()')); // RawQuery inlined
$qb->getParameter()->all();
// [
// ':country' => 'TR',
// ':country_1' => 'US',
// ]Only the strings landed in the bag — integers and the NOW() call were
inlined directly into the SQL.
Next: Drivers
InitORM QueryBuilder — MIT licensed · authored by Muhammet ŞAFAK · part of the InitORM family · report an issue · security disclosure