Introduction
Real-world queries need more than simple equality checks. IN clauses, NULL handling, OR groups, LIKE patterns, and subqueries are everyday requirements. Extending the query builder with these conditions while maintaining safety requires careful handling of edge cases.
Key Concepts
- IN Clause: Filters rows where a column matches any value in a set.
- Empty Array Edge Case: An
IN ()with no values is invalid SQL and must be handled explicitly. - Condition Groups: Parenthesized groups of AND/OR conditions that control evaluation order.
Real World Context
A product catalog filter might combine category IN (list), price range, search text, and optional availability. The query builder must compose these conditions safely without producing invalid SQL for empty filter lists.
Deep Dive
The whereIn method must handle the empty array edge case. An empty IN () is invalid SQL in every database:
php<?php public function whereIn(string $column, array $values): self { if (empty($values)) { // Empty IN = impossible condition = no results return $this->whereRaw('1 = 0'); } $clone = clone $this; $placeholders = []; foreach ($values as $i => $value) { $key = ':wherein_' . count($clone->wheres) . '_' . $i; $placeholders[] = $key; $clone->bindings[$key] = $value; } $clone->wheres[] = "$column IN (" . implode(', ', $placeholders) . ')'; return $clone; }
The whereRaw('1 = 0') trick ensures the query returns zero rows instead of crashing with a syntax error.
Similarly, whereNotIn with an empty array should return all rows (no exclusions):
php<?php public function whereNotIn(string $column, array $values): self { if (empty($values)) { return $this; // Nothing to exclude } $clone = clone $this; $placeholders = []; foreach ($values as $i => $value) { $key = ':wherenotin_' . count($clone->wheres) . '_' . $i; $placeholders[] = $key; $clone->bindings[$key] = $value; } $clone->wheres[] = "$column NOT IN (" . implode(', ', $placeholders) . ')'; return $clone; }
NULL handling requires special SQL syntax since = NULL is always false:
php<?php public function whereNull(string $column): self { $clone = clone $this; $clone->wheres[] = "$column IS NULL"; return $clone; } public function whereNotNull(string $column): self { $clone = clone $this; $clone->wheres[] = "$column IS NOT NULL"; return $clone; }
Grouped conditions allow complex OR logic:
php<?php public function whereGroup(callable $callback): self { $clone = clone $this; $group = new self($this->pdo); $callback($group); if ($group->wheres) { $clone->wheres[] = '(' . implode(' AND ', $group->wheres) . ')'; $clone->bindings = array_merge($clone->bindings, $group->bindings); } return $clone; } // Usage $results = $qb->table('users') ->where('status', 'active') ->whereGroup(function($q) { $q->where('role', 'admin'); $q->orWhere('role', 'moderator'); }) ->get(); // WHERE status = :w0 AND (role = :w1 OR role = :w2)
LIKE queries for text search:
php<?php public function whereLike(string $column, string $pattern): self { $clone = clone $this; $placeholder = ':like_' . count($clone->wheres); $clone->wheres[] = "$column LIKE $placeholder"; $clone->bindings[$placeholder] = $pattern; return $clone; } $users = $qb->table('users') ->whereLike('email', '%@gmail.com') ->get();
Common Pitfalls
- Not handling empty arrays in IN clauses — Passing an empty array to
WHERE id IN ()produces a SQL syntax error. Always check for empty and short-circuit. - Using
= NULLinstead ofIS NULL— In SQL,NULL = NULLevaluates to NULL (falsy), not true. You must useIS NULLandIS NOT NULL.
Best Practices
- Short-circuit empty IN clauses explicitly — Use
1 = 0for emptyIN(no results) and skip the clause entirely for emptyNOT IN(all results). - Validate LIKE patterns — If the pattern comes from user input, escape
%and_characters unless you intentionally want wildcard behavior.
Summary
- Handle the empty array edge case in IN clauses to prevent SQL syntax errors.
- Use
IS NULL/IS NOT NULLinstead of equality checks for NULL values. - Grouped conditions with callbacks enable complex OR logic with correct parenthesization.
- Always escape or validate user-provided LIKE patterns.
Code Examples
<?php
declare(strict_types=1);
class QueryBuilder {
private string $table = '';
private array $columns = ['*'];
private array $wheres = [];
private array $bindings = [];
private array $orderBy = [];
private array $joins = [];
private ?int $limit = null;
private ?int $offset = null;
public function __construct(private PDO $pdo) {}
public function table(string $table): self {
$clone = clone $this;
$clone->table = $table;
return $clone;
}
public function whereIn(string $column, array $values): self {
if (empty($values)) {
return $this->whereRaw('1 = 0');
}
$clone = clone $this;
$keys = [];
foreach ($values as $i => $value) {
$key = ':in' . count($clone->bindings) . '_' . $i;
$keys[] = $key;
$clone->bindings[$key] = $value;
}
$clone->wheres[] = "$column IN (" . implode(', ', $keys) . ')';
return $clone;
}
public function whereRaw(string $sql): self {
$clone = clone $this;
$clone->wheres[] = $sql;
return $clone;
}
}
?>