Przejdź do głównej treści
Grafika przedstawia ukryty obrazek

Realizacja Query Buildera w PHP z wykorzystaniem PDO

Wizualizacja tematu 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ł:

  1. Budowanie zapytań typu SELECT, INSERT, UPDATE, DELETE.
  2. Wykorzystanie parametryzowanych zapytań w celu ochrony przed SQL Injection.
  3. Łatwe łączenie warunków WHERE, ORDER BY, LIMIT.
  4. Integrację z PDO i 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 BY czy HAVING.
  • 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:

  1. groupBy() i having() — umożliwią agregację danych.
  2. raw() — pozwoli wstrzykiwać fragmenty SQL (np. COUNT(id) lub NOW()), przy zachowaniu bezpieczeństwa.
  3. Fluent API chaining — np. first() do pobrania tylko jednego rekordu.
  4. 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ć

  1. Skopiuj wszystkie trzy pliki (db.php, QueryBuilder.php, example.php) do jednego katalogu.
  2. Uruchom w terminalu:
php example.php
  1. 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.

Pobierz cały projekt

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 Database i 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, raw itd.),
  • 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.

9 listopada 2025 4

Kategorie

programowanie

Tagi

php sql

Dziękujemy!
()

Powiązane wpisy


Informacja o cookies

Moja strona internetowa wykorzystuje wyłącznie niezbędne pliki cookies, które są wymagane do jej prawidłowego działania. Nie używam ciasteczek w celach marketingowych ani analitycznych. Korzystając z mojej strony, wyrażasz zgodę na stosowanie tych plików. Możesz dowiedzieć się więcej w mojej polityce prywatności.