PHPackages                             sodaho/pdo-wrapper - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. sodaho/pdo-wrapper

ActiveLibrary[Database &amp; ORM](/categories/database)

sodaho/pdo-wrapper
==================

A lightweight PDO wrapper with fluent query builder and CRUD methods for MySQL, PostgreSQL and SQLite

v1.0.0(3mo ago)08[1 issues](https://github.com/SoDaHo/pdo-wrapper/issues)MITPHPPHP ^8.2CI passing

Since Mar 19Pushed 3w agoCompare

[ Source](https://github.com/SoDaHo/pdo-wrapper)[ Packagist](https://packagist.org/packages/sodaho/pdo-wrapper)[ RSS](/packages/sodaho-pdo-wrapper/feed)WikiDiscussions main Synced 3w ago

READMEChangelogDependencies (3)Versions (2)Used By (0)

PDO Wrapper
===========

[](#pdo-wrapper)

A lightweight PHP PDO wrapper with fluent Query Builder, supporting MySQL/MariaDB, PostgreSQL, and SQLite.

Why This Library?
-----------------

[](#why-this-library)

- **No dependencies** -- just PDO, which ships with PHP.
- **Readable codebase** -- the entire source fits in a handful of files. You can read and understand all of it in minutes.
- **Multi-database** -- MySQL, MariaDB, PostgreSQL, SQLite behind one API, with driver-specific details handled internally.
- **Safe defaults** -- prepared statements, identifier quoting, operator whitelist. Hard to accidentally write an injection vulnerability.
- **Intentionally limited** -- no OR conditions, no subqueries, no UNION in the query builder. When you need complex SQL, you write SQL. The builder handles the straightforward queries.

Installation
------------

[](#installation)

```
composer require sodaho/pdo-wrapper
```

Quick Start
-----------

[](#quick-start)

```
use Sodaho\PdoWrapper\Database;

// Connect to SQLite
$db = Database::sqlite(':memory:');

// Connect to MySQL
$db = Database::mysql([
    'host' => 'localhost',
    'database' => 'myapp',
    'username' => 'root',
    'password' => 'secret',
]);

// Connect to PostgreSQL
$db = Database::postgres([
    'host' => 'localhost',
    'database' => 'myapp',
    'username' => 'postgres',
    'password' => 'secret',
]);
```

Connection Options
------------------

[](#connection-options)

### MySQL

[](#mysql)

```
$db = Database::mysql([
    'host' => 'localhost',      // required
    'database' => 'myapp',      // required
    'username' => 'root',       // required
    'password' => 'secret',     // optional
    'port' => 3306,             // optional, default: 3306
    'charset' => 'utf8mb4',     // optional, default: utf8mb4
    'options' => [],            // optional, PDO options
]);
```

### PostgreSQL

[](#postgresql)

```
$db = Database::postgres([
    'host' => 'localhost',      // required
    'database' => 'myapp',      // required
    'username' => 'postgres',   // required
    'password' => 'secret',     // optional
    'port' => 5432,             // optional, default: 5432
    'options' => [],            // optional, PDO options
]);
```

### SQLite

[](#sqlite)

```
// In-memory database
$db = Database::sqlite(':memory:');

// File-based database
$db = Database::sqlite('/path/to/database.db');
```

### Environment Variables

[](#environment-variables)

All drivers support configuration via environment variables:

```
// MySQL/PostgreSQL read from:
// DB_HOST, DB_DATABASE, DB_USERNAME, DB_PASSWORD, DB_PORT

// SQLite reads from:
// DB_SQLITE_PATH
```

**Priority:** `$config` array &gt; `$_ENV` &gt; `getenv()`. The library checks `$_ENV` first (thread-safe), then falls back to `getenv()` for legacy compatibility. Use a library like [sodaho/env-loader](https://github.com/sodaho/env-loader) to load `.env` files.

Raw Queries
-----------

[](#raw-queries)

```
// SELECT query
$stmt = $db->query('SELECT * FROM users WHERE id = ?', [1]);
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);

// INSERT/UPDATE/DELETE (returns affected rows)
$affected = $db->execute('UPDATE users SET active = ? WHERE id = ?', [1, 5]);

// Get last insert ID
$id = $db->lastInsertId();

// Access underlying PDO — for features not covered by the wrapper
// (e.g., LOCK TABLES, driver-specific methods, passing PDO to third-party tools)
$pdo = $db->getPdo();
```

CRUD Methods
------------

[](#crud-methods)

### Insert

[](#insert)

```
$id = $db->insert('users', [
    'name' => 'John',
    'email' => 'john@example.com',
]);
```

### Update

[](#update)

```
// Returns affected rows
$affected = $db->update('users',
    ['name' => 'Jane'],           // data
    ['id' => 1]                   // where
);
```

### Delete

[](#delete)

```
// Returns affected rows
$affected = $db->delete('users', ['id' => 1]);
```

### Find

[](#find)

```
// Find one record
$user = $db->findOne('users', ['id' => 1]);

// Find all matching records
$users = $db->findAll('users', ['active' => 1]);

// Find all records in table
$users = $db->findAll('users');
```

### Update Multiple

[](#update-multiple)

```
$db->updateMultiple('users', [
    ['id' => 1, 'name' => 'John'],
    ['id' => 2, 'name' => 'Jane'],
], 'id');  // key column
```

**Note:** This method executes one UPDATE query per row within a transaction. Best suited for batch sizes under ~100 rows. For larger datasets, consider using `execute()` with database-specific bulk update syntax (e.g., `INSERT ... ON DUPLICATE KEY UPDATE` for MySQL).

Query Builder
-------------

[](#query-builder)

### Basic Select

[](#basic-select)

```
// Get all
$users = $db->table('users')->get();

// Get first
$user = $db->table('users')->first();

// Select specific columns
$users = $db->table('users')
    ->select(['id', 'name', 'email'])
    ->get();

// Select with string
$users = $db->table('users')
    ->select('id, name, email')
    ->get();

// Distinct
$names = $db->table('users')
    ->select('name')
    ->distinct()
    ->get();
```

### Where Conditions

[](#where-conditions)

```
// Basic where
$users = $db->table('users')
    ->where('active', 1)
    ->get();

// With operator
$users = $db->table('users')
    ->where('age', '>=', 18)
    ->get();

// Multiple conditions (AND)
$users = $db->table('users')
    ->where('active', 1)
    ->where('role', 'admin')
    ->get();

// Array syntax
$users = $db->table('users')
    ->where(['active' => 1, 'role' => 'admin'])
    ->get();

// Where In
$users = $db->table('users')
    ->whereIn('id', [1, 2, 3])
    ->get();

// Where Not In
$users = $db->table('users')
    ->whereNotIn('status', ['banned', 'deleted'])
    ->get();

// Where Between
$users = $db->table('users')
    ->whereBetween('age', [18, 65])
    ->get();

// Where Not Between
$users = $db->table('users')
    ->whereNotBetween('created_at', ['2020-01-01', '2020-12-31'])
    ->get();

// Where Null
$users = $db->table('users')
    ->whereNull('deleted_at')
    ->get();

// Where Not Null
$users = $db->table('users')
    ->whereNotNull('email_verified_at')
    ->get();

// Where Like
$users = $db->table('users')
    ->whereLike('name', '%john%')
    ->get();

// Where Not Like
$users = $db->table('users')
    ->whereNotLike('email', '%spam%')
    ->get();
```

### Joins

[](#joins)

```
// Inner Join
$posts = $db->table('posts')
    ->select(['posts.title', 'users.name as author'])
    ->join('users', 'users.id', '=', 'posts.user_id')
    ->get();

// Left Join
$users = $db->table('users')
    ->select(['users.name', 'posts.title'])
    ->leftJoin('posts', 'posts.user_id', '=', 'users.id')
    ->get();

// Right Join
$posts = $db->table('posts')
    ->rightJoin('users', 'users.id', '=', 'posts.user_id')
    ->get();
```

### Ordering, Limit, Offset

[](#ordering-limit-offset)

```
$users = $db->table('users')
    ->orderBy('name', 'ASC')
    ->orderBy('created_at', 'DESC')
    ->limit(10)
    ->offset(20)
    ->get();
```

### Group By, Having

[](#group-by-having)

```
use Sodaho\PdoWrapper\Database;

$stats = $db->table('posts')
    ->select(['user_id', Database::raw('COUNT(*) as post_count')])
    ->groupBy('user_id')
    ->having(Database::raw('COUNT(*)'), '>', 5)
    ->get();
```

### Aggregates

[](#aggregates)

```
$count = $db->table('users')->count();
$count = $db->table('users')->where('active', 1)->count();

$sum = $db->table('orders')->sum('total');
$avg = $db->table('orders')->avg('total');
$min = $db->table('orders')->min('total');
$max = $db->table('orders')->max('total');

$exists = $db->table('users')->where('email', 'test@example.com')->exists();
```

### Insert, Update, Delete via Query Builder

[](#insert-update-delete-via-query-builder)

```
// Insert
$id = $db->table('users')->insert([
    'name' => 'John',
    'email' => 'john@example.com',
]);

// Update (requires where)
$affected = $db->table('users')
    ->where('id', 1)
    ->update(['name' => 'Jane']);

// Delete (requires where)
$affected = $db->table('users')
    ->where('id', 1)
    ->delete();
```

### Debug Query

[](#debug-query)

```
[$sql, $params] = $db->table('users')
    ->where('active', 1)
    ->orderBy('name')
    ->toSql();

// $sql = 'SELECT * FROM "users" WHERE "active" = ? ORDER BY "name" ASC'
// $params = [1]
```

Transactions
------------

[](#transactions)

```
// Automatic transaction with callback (auto-rollback on exception)
$db->transaction(function ($db) {
    $db->insert('users', ['name' => 'John']);
    $db->insert('profiles', ['user_id' => $db->lastInsertId()]);
});

// With return value - real world example
$orderId = $db->transaction(function ($db) use ($orderData, $items) {
    // Insert order
    $orderId = $db->insert('orders', [
        'user_id' => $orderData['user_id'],
        'total' => $orderData['total'],
        'status' => 'pending'
    ]);

    // Insert order items
    foreach ($items as $item) {
        $db->insert('order_items', [
            'order_id' => $orderId,
            'product_id' => $item['product_id'],
            'quantity' => $item['quantity'],
            'price' => $item['price']
        ]);

        // Update inventory with raw query
        $db->execute(
            'UPDATE products SET stock = stock - ? WHERE id = ?',
            [$item['quantity'], $item['product_id']]
        );
    }

    return $orderId;  // Return value is passed through
});

// Manual transaction control
$db->beginTransaction();
try {
    $db->insert('users', ['name' => 'John']);
    $db->commit();
} catch (Exception $e) {
    $db->rollback();
    throw $e;
}
```

Hooks
-----

[](#hooks)

Register callbacks for query logging, debugging, or monitoring:

```
// Log all queries
$db->on('query', function (array $data) {
    echo "SQL: {$data['sql']}\n";
    echo "Params: " . json_encode($data['params']) . "\n";
    echo "Duration: {$data['duration']}s\n";
    echo "Rows: {$data['rows']}\n";
});

// Log errors
$db->on('error', function (array $data) {
    error_log("Query failed: {$data['error']} | SQL: {$data['sql']}");
});

// Transaction hooks
$db->on('transaction.begin', fn() => echo "Transaction started\n");
$db->on('transaction.commit', fn() => echo "Transaction committed\n");
$db->on('transaction.rollback', fn() => echo "Transaction rolled back\n");
```

Exceptions
----------

[](#exceptions)

All exceptions extend `DatabaseException`, which extends PHP's base `Exception`:

```
use Sodaho\PdoWrapper\Exception\DatabaseException;
use Sodaho\PdoWrapper\Exception\ConnectionException;
use Sodaho\PdoWrapper\Exception\QueryException;
use Sodaho\PdoWrapper\Exception\TransactionException;

// Catch all pdo-wrapper exceptions
try {
    $db->query('...');
} catch (DatabaseException $e) {
    // Catches ConnectionException, QueryException, TransactionException
}

try {
    $db = Database::mysql([...]);
} catch (ConnectionException $e) {
    // Connection failed
    echo $e->getMessage();        // User-friendly message
    echo $e->getDebugMessage();   // Detailed debug info
}

try {
    $db->query('INVALID SQL');
} catch (QueryException $e) {
    // Query failed
}

try {
    $db->transaction(fn() => throw new Exception('oops'));
} catch (TransactionException $e) {
    // Transaction failed
}
```

Schema-Qualified Tables
-----------------------

[](#schema-qualified-tables)

For PostgreSQL schemas or MySQL database-qualified names:

```
// PostgreSQL
$db->insert('public.users', ['name' => 'John']);
$db->table('public.users')->where('id', 1)->first();

// MySQL
$db->insert('mydb.users', ['name' => 'John']);
$db->table('mydb.users')->where('id', 1)->first();
```

Security
--------

[](#security)

This library protects against SQL injection through:

- **Prepared statements** for all values (WHERE, INSERT, UPDATE)
- **Identifier quoting** for all column and table names
- **Operator whitelist** validation (only `=`, `!=`, ``, ``, `=`, `LIKE`, `NOT LIKE`, `IS`, `IS NOT`)

### Raw Expressions

[](#raw-expressions)

For aggregate functions or complex SQL expressions, use `Database::raw()`:

```
use Sodaho\PdoWrapper\Database;

// Aggregates require Database::raw()
$db->table('users')
    ->select([Database::raw('COUNT(*) as total')])
    ->get();

// Regular column names are automatically quoted and safe
$db->table('users')
    ->select(['id', 'name', 'email'])  // Becomes: "id", "name", "email"
    ->get();
```

**Security Note:** Never pass user input to `Database::raw()`. Raw expressions bypass all identifier quoting.

### User Input in Column Names

[](#user-input-in-column-names)

Column names are safely quoted against SQL injection, but you should still validate user input to provide meaningful error messages instead of database errors:

```
// ✅ RECOMMENDED - Whitelist for better error handling
$allowedColumns = ['id', 'name', 'email', 'created_at'];
$column = $_GET['column'];

if (!in_array($column, $allowedColumns, true)) {
    throw new InvalidArgumentException('Invalid column');
}

$db->table('users')->orderBy($column)->get();
```

This applies to `select()`, `orderBy()`, `groupBy()`, and `join()`.

### LIKE Patterns with User Input

[](#like-patterns-with-user-input)

Use `Database::escapeLike()` to prevent LIKE wildcards (`%`, `_`) in user input from being interpreted as wildcards:

```
use Sodaho\PdoWrapper\Database;

$search = Database::escapeLike($_GET['q']); // "100%" → "100\%"

$db->table('products')
    ->whereLike('name', '%' . $search . '%')
    ->get();
// Matches "Rabatt: 100%" but NOT "1000" or "10099"
```

Limitations
-----------

[](#limitations)

This library is designed for simple, common use cases. The following features are **not supported**:

- **OR conditions** - All `where()` calls are joined with AND. For OR conditions, use raw queries:

    ```
    $db->query('SELECT * FROM users WHERE role = ? OR role = ?', ['admin', 'moderator']);
    ```
- **Nested WHERE groups** - Complex conditions like `(A AND B) OR (C AND D)` require raw queries.
- **Subqueries** - Use raw queries for subqueries in SELECT, WHERE, or FROM clauses.
- **UNION** - Combine queries manually or use raw SQL.
- **LIMIT/ORDER BY in update/delete** - `limit()`, `offset()`, and `orderBy()` are not supported with `update()` or `delete()` (not portable across databases). The QueryBuilder throws an exception if you try. Use a subquery instead:

    ```
    // Delete the 10 oldest logs (works on all databases)
    $db->execute(
        'DELETE FROM logs WHERE id IN (SELECT id FROM logs ORDER BY created_at ASC LIMIT 10)'
    );
    ```
- **NULL in where()** - `where('column', null)` throws an exception because `column = NULL` is always false in SQL. Use `whereNull()` or `whereNotNull()` instead.
- **PostgreSQL primary key convention** - `insert()` assumes the primary key column is named `id`. For custom PK names, use raw query with `RETURNING`:

    ```
    $stmt = $db->query('INSERT INTO users (name) VALUES (?) RETURNING user_id', ['John']);
    $userId = $stmt->fetch()['user_id'];
    ```

These limitations keep the QueryBuilder simple and predictable. For complex queries, use the `query()` method with raw SQL - prepared statements still protect against SQL injection.

Requirements
------------

[](#requirements)

- PHP 8.2+
- PDO extension
- Database-specific PDO driver (pdo\_mysql, pdo\_pgsql, pdo\_sqlite)

Testing
-------

[](#testing)

```
# Install dependencies
composer install

# Run SQLite tests only (no Docker needed)
./vendor/bin/phpunit --exclude-group mysql,postgres

# Run full test suite (requires Docker)
docker-compose up -d
./vendor/bin/phpunit
docker-compose down
```

Acknowledgments
---------------

[](#acknowledgments)

Parts of this project (refactoring, documentation, code review) were developed with AI assistance (Claude).

License
-------

[](#license)

MIT

###  Health Score

39

—

LowBetter than 85% of packages

Maintenance89

Actively maintained with recent releases

Popularity6

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity46

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

96d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/4407ed68227862f26dfbd1fff5c4d117c89ba024bf20202280d00d8199036cfc?d=identicon)[SoDaHo](/maintainers/SoDaHo)

---

Top Contributors

[![SoDaHo](https://avatars.githubusercontent.com/u/73506118?v=4)](https://github.com/SoDaHo "SoDaHo (17 commits)")

---

Tags

cruddatabasemariadbmysqlpdopdo-wrapperphppostgresquery-buildersqlitephpdatabasemysqlsqlitepostgresmariadbpdocrudquery builderpdo-wrapper

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/sodaho-pdo-wrapper/health.svg)

```
[![Health](https://phpackages.com/badges/sodaho-pdo-wrapper/health.svg)](https://phpackages.com/packages/sodaho-pdo-wrapper)
```

###  Alternatives

[scienta/doctrine-json-functions

A set of extensions to Doctrine that add support for json query functions.

58725.2M48](/packages/scienta-doctrine-json-functions)[clouddueling/mysqldump-php

PHP version of mysqldump cli that comes with MySQL

1.3k23.1k](/packages/clouddueling-mysqldump-php)[tommyknocker/pdo-database-class

Framework-agnostic PHP database library with unified API for MySQL, MariaDB, PostgreSQL, SQLite, MSSQL, and Oracle. Query Builder, caching, sharding, window functions, CTEs, JSON, migrations, ActiveRecord, CLI tools, AI-powered analysis. Zero external dependencies.

816.0k](/packages/tommyknocker-pdo-database-class)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
