initorm / query-builder
Lightweight, dialect-aware SQL query builder for PHP with parameterized output suitable for PDO.
Fund package maintenance!
Requires
- php: ^8.1
- ext-pdo: *
Requires (Dev)
- phpstan/phpstan: ^1.12
- phpunit/phpunit: ^10.5
- squizlabs/php_codesniffer: ^3.10
This package is not auto-updated.
Last update: 2026-05-24 17:54:07 UTC
README
A lightweight, dialect-aware SQL query builder for PHP. It turns fluent calls into a SQL string plus a separate parameter bag suitable for direct execution with PDO — without ever concatenating user values into SQL.
InitORM QueryBuilder is the lowest layer of the InitORM
package family; it has no runtime dependencies beyond the pdo extension
and is designed to be used either standalone or as part of the
initorm/database and initorm/orm stack.
Why this library
- Safe by default — every value goes through a collision-safe parameter
bag. Raw fragments are opt-in via
RawQuery. - Dialect aware — identifier escaping is delegated to pluggable drivers for MySQL/MariaDB, PostgreSQL, SQLite, plus a no-op generic driver.
- Tiny and predictable — single namespace, no service container, no reflection, no annotations; the whole thing is around 1 600 lines of code.
- Battle-tested clause DSL — comparison operators, BETWEEN, IN, LIKE
family (
like/startLike/endLike), NULL checks, REGEXP, SOUNDEX, FIND_IN_SET, sub-queries, parenthesized groups, closure-based JOIN ON expressions.
Requirements
- PHP ≥ 8.1
ext-pdo(only needed by the consumer at execution time; the builder itself does not require an open connection)
Installation
composer require initorm/query-builder
Quick start
use InitORM\QueryBuilder\QueryBuilder; $qb = new QueryBuilder('mysql'); $sql = $qb ->select('u.id', 'u.name') ->from('users AS u') ->where('u.status', 1) ->andWhere('u.country', 'TR') ->orderBy('u.id', 'DESC') ->limit(20) ->generateSelectQuery(); // $sql ───────────────────────────────────────────────────────────────── // SELECT `u`.`id`, `u`.`name` // FROM `users` AS `u` // WHERE `u`.`status` = 1 AND `u`.`country` = :country // ORDER BY `u`.`id` DESC // LIMIT 20 $pdo = new PDO('mysql:host=localhost;dbname=app', 'app', 'secret'); $stmt = $pdo->prepare($sql); $stmt->execute($qb->getParameter()->all()); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
INSERT
$qb->from('users')->set([ 'name' => 'Muhammet', 'email' => 'info@muhammetsafak.com.tr', 'created' => $qb->raw('NOW()'), ]); echo $qb->generateInsertQuery(); // INSERT INTO `users` (`name`, `email`, `created`) // VALUES (:name, :email, NOW());
Sub-query in WHERE IN
$qb->select('u.name') ->from('users AS u') ->whereIn('u.id', $qb->subQuery(function (QueryBuilder $sub) { $sub->select('id')->from('roles')->where('name', 'admin'); })); // SELECT `u`.`name` FROM `users` AS `u` // WHERE `u`.`id` IN (SELECT `id` FROM `roles` WHERE `name` = :name)
Closure-based JOIN ON
$qb->select('p.title', 'u.name') ->from('posts AS p') ->innerJoin('users AS u', function (QueryBuilder $j) { $j->on('u.id', 'p.user_id') ->where('u.active', 1); });
Batch UPDATE (CASE/WHEN)
$qb->from('posts') ->set(['id' => 1, 'title' => 'First', 'views' => 100]) ->set(['id' => 2, 'title' => 'Second', 'views' => 42]); echo $qb->generateUpdateBatchQuery('id'); // UPDATE `posts` // SET `title` = CASE WHEN `id` = 1 THEN :title WHEN `id` = 2 THEN :title_1 ELSE `title` END, // `views` = CASE WHEN `id` = 1 THEN 100 WHEN `id` = 2 THEN 42 ELSE `views` END // WHERE `id` IN (1, 2)
Supported drivers
| String | Driver class | Escape char |
|---|---|---|
'mysql' |
Drivers\MySqlDriver |
` |
'pgsql' / 'postgres' / 'postgresql' |
Drivers\PostgreSqlDriver |
" |
'sqlite' |
Drivers\SqliteDriver |
` |
null (or anything unknown) |
Drivers\GenericDriver (no quoting) |
(none) |
A custom dialect can be added by extending Drivers\AbstractDriver and
setting the NAME and ESCAPE_CHAR class constants.
Documentation
Full developer documentation with runnable examples lives in
docs/ — see docs/en/index.md for the table
of contents.
Security
InitORM QueryBuilder is built around the rule "user input is a value, never an identifier or a SQL fragment". Defenses shipped in 2.0.0:
- Identifier hardening —
escapeIdentifier()rejects;and--so query-breakout characters in a column or table name cannot survive the escape pass (relevant especially on PostgreSQL, where PDO allows multi-statement queries by default). - LIKE wildcard auto-escape —
%,_, and\inside user-supplied LIKE values are escaped by default. Opt out with$qb->raw(...)when raw wildcards are intentional. - Strict placeholder regex — placeholder names are now tightly bound
to
^:\w+$. - FIND_IN_SET parameter fix (B28) — a pre-2.0.0 inversion bug inlined raw user strings as SQL; fixed.
The full threat model, residual application-level concerns
(ORDER BY whitelisting, value-shaped function detection), and a complete
regression suite live in docs/en/security.md and
tests/SecurityTest.php.
Report vulnerabilities through the organization-wide security policy.
Tests, lint, static analysis
composer install composer test # phpunit (with pcov line-coverage summary) composer cs # PHP_CodeSniffer (PSR-12) composer cs-fix # phpcbf — auto-fix style violations composer stan # PHPStan level 6 composer qa # cs-ci + stan + test
The repository ships with GitHub Actions workflows under
.github/workflows/ that run the same checks on every
push and pull request, across the PHP 8.1 → 8.4 matrix.
Current numbers: 293 tests / 391 assertions / 96.46 % line coverage.
Contributing
The contribution workflow, code style, and pull-request template are shared across the InitORM organization. See InitORM/.github → CONTRIBUTING and the PR template. A short summary:
- Branch from
master. - Stick to PSR-12; run
composer qabefore opening a PR. - Add tests for new behavior — the test suite is the contract.
- Reference issues with
Fixes #123/Refs #123.
Security issues should follow the disclosure process in InitORM/.github → SECURITY.
Versioning
This package follows Semantic Versioning. The behavioral and structural changes between 1.x and 2.x are listed in CHANGELOG.md.
License
MIT — see LICENSE.
Credits
Authored and maintained by Muhammet ŞAFAK <info@muhammetsafak.com.tr>. Issues and contributions are welcome on GitHub.