承接 rcalicdan/query-builder-primitives 相关项目开发

从需求分析到上线部署,全程专人跟进,保证项目质量与交付效率

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

rcalicdan/query-builder-primitives

Composer 安装命令:

composer require rcalicdan/query-builder-primitives

包简介

Immutable querybuilder building block for creating sql query builder libraries

README 文档

README

Here's the updated README with QueryLocking fully documented and integrated throughout:

***

# Query Builder Primitives

A collection of PHP traits for building immutable, fluent query builders. This library provides low-level primitives without forcing any specific implementation.

## Installation

```bash
composer require rcalicdan/query-builder-primitives

Philosophy

This library provides building blocks, not a complete query builder. You compose the traits you need to create your own custom query builder implementation.

Supported Database Drivers

  • MySQL/MariaDB
  • PostgreSQL
  • SQLite

Architecture Overview

Dependency Map

QueryBuilderCore (foundation - required)
  ↓
SqlBuilder (depends on: properties from condition/join/grouping traits)
  ↓
QueryConditions (depends on: QueryBuilderCore)
  ↓
QueryAdvancedConditions (depends on: QueryConditions, SqlBuilder)

QueryJoin (depends on: QueryBuilderCore)
QueryGrouping (depends on: QueryBuilderCore)
QueryLocking (depends on: QueryBuilderCore, SqlBuilder)
QueryDebug (depends on: all traits)

Trait Descriptions

Trait Purpose Dependencies
QueryBuilderCore Core properties and table/select methods None (foundation)
SqlBuilder Builds SQL query strings QueryBuilderCore + condition/join/grouping traits
QueryConditions Basic WHERE, HAVING, LIKE clauses QueryBuilderCore
QueryAdvancedConditions Nested conditions, EXISTS, subqueries QueryConditions, SqlBuilder
QueryJoin JOIN operations (INNER, LEFT, RIGHT, CROSS) QueryBuilderCore
QueryGrouping GROUP BY, ORDER BY, LIMIT, OFFSET QueryBuilderCore
QueryLocking Pessimistic locking (FOR UPDATE, FOR SHARE, NOWAIT, SKIP LOCKED) QueryBuilderCore, SqlBuilder
QueryDebug Debug utilities (toSql, dump, dd) All traits

Quick Start

Minimal Query Builder

<?php

namespace App\Database;

use Rcalicdan\QueryBuilderPrimitives\{
    QueryBuilderCore,
    QueryConditions,
    SqlBuilder
};

class QueryBuilder
{
    use QueryBuilderCore;
    use SqlBuilder;
    use QueryConditions;
    
    public function __construct(?string $table = null)
    {
        if ($table !== null) {
            $this->table = $table;
        }
    }
}

// Usage
$qb = new QueryBuilder();
$sql = $qb->table('users')
    ->select('id, name, email')
    ->where('status', 'active')
    ->where('age', '>=', 18)
    ->toSql();

echo $sql;
// SELECT id, name, email FROM users WHERE status = ? AND age >= ?

$bindings = $qb->getBindings();
// ['active', 18]

Full-Featured Query Builder

<?php

namespace App\Database;

use Rcalicdan\QueryBuilderPrimitives\{
    QueryBuilderCore,
    QueryConditions,
    QueryAdvancedConditions,
    QueryJoin,
    QueryGrouping,
    QueryLocking,
    QueryDebug,
    SqlBuilder
};

class FullQueryBuilder
{
    use QueryBuilderCore;
    use SqlBuilder;
    use QueryConditions;
    use QueryAdvancedConditions;
    use QueryJoin;
    use QueryGrouping;
    use QueryLocking;
    use QueryDebug;
    
    public function __construct(?string $table = null)
    {
        if ($table !== null) {
            $this->table = $table;
        }
    }
}

// Usage with advanced features
$qb = new FullQueryBuilder();
$qb->table('users')
    ->select('users.*, orders.total')
    ->leftJoin('orders', 'orders.user_id = users.id')
    ->whereGroup(function($query) {
        return $query
            ->where('status', 'active')
            ->orWhere('status', 'pending');
    })
    ->groupBy('users.id')
    ->orderBy('created_at', 'DESC')
    ->limit(10)
    ->dd(); // Debug and die

Trait Details

QueryBuilderCore

Foundation trait providing core functionality.

Properties:

  • $table - Table name
  • $select - Select columns
  • $bindings - Parameter bindings array

Methods:

table(string $table): static
select(string|array $columns): static
addSelect(string|array $columns): static
selectDistinct(string|array $columns): static
setDriver(string $driver): static  // 'mysql' | 'pgsql' | 'sqlite'

Example:

$qb->table('users')
    ->select(['id', 'name'])
    ->addSelect('email')
    ->setDriver('pgsql');

QueryConditions

Basic WHERE and HAVING clauses.

Methods:

where(string $column, mixed $operator, mixed $value): static
orWhere(string $column, mixed $operator, mixed $value): static
whereIn(string $column, array $values): static
whereNotIn(string $column, array $values): static
whereBetween(string $column, array $values): static
whereNull(string $column): static
whereNotNull(string $column): static
like(string $column, string $value, string $side = 'both'): static
having(string $column, mixed $operator, mixed $value): static
havingRaw(string $condition, array $bindings = []): static
whereRaw(string $condition, array $bindings = [], string $operator = 'AND'): static
orWhereRaw(string $condition, array $bindings = []): static
resetWhere(): static

Examples:

// Basic WHERE
$qb->where('status', 'active')
   ->where('age', '>=', 18);

// Two-argument shorthand (defaults to '=')
$qb->where('status', 'active');

// WHERE IN
$qb->whereIn('id', [1, 2, 3, 4, 5]);

// WHERE BETWEEN
$qb->whereBetween('age', [18, 65]);

// NULL checks
$qb->whereNull('deleted_at')
   ->whereNotNull('email');

// LIKE clauses
$qb->like('name', 'John', 'both');          // %John%
$qb->like('email', '@gmail.com', 'before'); // %@gmail.com
$qb->like('username', 'admin', 'after');    // admin%

// Raw WHERE
$qb->whereRaw('DATE(created_at) = CURDATE()');
$qb->whereRaw('age > ? AND status = ?', [18, 'active']);

// OR WHERE
$qb->where('status', 'active')
   ->orWhere('status', 'pending');

// HAVING
$qb->groupBy('user_id')
   ->having('COUNT(*)', '>', 5);

QueryAdvancedConditions

Advanced nested conditions and subqueries.

Dependencies: Requires QueryConditions and SqlBuilder

Methods:

whereGroup(callable $callback, string $logicalOperator = 'AND'): static
whereNested(callable $callback, string $operator = 'AND'): static
orWhereNested(callable $callback): static
whereExists(callable $callback, string $operator = 'AND'): static
whereNotExists(callable $callback, string $operator = 'AND'): static
orWhereExists(callable $callback): static
orWhereNotExists(callable $callback): static
whereSub(string $column, string $operator, callable $callback): static

Examples:

// Nested conditions with grouping
$qb->table('users')
    ->where('role', 'admin')
    ->whereGroup(function($query) {
        return $query
            ->where('status', 'active')
            ->orWhere('status', 'pending');
    });
// WHERE role = ? AND (status = ? OR status = ?)

// OR nested groups
$qb->where('type', 'premium')
    ->orWhereNested(function($query) {
        return $query
            ->where('trial_active', true)
            ->where('trial_ends_at', '>', date('Y-m-d'));
    });

// EXISTS subquery
$qb->table('users')
    ->whereExists(function($query) {
        return $query
            ->table('orders')
            ->whereRaw('orders.user_id = users.id')
            ->where('orders.total', '>', 1000);
    });
// WHERE EXISTS (SELECT * FROM orders WHERE orders.user_id = users.id AND orders.total > ?)

// NOT EXISTS
$qb->table('users')
    ->whereNotExists(function($query) {
        return $query
            ->table('orders')
            ->whereRaw('orders.user_id = users.id');
    });

// Subquery in WHERE
$qb->table('users')
    ->whereSub('total_orders', '>', function($query) {
        return $query
            ->table('orders')
            ->selectRaw('COUNT(*)')
            ->whereRaw('orders.user_id = users.id');
    });

QueryJoin

JOIN operations.

Dependencies: Requires QueryBuilderCore

Methods:

join(string $table, string $condition, string $type = 'INNER'): static
leftJoin(string $table, string $condition): static
rightJoin(string $table, string $condition): static
innerJoin(string $table, string $condition): static
crossJoin(string $table): static

Examples:

// INNER JOIN
$qb->table('users')
    ->innerJoin('profiles', 'profiles.user_id = users.id');

// LEFT JOIN
$qb->table('users')
    ->leftJoin('orders', 'orders.user_id = users.id');

// Multiple joins
$qb->table('users')
    ->leftJoin('profiles', 'profiles.user_id = users.id')
    ->leftJoin('orders', 'orders.user_id = users.id')
    ->leftJoin('payments', 'payments.order_id = orders.id');

// CROSS JOIN
$qb->table('colors')
    ->crossJoin('sizes');

QueryGrouping

Grouping, ordering, and pagination.

Dependencies: Requires QueryBuilderCore

Methods:

groupBy(string|array $columns): static
orderBy(string $column, string $direction = 'ASC'): static
orderByAsc(string $column): static
orderByDesc(string $column): static
limit(int $limit, ?int $offset = null): static
offset(int $offset): static
forPage(int $page, int $perPage = 15): static

Examples:

// GROUP BY
$qb->select('user_id, COUNT(*) as total')
    ->groupBy('user_id');

// Multiple GROUP BY
$qb->groupBy(['user_id', 'status']);

// ORDER BY
$qb->orderBy('created_at', 'DESC')
    ->orderBy('name', 'ASC');

// Shorthand
$qb->orderByDesc('created_at')
    ->orderByAsc('name');

// LIMIT and OFFSET
$qb->limit(10)
    ->offset(20);

// Or combined
$qb->limit(10, 20); // LIMIT 10 OFFSET 20

// Pagination helper
$qb->forPage(2, 25); // Page 2, 25 per page
// Equivalent to: limit(25, 25)

QueryLocking

Pessimistic locking for concurrency control within database transactions.

Dependencies: Requires QueryBuilderCore and SqlBuilder

Important: Lock clauses are only meaningful inside a database transaction. Always wrap locking queries in BEGIN / COMMIT.

Methods:

lockForUpdate(): static
lockForShare(): static
noWait(): static
skipLocked(): static
lockOf(string|array $tables): static   // PostgreSQL only
withoutLock(): static

Driver support matrix

Feature MySQL PostgreSQL SQLite
lockForUpdate() FOR UPDATE FOR UPDATE ❌ throws
lockForShare() LOCK IN SHARE MODE FOR SHARE ❌ throws
noWait() on FOR UPDATE ❌ throws
noWait() on FOR SHARE ❌ throws ❌ throws
skipLocked() on FOR UPDATE ❌ throws
skipLocked() on FOR SHARE ❌ throws ❌ throws
lockOf() ❌ throws ❌ throws

SQLite note: SQLite has no row-level locking. Use BEGIN EXCLUSIVE or BEGIN IMMEDIATE at the connection level instead.

Examples:

// Exclusive lock — no other transaction can read or modify these rows
$qb->table('orders')
    ->where('id', 1)
    ->lockForUpdate()
    ->toSql();
// MySQL/PgSQL: SELECT * FROM orders WHERE id = ? FOR UPDATE

// Shared lock — other transactions can read but not modify
$qb->table('inventory')
    ->where('product_id', 42)
    ->lockForShare()
    ->toSql();
// MySQL:  SELECT * FROM inventory WHERE product_id = ? LOCK IN SHARE MODE
// PgSQL:  SELECT * FROM inventory WHERE product_id = ? FOR SHARE

// Fail immediately if rows are already locked (MySQL 8+ / PostgreSQL)
$qb->table('orders')
    ->where('status', 'pending')
    ->lockForUpdate()
    ->noWait()
    ->toSql();
// SELECT * FROM orders WHERE status = ? FOR UPDATE NOWAIT

// Queue worker pattern — skip rows locked by other workers
$qb->table('jobs')
    ->where('status', 'pending')
    ->orderBy('created_at')
    ->limit(1)
    ->lockForUpdate()
    ->skipLocked()
    ->toSql();
// SELECT * FROM jobs WHERE status = ? ORDER BY created_at ASC LIMIT 1 FOR UPDATE SKIP LOCKED

// PostgreSQL: lock only the orders table when joining (OF clause)
$qb->table('orders')
    ->setDriver('pgsql')
    ->join('users', 'orders.user_id = users.id')
    ->lockForUpdate()
    ->lockOf('orders')
    ->toSql();
// SELECT * FROM orders INNER JOIN users ON orders.user_id = users.id FOR UPDATE OF orders

// PostgreSQL: OF with multiple tables
$qb->table('orders')
    ->setDriver('pgsql')
    ->join('items', 'orders.id = items.order_id')
    ->lockForUpdate()
    ->lockOf(['orders', 'items'])
    ->noWait()
    ->toSql();
// SELECT * FROM orders INNER JOIN items ON orders.id = items.order_id FOR UPDATE OF orders, items NOWAIT

// Remove a lock from a reused base query
$base = $qb->table('orders')->lockForUpdate();
$unlocked = $base->withoutLock();

Clause ordering

The lock clause is always appended last, after LIMIT / OFFSET:

SELECT ... FROM ... JOIN ... WHERE ... GROUP BY ... HAVING ... ORDER BY ... LIMIT ... OFFSET ... <LOCK>

QueryDebug

Debugging utilities.

Dependencies: Requires all other traits

Methods:

toSql(): string
getBindings(): array
toRawSql(): string
dump(): static
dd(): never

Examples:

// Get SQL query
$sql = $qb->table('users')
    ->where('status', 'active')
    ->toSql();
echo $sql; // SELECT * FROM users WHERE status = ?

// Get bindings
$bindings = $qb->getBindings();
var_dump($bindings); // ['active']

// Get interpolated SQL (DEBUG ONLY — never use for execution!)
$rawSql = $qb->toRawSql();
echo $rawSql; // SELECT * FROM users WHERE status = 'active'

// Dump and continue
$qb->table('users')
    ->where('status', 'active')
    ->dump() // Prints debug info
    ->where('age', '>=', 18)
    ->dump();

// Dump and die (stops execution)
$qb->table('users')
    ->where('status', 'active')
    ->dd();

// Debug output includes:
// - Formatted SQL with syntax highlighting
// - Bindings with types
// - Raw SQL with values interpolated
// - Query statistics (table, binding count, joins, conditions, etc.)

SqlBuilder

Builds SQL query strings from accumulated state.

Dependencies: Requires QueryBuilderCore and properties from condition/join/grouping traits

Protected Methods (used internally):

buildSelectQuery(): string
buildCountQuery(string $column = '*'): string
buildInsertQuery(array $data): string
buildInsertBatchQuery(array $data): string
buildUpdateQuery(array $data): string
buildDeleteQuery(): string
buildWhereClause(): string

Note: These are protected methods intended for internal use or for extending the query builder with execution methods.

Immutability

All methods return a new instance of the query builder, ensuring immutability:

$baseQuery = $qb->table('users')->where('status', 'active');

$query1 = $baseQuery->where('age', '>=', 18);
$query2 = $baseQuery->where('country', 'US');

// $baseQuery remains unchanged
// $query1 and $query2 are different queries

// Same applies to locks
$base   = $qb->table('orders')->where('status', 'pending');
$locked = $base->lockForUpdate();

// $base has no lock, $locked does

Extending with Execution

Since this is a primitive library, execution is not included. Here's how you'd add it:

<?php

namespace App\Database;

use PDO;

class ExecutableQueryBuilder extends FullQueryBuilder
{
    public function __construct(
        private PDO $pdo,
        ?string $table = null
    ) {
        parent::__construct($table);
    }
    
    public function get(): array
    {
        $sql = $this->buildSelectQuery();
        $bindings = $this->getCompiledBindings();
        
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($bindings);
        
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
    
    public function first(): ?array
    {
        $result = $this->limit(1)->get();
        return $result[0] ?? null;
    }
    
    public function count(string $column = '*'): int
    {
        $sql = $this->buildCountQuery($column);
        $bindings = $this->getCompiledBindings();
        
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($bindings);
        
        return (int) $stmt->fetchColumn();
    }
    
    public function insert(array $data): bool
    {
        $sql = $this->buildInsertQuery($data);
        $stmt = $this->pdo->prepare($sql);
        
        return $stmt->execute(array_values($data));
    }
    
    public function update(array $data): int
    {
        $sql = $this->buildUpdateQuery($data);
        $bindings = array_merge(array_values($data), $this->getCompiledBindings());
        
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($bindings);
        
        return $stmt->rowCount();
    }
    
    public function delete(): int
    {
        $sql = $this->buildDeleteQuery();
        $bindings = $this->getCompiledBindings();
        
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($bindings);
        
        return $stmt->rowCount();
    }
}

// Usage
$pdo = new PDO('mysql:host=localhost;dbname=mydb', 'user', 'pass');
$qb = new ExecutableQueryBuilder($pdo);

$users = $qb->table('users')
    ->where('status', 'active')
    ->orderBy('created_at', 'DESC')
    ->limit(10)
    ->get();

// With locking inside a transaction
$pdo->beginTransaction();

$job = $qb->table('jobs')
    ->where('status', 'pending')
    ->orderBy('created_at')
    ->limit(1)
    ->lockForUpdate()
    ->skipLocked()
    ->first();

// process $job ...

$pdo->commit();

Recommended Compositions

1. Read-Only Query Builder

class ReadOnlyQueryBuilder
{
    use QueryBuilderCore;
    use SqlBuilder;
    use QueryConditions;
    use QueryJoin;
    use QueryGrouping;
    use QueryDebug;
}

2. Simple Query Builder (No Advanced Features)

class SimpleQueryBuilder
{
    use QueryBuilderCore;
    use SqlBuilder;
    use QueryConditions;
    use QueryGrouping;
}

3. Reporting Query Builder (Heavy on Joins/Grouping)

class ReportingQueryBuilder
{
    use QueryBuilderCore;
    use SqlBuilder;
    use QueryConditions;
    use QueryJoin;
    use QueryGrouping;
    use QueryDebug;
}

4. Complex Query Builder (All Features)

class ComplexQueryBuilder
{
    use QueryBuilderCore;
    use SqlBuilder;
    use QueryConditions;
    use QueryAdvancedConditions;
    use QueryJoin;
    use QueryGrouping;
    use QueryLocking;
    use QueryDebug;
}

Common Patterns

Complex WHERE Logic

// (status = 'active' AND role = 'admin') OR (status = 'pending' AND invited = true)
$qb->table('users')
    ->whereGroup(function($q) {
        return $q->where('status', 'active')
                 ->where('role', 'admin');
    })
    ->whereGroup(function($q) {
        return $q->where('status', 'pending')
                 ->where('invited', true);
    });

Subquery Patterns

// Users who have placed orders over $1000
$qb->table('users')
    ->whereExists(function($q) {
        return $q->table('orders')
                 ->whereRaw('orders.user_id = users.id')
                 ->where('total', '>', 1000);
    });

// Users with more orders than average
$qb->table('users')
    ->whereSub('total_orders', '>', function($q) {
        return $q->table('orders')
                 ->selectRaw('AVG(order_count)')
                 ->table('(SELECT user_id, COUNT(*) as order_count FROM orders GROUP BY user_id) as subquery');
    });

Pessimistic Locking Patterns

// Payment processing — hold rows exclusively while charging
$pdo->beginTransaction();

$order = $qb->table('orders')
    ->where('id', $orderId)
    ->where('status', 'pending')
    ->lockForUpdate()
    ->first();

if ($order) {
    // safe to charge — no other process can touch this row
}

$pdo->commit();

// Job queue — multiple workers each claim one job without colliding
$pdo->beginTransaction();

$job = $qb->table('jobs')
    ->where('status', 'available')
    ->orderBy('priority', 'DESC')
    ->orderBy('created_at')
    ->limit(1)
    ->lockForUpdate()
    ->skipLocked()   // other workers skip this row instead of waiting
    ->first();

$pdo->commit();

// Inventory check — read-consistent snapshot while others can still read
$pdo->beginTransaction();

$stock = $qb->table('inventory')
    ->where('product_id', $productId)
    ->lockForShare()
    ->first();

$pdo->commit();

Reporting Queries

$qb->table('orders')
    ->select([
        'users.name',
        'COUNT(orders.id) as total_orders',
        'SUM(orders.total) as total_spent',
        'AVG(orders.total) as avg_order'
    ])
    ->leftJoin('users', 'users.id = orders.user_id')
    ->where('orders.status', 'completed')
    ->whereBetween('orders.created_at', ['2024-01-01', '2024-12-31'])
    ->groupBy('users.id')
    ->having('total_orders', '>', 5)
    ->orderByDesc('total_spent')
    ->limit(100);

Requirements

  • PHP 8.2 or higher

License

MIT

Contributing

This is a primitive library, keep it simple and focused on building blocks, not opinions.

统计信息

  • 总下载量: 10
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 1
  • 点击次数: 1
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 1
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2025-10-14

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固