solophp/query-builder

A lightweight, fluent SQL query builder for PHP

v2.1.0 2025-05-09 10:31 UTC

This package is auto-updated.

Last update: 2025-05-09 10:32:24 UTC


README

A lightweight and flexible SQL query builder for PHP 8.2+ with support for multiple SQL dialects.

Version License

Features

  • 🚀 Fast and lightweight SQL builder with zero external dependencies
  • 💪 PHP 8.2+ support with strict typing
  • 🔒 Secure parameterized queries for protection against SQL injections
  • 🧩 Intuitive fluent interface for building queries
  • 🔄 Support for different DBMS (MySQL, PostgreSQL, SQLite) with extensibility
  • ⚡️ Optional PSR-16 caching of SELECT query results (local and global control)
  • 🧩 Advanced features: subqueries, raw SQL expressions, conditional queries, DISTINCT selection
  • 🔄 Array handling in WHERE conditions with whereIn, orWhereIn, havingIn, and orHavingIn methods

Installation

composer require solophp/query-builder

Quick Start

use Solo\QueryBuilder\Utility\QueryFactory;
use Solo\QueryBuilder\Facade\Query;

// Simple initialization using helper factory
$query = QueryFactory::createWithPdo('localhost', 'username', 'password', 'database');

// Enable global cache (ExampleCache, TTL 600 seconds)
use ExampleCache;
Query::enableCache(new ExampleCache(), 600);

// Select data
$results = $query->from('users')
    ->select('id', 'name', 'email')
    ->where('status = ?', 'active')
    ->orderBy('created_at', 'DESC')
    ->limit(10)
    ->getAllAssoc();

// Insert data
$insertId = $query->insert('users')
    ->values([
        'name' => 'John Doe', 
        'email' => 'john@example.com', 
        'created_at' => date('Y-m-d H:i:s')
    ])
    ->insertGetId();

// Update data
$affectedRows = $query->update('users')
    ->set('status', 'inactive')
    ->set('updated_at', date('Y-m-d H:i:s'))
    ->where('last_login < ?', date('Y-m-d', strtotime('-6 months')))
    ->execute();

// Delete data
$affectedRows = $query->delete('users')
    ->where('id = ?', 5)
    ->execute();

Caching (PSR-16)

This library supports caching SELECT query results via any PSR-16 cache implementation:

  • Global cache: call Query::enableCache($cache, $ttl) once in your bootstrap to apply caching to all queries.
  • Per-instance cache: chain ->withCache($cache, $ttl) on a Query instance to override or set caching locally.
  • Disable cache: use Query::disableCache() to turn off global caching.
use Solo\QueryBuilder\Facade\Query;
use ExampleCache;

// Global cache for all Query instances (TTL 600s)
Query::enableCache(new ExampleCache(), 600);

// Local override (TTL 300s)
$query = $query->withCache(new ExampleCache(), 300);
$users = $query->from('users')->getAllAssoc();

// Disable global cache entirely
Query::disableCache();

Manual Initialization

If you need more control over the initialization process, you can create all components manually:

use Solo\QueryBuilder\Facade\Query;
use Solo\QueryBuilder\Executors\PdoExecutor\PdoExecutor;
use Solo\QueryBuilder\Executors\PdoExecutor\Connection;
use Solo\QueryBuilder\Executors\PdoExecutor\Config;
use Solo\QueryBuilder\Factory\BuilderFactory;
use Solo\QueryBuilder\Factory\GrammarFactory;

// Create factories
$grammarFactory = new GrammarFactory();

// Create PDO executor
$config = new Config(
    'localhost',        // host
    'username',         // username
    'password',         // password
    'database',         // database
    PDO::FETCH_ASSOC,   // fetchMode
    'mysql',            // driver
    null,               // port (optional)
    []                  // options (optional)
);

$connection = new Connection($config);
$executor = new PdoExecutor($connection);

// Creating a BuilderFactory with executor
$builderFactory = new BuilderFactory($grammarFactory, $executor, 'mysql');

// Creating a Query instance
$query = new Query($builderFactory);

Building without Executing

You can also build queries without executing them:

// Build a query without executing
[$sql, $bindings] = $query->from('users')
    ->select('id', 'name')
    ->where('status = ?', 'active')
    ->build();

// Now you have the SQL string and parameter bindings
echo $sql;
// SELECT `id`, `name` FROM `users` WHERE status = ?

print_r($bindings);
// ['active']

Multi-DBMS Support

The library implements SQL grammar abstraction, allowing you to work with different database systems using the same API.

Setting Default DBMS

// Set MySQL as default grammar
$query->setDatabaseType('mysql');

// Set PostgreSQL as default grammar
$query->setDatabaseType('postgresql'); // or 'postgres', 'pgsql'

// Set SQLite as default grammar
$query->setDatabaseType('sqlite');

SELECT Queries

Basic Selection Operations

// Select all records from table
$allUsers = $query->from('users')->getAllAssoc();

// Select specific columns
$users = $query->from('users')
    ->select('id', 'name', 'email')
    ->getAllAssoc();

// Use DISTINCT to select only unique values
$uniqueCities = $query->from('users')
    ->select('city')
    ->distinct()
    ->getAllAssoc();

// WHERE conditions
$activeUsers = $query->from('users')
    ->where('status = ?', 'active')
    ->getAllAssoc();

// Multiple conditions
$recentActiveUsers = $query->from('users')
    ->where('status = ?', 'active')
    ->where('created_at > ?', '2023-01-01')
    ->getAllAssoc();

// WHERE IN condition
$specificUsers = $query->from('users')
    ->whereIn('id', [1, 2, 3])
    ->getAllAssoc();

// OR WHERE IN condition
$usersWithRoles = $query->from('users')
    ->where('status = ?', 'active')
    ->orWhereIn('role', ['admin', 'editor'])
    ->getAllAssoc();

// Sorting
$sortedUsers = $query->from('users')
    ->orderBy('name')                // ASC by default
    ->addOrderBy('created_at', 'DESC') // additional sorting
    ->getAllAssoc();

// Limit and offset
$paginatedUsers = $query->from('users')
    ->limit(10, 100) // 10 records starting from offset 100
    ->getAllAssoc();
    
// Pagination with page
$paginatedUsers = $query->from('users')
    ->paginate(25, 1) // 25 records per page, page 1
    ->getAllAssoc();
    
// Get a single record
$user = $query->from('users')
    ->where('id = ?', 1)
    ->getAssoc();
    
// Get records as objects
$userObjects = $query->from('users')
    ->where('status = ?', 'active')
    ->getAllObj();
    
// Get a single value
$count = $query->from('users')
    ->select('COUNT(*) as count')
    ->getValue();
    
// Get an array of email addresses
$emails = $query->from('users')
    ->select('id', 'email', 'name')
    ->where('status = ?', 'active')
    ->getColumn('email');
// Result: ['john@example.com', 'jane@example.com', 'bob@example.com']

// Get an associative array of [id => name]
$userNames = $query->from('users')
    ->select('id', 'name')
    ->getColumn('name', 'id');
// Result: [1 => 'John', 2 => 'Jane', 3 => 'Bob']

// Basic HAVING
$orderStats = $query->from('orders')
    ->select('user_id', '{COUNT(*) as order_count}')
    ->groupBy('user_id')
    ->having('order_count > ?', 5)
    ->getAllAssoc();

// HAVING IN
$orderStats = $query->from('orders')
    ->select('user_id', '{COUNT(*) as order_count}')
    ->groupBy('user_id')
    ->havingIn('user_id', [1, 2, 3])
    ->getAllAssoc();

// Combined HAVING conditions
$orderStats = $query->from('orders')
    ->select('user_id', '{COUNT(*) as order_count}')
    ->groupBy('user_id')
    ->having('order_count > ?', 10)
    ->orHavingIn('user_id', [5, 6, 7])
    ->getAllAssoc();

// Get prices from products
$prices = $query->from('products')
    ->select('id', 'name', 'price')
    ->where('category_id = ?', 5)
    ->getColumn('price');
// Result: [19.99, 24.99, 14.50]

// Count the number of records
$totalUsers = $query->from('users')->count();

// Count records with conditions
$activeUserCount = $query->from('users')
    ->where('status = ?', 'active')
    ->count();

// Count specific fields or unique values
$emailCount = $query->from('users')->count('email'); // Count of non-NULL emails
$uniqueCities = $query->from('users')->count('city', true); // Count of unique cities

Raw SQL Expressions

You can use raw SQL expressions by enclosing them in curly braces {...}:

// Raw expressions in select
$users = $query->from('users')
    ->select('id', 'name', '{CONCAT(first_name, " ", last_name) as full_name}')
    ->getAllAssoc();

// Aggregation functions
$userStats = $query->from('orders')
    ->select('user_id', '{COUNT(*) as order_count}', '{SUM(amount) as total_spend}')
    ->groupBy('user_id')
    ->having('total_spend > ?', 1000)
    ->getAllAssoc();

// Date functions
$ordersByMonth = $query->from('orders')
    ->select('id', '{DATE_FORMAT(created_at, "%Y-%m") as month}', 'status')
    ->where('created_at >= ?', '2023-01-01')
    ->getAllAssoc();

Conditional Queries with when()

The when() method allows you to add clauses to your query conditionally:

// Only apply where clause if condition is true
$email = 'test@example.com';
$status = null;

$users = $query->from('users')
    ->when($email !== null, function($q) use ($email) {
        return $q->where('email = ?', $email);
    })
    ->when($status !== null, function($q) use ($status) {
        return $q->where('status = ?', $status);
    })
    ->getAllAssoc();

// Apply a default callback when condition is false
$minPrice = null;
$defaultMinPrice = 10;

$products = $query->from('products')
    ->when($minPrice !== null,
        function($q) use ($minPrice) {
            return $q->where('price >= ?', $minPrice);
        },
        function($q) use ($defaultMinPrice) {
            return $q->where('price >= ?', $defaultMinPrice);
        }
    )
    ->getAllAssoc();

JOIN Operations

// INNER JOIN
$ordersWithUsers = $query->from('orders')
    ->select('orders.id', 'orders.amount', 'users.name')
    ->join('users', 'orders.user_id = users.id')
    ->getAllAssoc();
    
// LEFT JOIN
$usersWithProfiles = $query->from('users')
    ->select('users.id', 'users.name', 'profiles.bio')
    ->leftJoin('profiles', 'users.id = profiles.user_id')
    ->getAllAssoc();

// RIGHT JOIN
$usersWithOrders = $query->from('orders')
    ->select('orders.id', 'users.name')
    ->rightJoin('users', 'orders.user_id = users.id')
    ->getAllAssoc();

// FULL JOIN
$allUsersProfiles = $query->from('users')
    ->select('users.id', 'profiles.bio')
    ->fullJoin('profiles', 'users.id = profiles.user_id')
    ->getAllAssoc();

Grouping and Aggregation

// GROUP BY with aggregate functions
$userOrderStats = $query->from('orders')
    ->select('user_id', '{COUNT(*) as order_count}', '{SUM(amount) as total_spend}')
    ->groupBy('user_id')
    ->having('total_spend > ?', 1000)
    ->getAllAssoc();

INSERT Queries

// Insert one record and get ID
$userId = $query->insert('users')
    ->values([
        'name' => 'John Doe', 
        'email' => 'john@example.com', 
        'created_at' => date('Y-m-d H:i:s')
    ])
    ->insertGetId();

// Insert one record and get affected rows
$affectedRows = $query->insert('users')
    ->values([
        'name' => 'John Doe', 
        'email' => 'john@example.com', 
        'created_at' => date('Y-m-d H:i:s')
    ])
    ->execute();

// Insert multiple records
$affectedRows = $query->insert('logs')
    ->values([
        ['user_id' => 1, 'action' => 'login', 'created_at' => date('Y-m-d H:i:s')],
        ['user_id' => 2, 'action' => 'logout', 'created_at' => date('Y-m-d H:i:s')]
    ])
    ->execute();

UPDATE Queries

// Update with array of values
$affectedRows = $query->update('users')
    ->set([
        'status' => 'inactive',
        'updated_at' => date('Y-m-d H:i:s')
    ])
    ->where('last_login < ?', date('Y-m-d', strtotime('-6 months')))
    ->execute();

// Or update by setting fields individually
$affectedRows = $query->update('users')
    ->set('status', 'inactive')
    ->set('updated_at', date('Y-m-d H:i:s'))
    ->where('id = ?', 5)
    ->execute();

DELETE Queries

// Delete with condition
$affectedRows = $query->delete('expired_tokens')
    ->where('expires_at < ?', date('Y-m-d H:i:s'))
    ->execute();

// Delete by ID
$affectedRows = $query->delete('users')
    ->where('id = ?', 5)
    ->execute();

Checking for Records

// Check if records exist
$exists = $query->from('users')
    ->where('status = ?', 'active')
    ->exists();

Transaction Support

try {
    $query->beginTransaction();
    
    // Perform multiple operations
    $query->insert('users')->values(['name' => 'John'])->execute();
    $query->update('stats')->set('user_count', '{user_count + 1}')->execute();
    
    $query->commit();
} catch (\Exception $e) {
    $query->rollBack();
    throw $e;
}

API Reference

Query Methods

Method Description
from(string $table) Sets the table to select from
select(string ...$columns) Sets the columns to select
distinct(bool $value = true) Enables or disables DISTINCT selection
insert(string $table) Starts an insert query
update(string $table) Starts an update query
delete(string $table) Starts a delete query
setDatabaseType(string $type) Sets the database type (mysql, postgresql, sqlite)

Where Conditions

Method Description
where(string|\Closure $expr, mixed ...$bindings) Adds a WHERE condition
orWhere(string|\Closure $expr, mixed ...$bindings) Adds an OR WHERE condition
andWhere(string|\Closure $expr, mixed ...$bindings) Adds an AND WHERE condition
whereIn(string $column, array $values) Adds a WHERE IN condition
orWhereIn(string $column, array $values) Adds an OR WHERE IN condition
andWhereIn(string $column, array $values) Adds an AND WHERE IN condition
when(bool $condition, callable $callback, ?callable $default = null) Conditionally adds clauses

Having Conditions

Method Description
having(string|\Closure $expr, mixed ...$bindings) Adds a HAVING condition
orHaving(string|\Closure $expr, mixed ...$bindings) Adds an OR HAVING condition
andHaving(string|\Closure $expr, mixed ...$bindings) Adds an AND HAVING condition
havingIn(string $column, array $values) Adds a HAVING IN condition
orHavingIn(string $column, array $values) Adds an OR HAVING IN condition
andHavingIn(string $column, array $values) Adds an AND HAVING IN condition

Joins

Method Description
join(string $table, string $condition, mixed ...$bindings) Adds an INNER JOIN
leftJoin(string $table, string $condition, mixed ...$bindings) Adds a LEFT JOIN
rightJoin(string $table, string $condition, mixed ...$bindings) Adds a RIGHT JOIN
fullJoin(string $table, string $condition, mixed ...$bindings) Adds a FULL OUTER JOIN
joinSub(\Closure $callback, string $alias, string $condition, mixed ...$bindings) Adds a subquery join

Clauses

Method Description
groupBy(string ...$cols) Adds a GROUP BY clause
having(string|\Closure $expr, mixed ...$bindings) Adds a HAVING clause
orHaving(string|\Closure $expr, mixed ...$bindings) Adds an OR HAVING clause
orderBy(string $column, string $direction = 'ASC') Sets the ORDER BY clause
addOrderBy(string $column, string $direction = 'ASC') Adds an additional ORDER BY clause
limit(int $limit, ?int $offset = null) Adds a LIMIT clause

Count Methods

Method Description
count(?string $column = null, bool $distinct = false) Counts records

Insert Methods

Method Description
values(array $data) Sets values for insert
insertGetId() Executes the insert and returns the last insert ID
execute() Executes the insert and returns the number of affected rows

Update Methods

Method Description
set(string|array $column, mixed $value = null) Sets the column(s) to update
execute() Executes the update and returns the number of affected rows

Select Result Methods

Method Description
getAssoc() Fetches a single row as an associative array
getAllAssoc() Fetches all rows as associative arrays
getObj(string $className = 'stdClass') Fetches a single row as an object
getAllObj(string $className = 'stdClass') Fetches all rows as objects
getValue() Fetches a single value from the first column
getColumn(string $column, ?string $keyColumn = null) Fetches an array of values from a single column
paginate(int $limit, int $page = 1) Sets pagination with page number (using LIMIT and OFFSET)
exists() Checks if any rows exist
count(?string $column = null, bool $distinct = false) Counts records that match the query
build() Returns the SQL and bindings without executing

Transaction Methods

Method Description
beginTransaction() Starts a new transaction
commit() Commits the current transaction
rollBack() Rolls back the current transaction
inTransaction() Checks if a transaction is active

Requirements

  • PHP 8.2 or higher
  • PDO Extension (for database connections)

License

MIT