EasyQuery
knifelemon/easy-query is a lightweight, fluent SQL query builder that generates SQL and parameters for prepared statements. Works with SimplePdo.
Features
- 🔗 Fluent API - Chain methods for readable query construction
- 🛡️ SQL Injection Protection - Automatic parameter binding with prepared statements
- 🔧 Raw SQL Support - Insert raw SQL expressions with
raw() - 📝 Multiple Query Types - SELECT, INSERT, UPDATE, DELETE, COUNT
- 🔀 JOIN Support - INNER, LEFT, RIGHT joins with aliases
- 🎯 Advanced Conditions - LIKE, IN, NOT IN, BETWEEN, comparison operators
- 🌐 Database Agnostic - Returns SQL + params, use with any DB connection
- 🪶 Lightweight - Minimal footprint with zero dependencies
Installation
composer require knifelemon/easy-queryQuick Start
use KnifeLemon\EasyQuery\Builder;
$q = Builder::table('users')
->select(['id', 'name', 'email'])
->where(['status' => 'active'])
->orderBy('created_at DESC')
->limit(10)
->build();
// Use with Flight's SimplePdo
$users = Flight::db()->fetchAll($q['sql'], $q['params']);Understanding build()
The build() method returns an array with sql and params. This separation keeps your database safe by using prepared statements.
$q = Builder::table('users')
->where(['email' => 'user@example.com'])
->build();
// Returns:
// [
// 'sql' => 'SELECT * FROM users WHERE email = ?',
// 'params' => ['user@example.com']
// ]Query Types
SELECT
// Select all columns
$q = Builder::table('users')->build();
// SELECT * FROM users
// Select specific columns
$q = Builder::table('users')
->select(['id', 'name', 'email'])
->build();
// SELECT id, name, email FROM users
// With table alias
$q = Builder::table('users')
->alias('u')
->select(['u.id', 'u.name'])
->build();
// SELECT u.id, u.name FROM users AS uINSERT
$q = Builder::table('users')
->insert([
'name' => 'John Doe',
'email' => 'john@example.com',
'status' => 'active'
])
->build();
// INSERT INTO users SET name = ?, email = ?, status = ?
Flight::db()->runQuery($q['sql'], $q['params']);
$userId = Flight::db()->lastInsertId();UPDATE
$q = Builder::table('users')
->update(['status' => 'inactive', 'updated_at' => date('Y-m-d H:i:s')])
->where(['id' => 123])
->build();
// UPDATE users SET status = ?, updated_at = ? WHERE id = ?
Flight::db()->runQuery($q['sql'], $q['params']);DELETE
$q = Builder::table('users')
->delete()
->where(['id' => 123])
->build();
// DELETE FROM users WHERE id = ?
Flight::db()->runQuery($q['sql'], $q['params']);COUNT
$q = Builder::table('users')
->count()
->where(['status' => 'active'])
->build();
// SELECT COUNT(*) AS cnt FROM users WHERE status = ?
$count = Flight::db()->fetchField($q['sql'], $q['params']);WHERE Conditions
Simple Equality
$q = Builder::table('users')
->where(['id' => 123, 'status' => 'active'])
->build();
// WHERE id = ? AND status = ?Comparison Operators
$q = Builder::table('users')
->where([
'age' => ['>=', 18],
'score' => ['<', 100],
'name' => ['!=', 'admin']
])
->build();
// WHERE age >= ? AND score < ? AND name != ?LIKE
$q = Builder::table('users')
->where(['name' => ['LIKE', '%john%']])
->build();
// WHERE name LIKE ?IN / NOT IN
// IN
$q = Builder::table('users')
->where(['id' => ['IN', [1, 2, 3, 4, 5]]])
->build();
// WHERE id IN (?, ?, ?, ?, ?)
// NOT IN
$q = Builder::table('users')
->where(['status' => ['NOT IN', ['banned', 'deleted']]])
->build();
// WHERE status NOT IN (?, ?)BETWEEN
$q = Builder::table('products')
->where(['price' => ['BETWEEN', [100, 500]]])
->build();
// WHERE price BETWEEN ? AND ?OR Conditions
Use orWhere() to add OR grouped conditions:
$q = Builder::table('users')
->where(['status' => 'active'])
->orWhere([
'role' => 'admin',
'permissions' => ['LIKE', '%manage%']
])
->build();
// WHERE status = ? AND (role = ? OR permissions LIKE ?)JOIN
INNER JOIN
$q = Builder::table('users')
->alias('u')
->select(['u.id', 'u.name', 'p.title'])
->innerJoin('posts', 'u.id = p.user_id', 'p')
->build();
// SELECT u.id, u.name, p.title FROM users AS u INNER JOIN posts AS p ON u.id = p.user_idLEFT JOIN
$q = Builder::table('users')
->alias('u')
->select(['u.name', 'o.total'])
->leftJoin('orders', 'u.id = o.user_id', 'o')
->build();
// ... LEFT JOIN orders AS o ON u.id = o.user_idMultiple JOINs
$q = Builder::table('orders')
->alias('o')
->select(['o.id', 'u.name AS customer', 'p.title AS product'])
->innerJoin('users', 'o.user_id = u.id', 'u')
->leftJoin('order_items', 'o.id = oi.order_id', 'oi')
->leftJoin('products', 'oi.product_id = p.id', 'p')
->where(['o.status' => 'completed'])
->build();Ordering, Grouping, and Limits
ORDER BY
$q = Builder::table('users')
->orderBy('created_at DESC')
->build();
// ORDER BY created_at DESCGROUP BY
$q = Builder::table('orders')
->select(['user_id', 'COUNT(*) as order_count'])
->groupBy('user_id')
->build();
// SELECT user_id, COUNT(*) as order_count FROM orders GROUP BY user_idLIMIT and OFFSET
$q = Builder::table('users')
->limit(10)
->build();
// LIMIT 10
$q = Builder::table('users')
->limit(10, 20) // limit, offset
->build();
// LIMIT 10 OFFSET 20Raw SQL Expressions
Use raw() when you need SQL functions or expressions that shouldn't be treated as bound parameters.
Basic Raw
$q = Builder::table('users')
->update([
'login_count' => Builder::raw('login_count + 1'),
'updated_at' => Builder::raw('NOW()')
])
->where(['id' => 123])
->build();
// SET login_count = login_count + 1, updated_at = NOW()Raw with Bound Parameters
$q = Builder::table('orders')
->update([
'total' => Builder::raw('COALESCE(subtotal, ?) + ?', [0, 10])
])
->where(['id' => 1])
->build();
// SET total = COALESCE(subtotal, ?) + ?
// params: [0, 10, 1]Raw in WHERE (Subquery)
$q = Builder::table('products')
->where([
'price' => ['>', Builder::raw('(SELECT AVG(price) FROM products)')]
])
->build();
// WHERE price > (SELECT AVG(price) FROM products)Safe Identifiers for User Input
When column names come from user input, use safeIdentifier() to prevent SQL injection:
$sortColumn = $_GET['sort']; // e.g., 'created_at'
$safeColumn = Builder::safeIdentifier($sortColumn);
$q = Builder::table('users')
->orderBy($safeColumn . ' DESC')
->build();
// If user tries: "name; DROP TABLE users--"
// Throws InvalidArgumentExceptionrawSafe for User-Provided Column Names
$userColumn = $_GET['aggregate_column'];
$q = Builder::table('orders')
->select([
Builder::rawSafe('SUM({col})', ['col' => $userColumn])->value . ' AS total'
])
->build();
// Validates column name, throws exception if invalidWarning: Never concatenate user input directly into
raw(). Always use bound parameters orsafeIdentifier().
Query Builder Reuse
Clear Methods
Clear specific parts to reuse the builder:
$query = Builder::table('users')
->select(['id', 'name'])
->where(['status' => 'active'])
->orderBy('created_at DESC');
// First query
$q1 = $query->limit(10)->build();
// Clear and reuse
$query->clearWhere()->clearLimit();
// Second query with different conditions
$q2 = $query
->where(['status' => 'pending'])
->limit(5)
->build();Available Clear Methods
| Method | Description |
|---|---|
clearWhere() |
Clear WHERE conditions and parameters |
clearSelect() |
Reset SELECT columns to default '*' |
clearJoin() |
Clear all JOIN clauses |
clearGroupBy() |
Clear GROUP BY clause |
clearOrderBy() |
Clear ORDER BY clause |
clearLimit() |
Clear LIMIT and OFFSET |
clearAll() |
Reset builder to initial state |
Pagination Example
$baseQuery = Builder::table('users')
->select(['id', 'name', 'email'])
->where(['status' => 'active'])
->orderBy('created_at DESC');
// Get total count
$countQuery = clone $baseQuery;
$countResult = $countQuery->clearSelect()->count()->build();
$total = Flight::db()->fetchField($countResult['sql'], $countResult['params']);
// Get paginated results
$page = 1;
$perPage = 20;
$listResult = $baseQuery->limit($perPage, ($page - 1) * $perPage)->build();
$users = Flight::db()->fetchAll($listResult['sql'], $listResult['params']);Dynamic Query Building
$query = Builder::table('products')->alias('p');
if (!empty($categoryId)) {
$query->where(['p.category_id' => $categoryId]);
}
if (!empty($minPrice)) {
$query->where(['p.price' => ['>=', $minPrice]]);
}
if (!empty($maxPrice)) {
$query->where(['p.price' => ['<=', $maxPrice]]);
}
if (!empty($searchTerm)) {
$query->where(['p.name' => ['LIKE', "%{$searchTerm}%"]]);
}
$result = $query->orderBy('p.created_at DESC')->limit(20)->build();
$products = Flight::db()->fetchAll($result['sql'], $result['params']);Full FlightPHP Example
use KnifeLemon\EasyQuery\Builder;
// List users with pagination
Flight::route('GET /users', function() {
$page = (int) (Flight::request()->query['page'] ?? 1);
$perPage = 20;
$q = Builder::table('users')
->select(['id', 'name', 'email', 'created_at'])
->where(['status' => 'active'])
->orderBy('created_at DESC')
->limit($perPage, ($page - 1) * $perPage)
->build();
$users = Flight::db()->fetchAll($q['sql'], $q['params']);
Flight::json(['users' => $users, 'page' => $page]);
});
// Create user
Flight::route('POST /users', function() {
$data = Flight::request()->data;
$q = Builder::table('users')
->insert([
'name' => $data->name,
'email' => $data->email,
'created_at' => Builder::raw('NOW()')
])
->build();
Flight::db()->runQuery($q['sql'], $q['params']);
Flight::json(['id' => Flight::db()->lastInsertId()]);
});
// Update user
Flight::route('PUT /users/@id', function($id) {
$data = Flight::request()->data;
$q = Builder::table('users')
->update([
'name' => $data->name,
'email' => $data->email,
'updated_at' => Builder::raw('NOW()')
])
->where(['id' => $id])
->build();
Flight::db()->runQuery($q['sql'], $q['params']);
Flight::json(['success' => true]);
});
// Delete user
Flight::route('DELETE /users/@id', function($id) {
$q = Builder::table('users')
->delete()
->where(['id' => $id])
->build();
Flight::db()->runQuery($q['sql'], $q['params']);
Flight::json(['success' => true]);
});API Reference
Static Methods
| Method | Description |
|---|---|
Builder::table(string $table) |
Create a new builder instance for the table |
Builder::raw(string $sql, array $bindings = []) |
Create a raw SQL expression |
Builder::rawSafe(string $expr, array $identifiers, array $bindings = []) |
Raw expression with safe identifier substitution |
Builder::safeIdentifier(string $identifier) |
Validate and return a safe column/table name |
Instance Methods
| Method | Description |
|---|---|
alias(string $alias) |
Set table alias |
select(string\|array $columns) |
Set columns to select (default: '*') |
where(array $conditions) |
Add WHERE conditions (AND) |
orWhere(array $conditions) |
Add OR WHERE conditions |
join(string $table, string $condition, string $alias, string $type) |
Add JOIN clause |
innerJoin(string $table, string $condition, string $alias) |
Add INNER JOIN |
leftJoin(string $table, string $condition, string $alias) |
Add LEFT JOIN |
groupBy(string $groupBy) |
Add GROUP BY clause |
orderBy(string $orderBy) |
Add ORDER BY clause |
limit(int $limit, int $offset = 0) |
Add LIMIT and OFFSET |
count(string $column = '*') |
Set query to COUNT |
insert(array $data) |
Set query to INSERT |
update(array $data) |
Set query to UPDATE |
delete() |
Set query to DELETE |
build() |
Build and return ['sql' => ..., 'params' => ...] |
get() |
Alias for build() |
Tracy Debugger Integration
EasyQuery automatically integrates with Tracy Debugger if installed. No setup required!
composer require tracy/tracyuse Tracy\Debugger;
Debugger::enable();
// All queries are automatically logged to Tracy panel
$q = Builder::table('users')->where(['status' => 'active'])->build();The Tracy panel shows:
- Total queries and breakdown by type
- Generated SQL (syntax highlighted)
- Parameters array
- Query details (table, where, joins, etc.)
For full documentation, visit the GitHub repository.