Querying with WDB
One query builder, two entry points, and a chain that changes nothing until it is finished.
Overview
Queries are built by chaining calls rather than by writing SQL into a string. The chain reads in the order the statement is written. Every value you pass as an argument is bound instead of pasted.
WDB is a static facade. Each of its methods forwards to the one Database object the model layer holds. The static form and a model's $this->db are the same connection and the same builder state.
Reference
Entry Points
Models. A chain may be started on one form and continued on the other.
whereGroup, getCount, pushState) are reached off the object a chain returns.
Reading
// Name the columns. "*" hands your callers whatever the table grows into later.
public static function select($arg = "*"); // a falsy $arg becomes "*"
public static function from($arg = ''); // "users_products" | "users_products AS t"
public static function join($type, $table, $where); // $type: "LEFT" | "INNER" | "RIGHT" - pasted before " JOIN "
public static function where($column, $mark = '', $value = '', $logical = '');
public static function group_by($arg = ''); // raw expression, not bound
public static function order_by($arg); // raw expression, not bound
public static function limit($arg1, $arg2 = null); // limit(25) -> LIMIT 0,25 · limit(50, 25) -> LIMIT 50,25
public static function build($isthis = false); // bool: matched rows > 0 · $isthis = true returns the Database object
// Fetchers read the statement build() left behind, or the one you hand them.
public static function fetch_assoc($statement = false); // list of rows, each column => value; [] when none matched
public static function getAssoc($statement = false); // one row as column => value; false when there is no row
public static function fetch_object($statement = false);// the same list as stdClass objects
public static function getObject($statement = false); // one row as stdClass; false when there is no row
public static function rowCounter($statement = false); // int: rows the last statement touched
What where() Accepts
The second argument is written into the statement as-is. It is an SQL operator, not a keyword from a list the builder owns. Three shapes are special-cased.
| $mark | $value | Produced SQL |
|---|---|---|
'=' '!=' |
scalar; an empty string is still bound | col = ? |
'>' '<' '>=' '<=' |
scalar | col > ? |
'LIKE' 'NOT LIKE' |
scalar carrying its own % |
col LIKE ? |
'IN' 'NOT IN' |
an array, one placeholder per element | col IN (?,?,?) |
'IS' 'IS NOT' |
'NULL', pasted rather than bound |
col IS NULL |
'IS NOT NULL' |
'', the whole test is the operator |
col IS NOT NULL |
omitted ('') |
a non-empty scalar | col = ? |
// $logical accepts exactly 'AND', '&&', 'OR', '||'. It joins THIS condition to the NEXT
// one; a condition left without it is given AND automatically once the next one arrives.
public static function where($column, $mark = '', $value = '', $logical = '');
// Parentheses: no static wrapper, so it is called on the object the chain returns.
// $logical joins the whole group to the condition that FOLLOWS it, exactly like the
// fourth argument of where(). Leaving it out is safe: the group is recorded as a
// joinable predicate, so the next condition supplies AND by itself. Pass it for OR.
public function whereGroup(callable $callback, string $logical = ''): self;
Writing
public static function insert($table, $data); // int affected rows; throws on an SQL error
public static function lastID(); // int identifier the insert produced; 0 with no connection
public static function update($table = '', $data = []); // a non-empty $data calls set() for you
public static function set($data = [], $special = false); // $special = true pastes the value RAW: ['uses' => 'uses+1']
public static function save($isthis = false); // bool: no error - TRUE also when zero rows matched
public static function delete($arg = '', $arg2 = ''); // delete("t") | delete("a", "a INNER JOIN b ON ...")
public static function run($isthis = false); // bool: deleted rows > 0
// $data for insert() and update()/set() is column => value. A null value is bound as
// SQL NULL, an int as an integer, everything else as a string.
// limit() applies to save() and run() too, as a row count with no offset: limit(5000).
// order_by() picks which rows a limited write takes. Two chains throw instead of
// running, because MySQL rejects them: update()->join(), and a multi-table
// delete("t", "table t") carrying order_by() or limit().
Raw Statements
public static function query($statement, $isthis = false); // PDOStatement, or false - the error is swallowed
public static function exec($arg = ''); // int affected rows; 0 on error - the error is swallowed
public static function hasTable($table = ''); // bool, via SHOW TABLES LIKE - the name is pasted, no prefix
public static function getPrefix(): string; // the schema prefix from the database configuration
public static function getErrorMessage(); // text of the last error, including the swallowed ones
Example
// A page of rows.
WDB::select('t.id, t.name, t.status, u.full_name')
->from('users_products AS t')
->join('LEFT', 'users AS u', 'u.id = t.owner_id')
->where('t.status', 'IN', ['active', 'inprocess'])
->where('t.type', '=', 'hosting')
->order_by('t.id DESC')
->limit(0, 25)
->build();
$rows = WDB::fetch_assoc(); // [] when nothing matched
// One row. build() is false when nothing matched, which is the guard to write.
$stmt = WDB::select('id, name, status')->from('users_products')->where('id', '=', $id)->limit(1);
$row = $stmt->build() ? $stmt->getAssoc() : [];
// A bound OR group, isolated from the conditions around it.
$search = WDB::select('id')->from('users_products');
$search->where('owner_id', '=', $userId);
$search->whereGroup(function ($q) use ($word) {
$q->where('name', 'LIKE', '%' . $word . '%', '||');
$q->where('status', 'LIKE', '%' . $word . '%');
}, '&&');
$search->where('type', '=', 'hosting');
$found = $search->build() ? $search->fetch_assoc() : [];
// Insert returns the number of affected rows, and the identifier is read afterwards.
$affected = WDB::insert('users_products', [
'owner_id' => $userId,
'product_id' => $productId,
'name' => $name,
'status' => 'waiting',
'notes' => null, // bound as SQL NULL
'cdate' => DateManager::Now(),
]);
$newId = $affected ? (int) WDB::lastID() : 0;
// Update: the chain does nothing until save().
WDB::update('users_products', ['status' => 'active'])->where('id', '=', $newId)->save();
// The one value that is deliberately not bound: an expression evaluated by the server.
WDB::update('coupons')->set(['uses' => 'uses+1'], true)->where('id', '=', $couponId)->save();
// Read the write back, from the other entry point - it is the same connection.
$check = WDB::select('status')->from('users_products')->where('id', '=', $newId);
$saved = $check->build() ? ($check->getAssoc()['status'] ?? '') : '';
// Delete: run(), not save().
WDB::delete('users_products')->where('id', '=', $newId)->run();
Pitfalls
Passing the value as an argument is what makes it bound. Building it into the column expression instead moves it into the statement. The first quote in a user's input then becomes an injection. Three places are unbound on purpose and must never receive user input. They are order_by and group_by, set($data, true), and the table name given to hasTable.
limit(25) is the first 25 rows, limit(50, 25) is 25 rows from offset 50. The count moves to the second slot as soon as there is an offset. The fourth argument of where() joins that condition to the next one, not to the previous one. It belongs on the condition before the alternative, never on the last one in a group.
An update needs save() and a delete needs run(); a chain that stops at the conditions changes nothing and reports nothing. Their return values then disagree on purpose. save() answers "no error", so it is true even when the condition matched no row. run() and build() answer "rows > 0". Checking that a record really changed means reading it back, not trusting the boolean.
build(), insert(), save() and run() turn a failed statement into an exception, which an operation converts into an error response for you. query() and exec() swallow it instead and return false or 0, so a schema statement can fail in complete silence. When you reach for them, read getErrorMessage().
Related Articles
Vielen Dank für Ihre Rückmeldung!
Unser Support-Team ist rund um die Uhr für Sie da, wenn Sie oben nicht fündig werden.