Realizacja Query Buildera w PHP z wykorzystaniem PDO
Wprowadzenie
Podczas pracy z bazami danych w PHP jednym z najczęstszych zadań jest tworzenie zapytań SQL. Choć użycie bezpośrednich zapytań z PDO jest elastyczne, to w większych projektach szybko prowadzi do problemów z czytelnością, powtarzalnością kodu i trudnością w utrzymaniu.
Rozwiązaniem tych problemów jest Query Builder — wzorzec, który pozwala dynamicznie budować zapytania SQL w sposób obiektowy, czytelny i bezpieczny.
W tym artykule pokażemy, jak od podstaw stworzyć prosty, rozszerzalny Query Builder w PHP, który współpracuje z PDO.
Założenia projektu
Nasz Query Builder będzie umożliwiał:
- Budowanie zapytań typu
SELECT,INSERT,UPDATE,DELETE. - Wykorzystanie parametryzowanych zapytań w celu ochrony przed SQL Injection.
- Łatwe łączenie warunków
WHERE,ORDER BY,LIMIT. - Integrację z
PDOi automatyczne wykonywanie zapytań.
Przygotowanie środowiska
Na początek załóżmy prostą strukturę projektu:
project/
│
├── db.php
├── QueryBuilder.php
└── index.php
W pliku db.php utworzymy połączenie PDO:
<?php
$dsn = 'mysql:host=localhost;dbname=test;charset=utf8mb4';
$user = 'root';
$pass = '';
try {
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
} catch (PDOException $e) {
die('Błąd połączenia: ' . $e->getMessage());
}
Implementacja klasy QueryBuilder
Poniżej przedstawiamy prostą, modularną implementację:
<?php
class QueryBuilder
{
private PDO $pdo;
private string $table = '';
private array $fields = ['*'];
private array $wheres = [];
private array $bindings = [];
private ?string $order = null;
private ?int $limit = null;
private string $queryType = 'select';
private array $insertData = [];
private array $updateData = [];
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
public function table(string $table): self
{
$this->table = $table;
return $this;
}
public function select(array $fields = ['*']): self
{
$this->queryType = 'select';
$this->fields = $fields;
return $this;
}
public function where(string $column, string $operator, $value): self
{
$placeholder = ':' . $column . count($this->bindings);
$this->wheres[] = "$column $operator $placeholder";
$this->bindings[$placeholder] = $value;
return $this;
}
public function orderBy(string $column, string $direction = 'ASC'): self
{
$this->order = "$column $direction";
return $this;
}
public function limit(int $limit): self
{
$this->limit = $limit;
return $this;
}
public function insert(array $data): self
{
$this->queryType = 'insert';
$this->insertData = $data;
return $this;
}
public function update(array $data): self
{
$this->queryType = 'update';
$this->updateData = $data;
return $this;
}
public function delete(): self
{
$this->queryType = 'delete';
return $this;
}
private function buildSelect(): string
{
$sql = "SELECT " . implode(', ', $this->fields) . " FROM {$this->table}";
if (!empty($this->wheres)) {
$sql .= " WHERE " . implode(' AND ', $this->wheres);
}
if ($this->order) {
$sql .= " ORDER BY {$this->order}";
}
if ($this->limit) {
$sql .= " LIMIT {$this->limit}";
}
return $sql;
}
private function buildInsert(): string
{
$columns = implode(', ', array_keys($this->insertData));
$placeholders = ':' . implode(', :', array_keys($this->insertData));
foreach ($this->insertData as $k => $v) {
$this->bindings[":$k"] = $v;
}
return "INSERT INTO {$this->table} ($columns) VALUES ($placeholders)";
}
private function buildUpdate(): string
{
$set = [];
foreach ($this->updateData as $k => $v) {
$placeholder = ":upd_$k";
$set[] = "$k = $placeholder";
$this->bindings[$placeholder] = $v;
}
$sql = "UPDATE {$this->table} SET " . implode(', ', $set);
if (!empty($this->wheres)) {
$sql .= " WHERE " . implode(' AND ', $this->wheres);
}
return $sql;
}
private function buildDelete(): string
{
$sql = "DELETE FROM {$this->table}";
if (!empty($this->wheres)) {
$sql .= " WHERE " . implode(' AND ', $this->wheres);
}
return $sql;
}
public function get()
{
$sql = match ($this->queryType) {
'select' => $this->buildSelect(),
'insert' => $this->buildInsert(),
'update' => $this->buildUpdate(),
'delete' => $this->buildDelete(),
};
$stmt = $this->pdo->prepare($sql);
$stmt->execute($this->bindings);
return match ($this->queryType) {
'select' => $stmt->fetchAll(PDO::FETCH_ASSOC),
default => $stmt->rowCount(),
};
}
}
Przykłady użycia
1. Zapytanie SELECT
require 'db.php';
require 'QueryBuilder.php';
$qb = new QueryBuilder($pdo);
$users = $qb->table('users')
->select(['id', 'name', 'email'])
->where('status', '=', 'active')
->orderBy('id', 'DESC')
->limit(5)
->get();
print_r($users);
2. Zapytanie INSERT
$qb->table('users')
->insert([
'name' => 'Jan Kowalski',
'email' => 'jan@example.com',
'status' => 'active'
])
->get();
3. Zapytanie UPDATE
$qb->table('users')
->update(['status' => 'inactive'])
->where('id', '=', 10)
->get();
4. Zapytanie DELETE
$qb->table('users')
->delete()
->where('id', '=', 15)
->get();
Zalety podejścia
- Bezpieczeństwo: wszystkie dane są bindowane do parametrów PDO, co zapobiega SQL Injection.
- Czytelność: zapytania są budowane obiektowo i logicznie.
- Elastyczność: można łatwo rozszerzyć o
JOIN,GROUP BYczyHAVING. - Reużywalność: jeden builder może obsłużyć wiele tabel i zapytań.
Rozszerzenie: Obsługa JOIN
W praktycznych zastosowaniach Query Builder często musi wykonywać zapytania z łączeniem tabel.
Dodajmy więc prostą implementację metody join() oraz leftJoin() do naszej klasy.
Zmiany w klasie QueryBuilder
Dodaj nową właściwość oraz metody:
private array $joins = [];
Następnie dodaj metody:
public function join(string $table, string $first, string $operator, string $second): self
{
$this->joins[] = "INNER JOIN $table ON $first $operator $second";
return $this;
}
public function leftJoin(string $table, string $first, string $operator, string $second): self
{
$this->joins[] = "LEFT JOIN $table ON $first $operator $second";
return $this;
}
Zmodyfikuj także metodę buildSelect(), aby uwzględniała sekcję JOIN:
private function buildSelect(): string
{
$sql = "SELECT " . implode(', ', $this->fields) . " FROM {$this->table}";
if (!empty($this->joins)) {
$sql .= ' ' . implode(' ', $this->joins);
}
if (!empty($this->wheres)) {
$sql .= " WHERE " . implode(' AND ', $this->wheres);
}
if ($this->order) {
$sql .= " ORDER BY {$this->order}";
}
if ($this->limit) {
$sql .= " LIMIT {$this->limit}";
}
return $sql;
}
Przykład użycia JOIN
$qb = new QueryBuilder($pdo);
$users = $qb->table('users')
->select(['users.id', 'users.name', 'orders.total'])
->join('orders', 'users.id', '=', 'orders.user_id')
->where('orders.status', '=', 'paid')
->orderBy('orders.created_at', 'DESC')
->limit(10)
->get();
print_r($users);
Wygenerowane zapytanie SQL będzie wyglądało następująco:
SELECT users.id, users.name, orders.total
FROM users
INNER JOIN orders ON users.id = orders.user_id
WHERE orders.status = :ordersstatus0
ORDER BY orders.created_at DESC
LIMIT 10
Rozszerzenie: Obsługa transakcji w Query Builderze
Transakcje w PDO są bardzo przydatne, gdy musimy wykonać kilka powiązanych operacji w sposób atomowy — tzn. wszystkie się udają albo żadna.
Dodajmy do naszej klasy metody:
public function beginTransaction(): void
{
$this->pdo->beginTransaction();
}
public function commit(): void
{
$this->pdo->commit();
}
public function rollBack(): void
{
$this->pdo->rollBack();
}
Przykład użycia transakcji
Załóżmy, że chcemy przenieść saldo między dwoma użytkownikami w bazie:
try {
$qb->beginTransaction();
// Odejmij saldo od użytkownika A
$qb->table('users')
->update(['balance' => 500])
->where('id', '=', 1)
->get();
// Dodaj saldo użytkownikowi B
$qb->table('users')
->update(['balance' => 1500])
->where('id', '=', 2)
->get();
$qb->commit();
echo "Transakcja zakończona sukcesem!";
} catch (Exception $e) {
$qb->rollBack();
echo "Błąd transakcji: " . $e->getMessage();
}
W przypadku błędu w którymkolwiek z zapytań (np. utrata połączenia lub niepoprawny SQL), cały zestaw operacji zostanie cofnięty (ROLLBACK), co zapewnia spójność danych.
Dalsze możliwości rozwoju
Jeśli chcesz rozbudować Query Builder jeszcze bardziej, oto kilka kierunków:
groupBy()ihaving()— umożliwią agregację danych.raw()— pozwoli wstrzykiwać fragmenty SQL (np.COUNT(id)lubNOW()), przy zachowaniu bezpieczeństwa.- Fluent API chaining — np.
first()do pobrania tylko jednego rekordu. - Obsługa relacji — mini ORM, np.
hasMany()/belongsTo().
Pełny kod klasy QueryBuilder.php
<?php
class QueryBuilder
{
private PDO $pdo;
private string $table = '';
private array $fields = ['*'];
private array $wheres = [];
private array $bindings = [];
private array $joins = [];
private ?string $order = null;
private ?int $limit = null;
private string $queryType = 'select';
private array $insertData = [];
private array $updateData = [];
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
/** ------------------------------ **/
/** USTAWIENIA PODSTAWOWE **/
/** ------------------------------ **/
public function table(string $table): self
{
$this->reset();
$this->table = $table;
return $this;
}
public function select(array $fields = ['*']): self
{
$this->queryType = 'select';
$this->fields = $fields;
return $this;
}
public function insert(array $data): self
{
$this->queryType = 'insert';
$this->insertData = $data;
return $this;
}
public function update(array $data): self
{
$this->queryType = 'update';
$this->updateData = $data;
return $this;
}
public function delete(): self
{
$this->queryType = 'delete';
return $this;
}
/** ------------------------------ **/
/** WARUNKI **/
/** ------------------------------ **/
public function where(string $column, string $operator, $value): self
{
$placeholder = ':' . str_replace('.', '_', $column) . count($this->bindings);
$this->wheres[] = "$column $operator $placeholder";
$this->bindings[$placeholder] = $value;
return $this;
}
public function orderBy(string $column, string $direction = 'ASC'): self
{
$this->order = "$column $direction";
return $this;
}
public function limit(int $limit): self
{
$this->limit = $limit;
return $this;
}
/** ------------------------------ **/
/** JOINY **/
/** ------------------------------ **/
public function join(string $table, string $first, string $operator, string $second): self
{
$this->joins[] = "INNER JOIN $table ON $first $operator $second";
return $this;
}
public function leftJoin(string $table, string $first, string $operator, string $second): self
{
$this->joins[] = "LEFT JOIN $table ON $first $operator $second";
return $this;
}
/** ------------------------------ **/
/** BUDOWANIE SQL **/
/** ------------------------------ **/
private function buildSelect(): string
{
$sql = "SELECT " . implode(', ', $this->fields) . " FROM {$this->table}";
if (!empty($this->joins)) {
$sql .= ' ' . implode(' ', $this->joins);
}
if (!empty($this->wheres)) {
$sql .= " WHERE " . implode(' AND ', $this->wheres);
}
if ($this->order) {
$sql .= " ORDER BY {$this->order}";
}
if ($this->limit) {
$sql .= " LIMIT {$this->limit}";
}
return $sql;
}
private function buildInsert(): string
{
$columns = implode(', ', array_keys($this->insertData));
$placeholders = ':' . implode(', :', array_keys($this->insertData));
foreach ($this->insertData as $k => $v) {
$this->bindings[":$k"] = $v;
}
return "INSERT INTO {$this->table} ($columns) VALUES ($placeholders)";
}
private function buildUpdate(): string
{
$set = [];
foreach ($this->updateData as $k => $v) {
$placeholder = ":upd_$k";
$set[] = "$k = $placeholder";
$this->bindings[$placeholder] = $v;
}
$sql = "UPDATE {$this->table} SET " . implode(', ', $set);
if (!empty($this->wheres)) {
$sql .= " WHERE " . implode(' AND ', $this->wheres);
}
return $sql;
}
private function buildDelete(): string
{
$sql = "DELETE FROM {$this->table}";
if (!empty($this->wheres)) {
$sql .= " WHERE " . implode(' AND ', $this->wheres);
}
return $sql;
}
/** ------------------------------ **/
/** WYKONYWANIE SQL **/
/** ------------------------------ **/
public function get()
{
$sql = match ($this->queryType) {
'select' => $this->buildSelect(),
'insert' => $this->buildInsert(),
'update' => $this->buildUpdate(),
'delete' => $this->buildDelete(),
};
$stmt = $this->pdo->prepare($sql);
$stmt->execute($this->bindings);
return match ($this->queryType) {
'select' => $stmt->fetchAll(PDO::FETCH_ASSOC),
default => $stmt->rowCount(),
};
}
/** ------------------------------ **/
/** TRANSAKCJE **/
/** ------------------------------ **/
public function beginTransaction(): void
{
$this->pdo->beginTransaction();
}
public function commit(): void
{
$this->pdo->commit();
}
public function rollBack(): void
{
$this->pdo->rollBack();
}
/** ------------------------------ **/
/** RESET STANU BUILDERA **/
/** ------------------------------ **/
private function reset(): void
{
$this->fields = ['*'];
$this->wheres = [];
$this->bindings = [];
$this->joins = [];
$this->order = null;
$this->limit = null;
$this->insertData = [];
$this->updateData = [];
$this->queryType = 'select';
}
}
Przykłady użycia
1. SELECT z JOIN i LIMIT
require 'db.php';
require 'QueryBuilder.php';
$qb = new QueryBuilder($pdo);
$results = $qb->table('users')
->select(['users.id', 'users.name', 'orders.total'])
->join('orders', 'users.id', '=', 'orders.user_id')
->where('orders.status', '=', 'paid')
->orderBy('orders.created_at', 'DESC')
->limit(5)
->get();
print_r($results);
2. INSERT
$qb->table('users')
->insert([
'name' => 'Anna Nowak',
'email' => 'anna@example.com',
'status' => 'active'
])
->get();
3. UPDATE z WHERE
$qb->table('users')
->update(['status' => 'inactive'])
->where('id', '=', 7)
->get();
4. DELETE
$qb->table('users')
->delete()
->where('id', '=', 9)
->get();
5. Transakcja (BEGIN / COMMIT / ROLLBACK)
try {
$qb->beginTransaction();
$qb->table('users')
->update(['balance' => 400])
->where('id', '=', 1)
->get();
$qb->table('users')
->update(['balance' => 1600])
->where('id', '=', 2)
->get();
$qb->commit();
echo "Transakcja zakończona sukcesem!";
} catch (Exception $e) {
$qb->rollBack();
echo "Błąd transakcji: " . $e->getMessage();
}
Kompletny projekt na SQLite
Poniżej znajdziesz kompletny przykład projektu składający się z trzech plików:
db.php– konfiguracja bazy SQLite,QueryBuilder.php– nasza klasa Query Buildera,example.php– pełny przykład działania (tworzenie tabeli, dodawanie, aktualizowanie, zapytania SELECT, DELETE, transakcje).
Struktura projektu
project/
│
├── db.php
├── QueryBuilder.php
└── example.php
Plik: db.php
<?php
// Tworzy plik bazy danych SQLite w bieżącym katalogu
$dsn = 'sqlite:' . __DIR__ . '/database.sqlite';
try {
$pdo = new PDO($dsn);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
} catch (PDOException $e) {
die('Błąd połączenia z bazą: ' . $e->getMessage());
}
Plik: QueryBuilder.php
(pełna wersja z obsługą JOIN, transakcji itd.)
<?php
class QueryBuilder
{
private PDO $pdo;
private string $table = '';
private array $fields = ['*'];
private array $wheres = [];
private array $bindings = [];
private array $joins = [];
private ?string $order = null;
private ?int $limit = null;
private string $queryType = 'select';
private array $insertData = [];
private array $updateData = [];
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
/** ------------------------------ **/
/** USTAWIENIA PODSTAWOWE **/
/** ------------------------------ **/
public function table(string $table): self
{
$this->reset();
$this->table = $table;
return $this;
}
public function select(array $fields = ['*']): self
{
$this->queryType = 'select';
$this->fields = $fields;
return $this;
}
public function insert(array $data): self
{
$this->queryType = 'insert';
$this->insertData = $data;
return $this;
}
public function update(array $data): self
{
$this->queryType = 'update';
$this->updateData = $data;
return $this;
}
public function delete(): self
{
$this->queryType = 'delete';
return $this;
}
/** ------------------------------ **/
/** WARUNKI **/
/** ------------------------------ **/
public function where(string $column, string $operator, $value): self
{
$placeholder = ':' . str_replace('.', '_', $column) . count($this->bindings);
$this->wheres[] = "$column $operator $placeholder";
$this->bindings[$placeholder] = $value;
return $this;
}
public function orderBy(string $column, string $direction = 'ASC'): self
{
$this->order = "$column $direction";
return $this;
}
public function limit(int $limit): self
{
$this->limit = $limit;
return $this;
}
/** ------------------------------ **/
/** JOINY **/
/** ------------------------------ **/
public function join(string $table, string $first, string $operator, string $second): self
{
$this->joins[] = "INNER JOIN $table ON $first $operator $second";
return $this;
}
public function leftJoin(string $table, string $first, string $operator, string $second): self
{
$this->joins[] = "LEFT JOIN $table ON $first $operator $second";
return $this;
}
/** ------------------------------ **/
/** BUDOWANIE SQL **/
/** ------------------------------ **/
private function buildSelect(): string
{
$sql = "SELECT " . implode(', ', $this->fields) . " FROM {$this->table}";
if (!empty($this->joins)) {
$sql .= ' ' . implode(' ', $this->joins);
}
if (!empty($this->wheres)) {
$sql .= " WHERE " . implode(' AND ', $this->wheres);
}
if ($this->order) {
$sql .= " ORDER BY {$this->order}";
}
if ($this->limit) {
$sql .= " LIMIT {$this->limit}";
}
return $sql;
}
private function buildInsert(): string
{
$columns = implode(', ', array_keys($this->insertData));
$placeholders = ':' . implode(', :', array_keys($this->insertData));
foreach ($this->insertData as $k => $v) {
$this->bindings[":$k"] = $v;
}
return "INSERT INTO {$this->table} ($columns) VALUES ($placeholders)";
}
private function buildUpdate(): string
{
$set = [];
foreach ($this->updateData as $k => $v) {
$placeholder = ":upd_$k";
$set[] = "$k = $placeholder";
$this->bindings[$placeholder] = $v;
}
$sql = "UPDATE {$this->table} SET " . implode(', ', $set);
if (!empty($this->wheres)) {
$sql .= " WHERE " . implode(' AND ', $this->wheres);
}
return $sql;
}
private function buildDelete(): string
{
$sql = "DELETE FROM {$this->table}";
if (!empty($this->wheres)) {
$sql .= " WHERE " . implode(' AND ', $this->wheres);
}
return $sql;
}
/** ------------------------------ **/
/** WYKONYWANIE SQL **/
/** ------------------------------ **/
public function get()
{
$sql = match ($this->queryType) {
'select' => $this->buildSelect(),
'insert' => $this->buildInsert(),
'update' => $this->buildUpdate(),
'delete' => $this->buildDelete(),
};
$stmt = $this->pdo->prepare($sql);
$stmt->execute($this->bindings);
return match ($this->queryType) {
'select' => $stmt->fetchAll(PDO::FETCH_ASSOC),
default => $stmt->rowCount(),
};
}
/** ------------------------------ **/
/** TRANSAKCJE **/
/** ------------------------------ **/
public function beginTransaction(): void
{
$this->pdo->beginTransaction();
}
public function commit(): void
{
$this->pdo->commit();
}
public function rollBack(): void
{
$this->pdo->rollBack();
}
/** ------------------------------ **/
/** RESET STANU BUILDERA **/
/** ------------------------------ **/
private function reset(): void
{
$this->fields = ['*'];
$this->wheres = [];
$this->bindings = [];
$this->joins = [];
$this->order = null;
$this->limit = null;
$this->insertData = [];
$this->updateData = [];
$this->queryType = 'select';
}
}
Plik: example.php
<?php
require 'db.php';
require 'QueryBuilder.php';
$qb = new QueryBuilder($pdo);
// Tworzymy przykładowe tabele
$pdo->exec("DROP TABLE IF EXISTS users");
$pdo->exec("DROP TABLE IF EXISTS orders");
$pdo->exec("
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
email TEXT,
balance REAL
)");
$pdo->exec("
CREATE TABLE orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
total REAL,
status TEXT,
FOREIGN KEY (user_id) REFERENCES users(id)
)");
echo "✅ Tabele utworzone.\n\n";
// Dodajmy kilku użytkowników
$qb->table('users')->insert(['name' => 'Jan Kowalski', 'email' => 'jan@example.com', 'balance' => 1000])->get();
$qb->table('users')->insert(['name' => 'Anna Nowak', 'email' => 'anna@example.com', 'balance' => 2000])->get();
// Dodajmy zamówienia
$qb->table('orders')->insert(['user_id' => 1, 'total' => 250.50, 'status' => 'paid'])->get();
$qb->table('orders')->insert(['user_id' => 1, 'total' => 300.00, 'status' => 'pending'])->get();
$qb->table('orders')->insert(['user_id' => 2, 'total' => 400.00, 'status' => 'paid'])->get();
echo "✅ Dane przykładowe dodane.\n\n";
// Przykład SELECT z JOIN
$results = $qb->table('users')
->select(['users.name', 'orders.total', 'orders.status'])
->join('orders', 'users.id', '=', 'orders.user_id')
->where('orders.status', '=', 'paid')
->orderBy('orders.total', 'DESC')
->get();
echo "📊 Wyniki zapytania JOIN:\n";
print_r($results);
// Przykład transakcji
try {
echo "\n💰 Rozpoczynamy transakcję...\n";
$qb->beginTransaction();
$qb->table('users')
->update(['balance' => 900])
->where('id', '=', 1)
->get();
$qb->table('users')
->update(['balance' => 2100])
->where('id', '=', 2)
->get();
$qb->commit();
echo "✅ Transakcja zakończona sukcesem.\n";
} catch (Exception $e) {
$qb->rollBack();
echo "❌ Błąd transakcji: " . $e->getMessage();
}
// Sprawdź aktualne dane użytkowników
$users = $qb->table('users')->select()->orderBy('id')->get();
echo "\n👥 Dane użytkowników po transakcji:\n";
print_r($users);
Jak uruchomić
- Skopiuj wszystkie trzy pliki (
db.php,QueryBuilder.php,example.php) do jednego katalogu. - Uruchom w terminalu:
php example.php
-
Zobaczysz w konsoli kolejno:
- komunikaty o tworzeniu tabel i danych,
- wyniki zapytania z
JOIN, - wynik transakcji i stan danych użytkowników po jej wykonaniu.
Bonus: Porównanie Query Buildera (autorskiego) z Koseven i Laravel
1. Nasz Query Builder (PDO + SQLite)
Charakterystyka:
- Lekki, bez zależności, oparty o czysty PHP i PDO.
- Pełna kontrola nad SQL, parametryzowanie i transakcje.
- Idealny do nauki, testów, mikroserwisów lub CLI.
Przykład użycia:
$qb->table('users')
->select(['name', 'email'])
->where('status', '=', 'active')
->orderBy('id', 'DESC')
->limit(5)
->get();
Wygenerowany SQL:
SELECT name, email FROM users
WHERE status = :status0
ORDER BY id DESC
LIMIT 5
Zalety:
- Minimalistyczny, prosty w debugowaniu, łatwy do rozszerzenia.
- W pełni kompatybilny z PDO (MySQL, SQLite, PostgreSQL, itp.).
Wady:
- Brak integracji z ORM (relacje, modele, walidacja).
- Brak abstrakcji dla migracji, seedów, soft deletes itp.
2. Query Builder we frameworku Koseven (dawniej Kohana)
Charakterystyka:
- Obiektowy, modularny Query Builder zorientowany na bezpośrednią integrację z ORM.
- Zwraca obiekty
Database_Query_Builder_Select,Database_Query_Builder_Insert, itd. - SQL generowany dopiero przy wykonaniu (
execute()).
Przykład użycia:
$users = DB::select('id', 'name', 'email')
->from('users')
->where('status', '=', 'active')
->order_by('id', 'DESC')
->limit(5)
->execute()
->as_array();
Wygenerowany SQL (analogiczny do naszego):
SELECT "id", "name", "email"
FROM "users"
WHERE "status" = 'active'
ORDER BY "id" DESC
LIMIT 5
Zalety:
- Automatyczne escapowanie nazw kolumn i tabel.
- Integracja z klasą
Database, obsługa połączeń, profilerów i cache. - API bardzo podobne do naszego Query Buildera (czytelne metody).
Wady:
- Wymaga pełnego frameworka Koseven (nie działa samodzielnie).
- Ograniczona popularność w porównaniu do Laravel.
- Mniej elastyczny niż ORM Eloquent w zakresie relacji.
Podobieństwa z naszym rozwiązaniem:
- Łańcuchowy interfejs (
->where()->order_by()->limit()), - Budowa zapytania w pamięci i przygotowanie przed wykonaniem,
- Bezpośrednie odwzorowanie składni SQL na metody PHP.
3. Query Builder w Laravel (Eloquent / DB Facade)
Charakterystyka:
- Rozbudowany, elastyczny, wspiera relacje i podzapytania.
- Może działać samodzielnie (
DB::table()), lub jako ORM (Model::query()). - Automatycznie mapuje wyniki na obiekty modelu (w przypadku Eloquent ORM).
Przykład użycia (Query Builder):
$users = DB::table('users')
->select('id', 'name', 'email')
->where('status', '=', 'active')
->orderBy('id', 'DESC')
->limit(5)
->get();
Przykład użycia (Eloquent ORM):
$users = User::where('status', 'active')
->orderByDesc('id')
->take(5)
->get();
Wygenerowany SQL:
select `id`, `name`, `email`
from `users`
where `status` = ?
order by `id` desc
limit 5
Zalety:
- Bardzo ekspresyjna składnia (łatwa do czytania).
- Integracja z ORM — relacje, mutatory, fillable, timestamps.
- Obsługa migracji, seederów, testów, eventów, transakcji.
Wady:
- Większy narzut i zależność od frameworka.
- Trudniejszy do debugowania „goły” SQL (lazy execution).
- Wymaga konfiguracji całego ekosystemu Laravel.
Podobieństwa z naszym rozwiązaniem:
- Fluent API i łańcuchowe metody.
- Bezpieczne bindowanie parametrów.
- Obsługa transakcji przez
DB::transaction().
Przykład transakcji w Laravel:
DB::transaction(function () {
DB::table('users')->where('id', 1)->update(['balance' => 900]);
DB::table('users')->where('id', 2)->update(['balance' => 2100]);
});
Analogiczny przykład w naszym Query Builderze:
try {
$qb->beginTransaction();
$qb->table('users')->update(['balance' => 900])->where('id', '=', 1)->get();
$qb->table('users')->update(['balance' => 2100])->where('id', '=', 2)->get();
$qb->commit();
} catch (Exception $e) {
$qb->rollBack();
}
Porównanie funkcjonalności
| Funkcja | Nasz QueryBuilder | Koseven Query Builder | Laravel Query Builder |
|---|---|---|---|
Fluent API (->where()) |
|||
| Obsługa JOIN | |||
Transakcje (begin/commit) |
(manualnie) | (DB::start/commit) | (DB::transaction) |
| ORM | (ORM optional) | (Eloquent) | |
| Cache zapytań | |||
| Lazy loading relacji | |||
| Migracje / seedy | |||
| Łatwość debugowania SQL | (pełny wgląd) | (via toSql()) |
|
| Wymagania środowiskowe | Minimalne | Średnie | Duże (pełny framework) |
Legenda: — pełne wsparcie, — częściowe / opcjonalne, — brak.
4. Wnioski
| Podejście | Idealne dla... |
|---|---|
| Nasz Query Builder | nauki, małych aplikacji, CLI, testów, serwisów mikro |
| Koseven | lekkich aplikacji MVC, gdy chcesz kontrolować SQL, ale z pomocą frameworka |
| Laravel | dużych systemów z ORM, relacjami, migracjami i strukturą DDD |
Porównanie składni – jeden cel, trzy style
Cel zapytania
Pobierz listę aktywnych użytkowników wraz z kwotą ostatniego zamówienia, posortowaną malejąco po wartości zamówienia i ograniczoną do 5 rekordów.
1. Nasz Query Builder (czysty PHP + PDO)
$qb->table('users')
->select(['users.id', 'users.name', 'orders.total'])
->join('orders', 'users.id', '=', 'orders.user_id')
->where('users.status', '=', 'active')
->orderBy('orders.total', 'DESC')
->limit(5)
->get();
Wygenerowany SQL:
SELECT users.id, users.name, orders.total
FROM users
INNER JOIN orders ON users.id = orders.user_id
WHERE users.status = :usersstatus0
ORDER BY orders.total DESC
LIMIT 5
Zalety:
- Lekki, pełna kontrola nad SQL.
- Czytelna składnia, działa z dowolnym PDO driverem (MySQL, SQLite, Postgres...).
- Idealny dla małych systemów lub mikroserwisów.
2. Koseven (Kohana 3.x)
$users = DB::select('users.id', 'users.name', 'orders.total')
->from('users')
->join('orders', 'INNER')
->on('users.id', '=', 'orders.user_id')
->where('users.status', '=', 'active')
->order_by('orders.total', 'DESC')
->limit(5)
->execute()
->as_array();
Wygenerowany SQL:
SELECT "users"."id", "users"."name", "orders"."total"
FROM "users"
INNER JOIN "orders" ON ("users"."id" = "orders"."user_id")
WHERE "users"."status" = 'active'
ORDER BY "orders"."total" DESC
LIMIT 5
Zalety:
- Bardzo bliska składnia SQL, w pełni obiektowa.
- Automatyczne escapowanie nazw kolumn i tabel (
"users"."name"). - Integracja z
Databasei ORM.
Wady:
- Wymaga frameworka Koseven, nie działa samodzielnie.
- Mniej nowoczesna składnia niż Laravel (snake_case, brak kolekcji).
3. Laravel Query Builder / Eloquent ORM
(a) Query Builder:
$users = DB::table('users')
->join('orders', 'users.id', '=', 'orders.user_id')
->select('users.id', 'users.name', 'orders.total')
->where('users.status', '=', 'active')
->orderByDesc('orders.total')
->limit(5)
->get();
(b) Eloquent ORM:
$users = User::query()
->join('orders', 'users.id', '=', 'orders.user_id')
->select('users.id', 'users.name', 'orders.total')
->where('users.status', 'active')
->orderByDesc('orders.total')
->take(5)
->get();
Wygenerowany SQL:
select `users`.`id`, `users`.`name`, `orders`.`total`
from `users`
inner join `orders` on `users`.`id` = `orders`.`user_id`
where `users`.`status` = ?
order by `orders`.`total` desc
limit 5
Zalety:
- Bardzo ekspresyjna i nowoczesna składnia.
- Możliwość natychmiastowego mapowania wyników na obiekty modeli (
User). - Łatwe dodanie relacji (
hasMany,belongsTo, itp.). - Obsługa
DB::transaction(),toSql(),paginate(),count()itp.
Wady:
- Wymaga pełnego środowiska Laravel.
- Mniej przejrzysty SQL przy debugowaniu (lazy evaluation).
Zestawienie składni (porównanie linii po linii)
| Czynność | Nasz Builder (PDO) | Koseven | Laravel |
|---|---|---|---|
| Inicjalizacja | $qb->table('users') |
DB::select(...)->from('users') |
DB::table('users') |
| JOIN | ->join('orders', 'users.id', '=', 'orders.user_id') |
->join('orders')->on('users.id', '=', 'orders.user_id') |
->join('orders', 'users.id', '=', 'orders.user_id') |
| WHERE | ->where('users.status', '=', 'active') |
->where('users.status', '=', 'active') |
->where('users.status', '=', 'active') |
| SORTOWANIE | ->orderBy('orders.total', 'DESC') |
->order_by('orders.total', 'DESC') |
->orderByDesc('orders.total') |
| LIMIT | ->limit(5) |
->limit(5) |
->limit(5) lub ->take(5) |
| Wykonanie | ->get() |
->execute()->as_array() |
->get() |
| Wynik | Tablica fetchAll(PDO::FETCH_ASSOC) |
Tablica asocjacyjna | Kolekcja obiektów (Collection) |
Ogólne różnice filozoficzne
| Aspekt | Nasz Query Builder | Koseven | Laravel |
|---|---|---|---|
| Typ | Lekki i proceduralny | Klasyczny OOP MVC | Nowoczesny ORM i fluent API |
| SQL | Pisany niemal ręcznie | Generowany z metod | Abstrahowany przez ORM |
| Bezpieczeństwo | Parametry PDO | Automatyczne escapowanie | Bindowanie parametrów |
| Debugowanie | Łatwe, pełny SQL | Wymaga profilu bazy | ->toSql() i DB::listen() |
| Relacje | Brak | Częściowe (ORM) | Pełne ORM (hasMany, belongsTo) |
| Wydajność | Wysoka, mały narzut | Średnia | Niższa (ORM overhead) |
| Cel | Nauka, mikroserwisy | Średnie aplikacje MVC | Duże systemy, REST API, SaaS |
Podsumowanie
Query Builder to potężne narzędzie, które pozwala tworzyć dynamiczne zapytania SQL w sposób obiektowy i bezpieczny.
Dzięki PDO mamy dodatkowo gwarancję bezpieczeństwa oraz zgodność z wieloma systemami bazodanowymi.
Przedstawiony przykład stanowi solidną podstawę, którą można rozwijać w kierunku własnego mini-ORM lub wykorzystać jako lekką warstwę abstrakcji nad SQL w dowolnym projekcie PHP.
Ta implementacja QueryBuildera w PHP:
- korzysta z PDO dla bezpieczeństwa i przenośności,
- posiada prosty, łańcuchowy interfejs fluent,
- jest rozszerzalna o nowe metody (
groupBy,having,rawitd.), - a dzięki metodom transakcyjnym nadaje się również do złożonych operacji biznesowych.
| Framework / Styl | Siła | Główne zastosowanie |
|---|---|---|
| Autorski Query Builder (PDO) | Minimalizm i pełna kontrola | Małe aplikacje, CLI, testy, nauka SQL |
| Koseven | Stabilność i prostota | Lekkie aplikacje MVC, serwisy wewnętrzne |
| Laravel (Eloquent / DB) | Produkcyjna moc i ORM | Aplikacje webowe, API, projekty enterprise |
Wniosek: Twój własny Query Builder to doskonała baza do zrozumienia, jak naprawdę działa Eloquent i inne ORM-y pod spodem. Zbudowałeś miniaturowy, w pełni funkcjonalny system warstwy dostępu do danych — coś, co frameworki rozwijają przez lata.