ndtan / dbf
Single-file PHP Database Framework — secure, PRO & Enterprise+ features.
Requires
- php: >=8.1
- ext-pdo: *
Requires (Dev)
- phpunit/phpunit: ^10.5 || ^11.0
README
NDT DBF — A single‑file PHP Database Framework (PRO · Enterprise+).
Secure by default, compact API, works as one file or via Composer/PSR‑4.
Website & Docs · Download single file · Donate
Table of Contents
- Features
- Requirements
- Installation
- Connection Setup
- Quick Start
- Query Builder
- Enterprise+ Features
- Raw SQL
- Security
- Debugging & Logging
- Tests & CI
- Contributing
- License
- Donate
Features
- ⚡️ Lightweight & single file: no third-party deps (PDO only)
- 🔐 Secure by default: prepared statements, per-driver identifier quoting, IN-list guard
- 🧱 Query Builder:
select / where / orWhere / whereIn / between / null / join / group / having / order / limit / offset
- 🧾 Full CRUD:
insert
,insertMany
,insertGet
(PG/SQLiteRETURNING
) - 🔁 Cross-dialect Upsert: MySQL (
ON DUP KEY
), PG/SQLite (ON CONFLICT
), safe fallback - 💳 Transactions: exponential backoff + jitter on deadlock
- 🚦 Readonly/Maintenance mode: block DML when system is frozen
- 🪶 Soft Delete:
withTrashed() / onlyTrashed() / restore() / forceDelete()
- 🧭 Master/Replica routing: auto/manual read-write split
- ⏱️ Per-query timeout (best-effort for MySQL/PG)
- 🧩 Middleware & Policy hooks + Logger & Metrics hook
- 📊 Aggregates & Sugar:
sum()
,avg()
,min()
,max()
,pluck()
- 🧪 Test Mode: build SQL without executing (great for unit tests/previews)
- 📚 Keyset pagination helper
Supports MySQL/MariaDB, PostgreSQL, SQLite, SQL Server.
Requirements
- PHP >= 8.1
- Extension: PDO (+ respective drivers:
pdo_mysql
,pdo_pgsql
,pdo_sqlite
,pdo_sqlsrv
)
Installation
Composer
composer require ndtan/dbf
<?php require 'vendor/autoload.php'; $db = new \ndtan\DBF([ 'type' => 'mysql', 'host' => '127.0.0.1', 'database' => 'app', 'username' => 'root', 'password' => 'secret', 'charset' => 'utf8mb4', 'logging' => false, ]);
Single file
Copy src/DBF.php
into your project:
<?php require __DIR__ . '/DBF.php'; $db = new \ndtan\DBF('mysql://root:secret@127.0.0.1/app?charset=utf8mb4');
You may also use ENV: set
NDTAN_DBF_URL
and callnew \ndtan\DBF()
.
Connection Setup
URI/DSN (one line)
$db = new \ndtan\DBF('pgsql://user:pass@localhost:5432/app'); $db = new \ndtan\DBF('sqlite:///:memory:'); // memory $db = new \ndtan\DBF('sqlite:///path/to/app.db'); // file $db = new \ndtan\DBF('sqlsrv://sa:pass@localhost/app');
Array config
$db = new \ndtan\DBF([ 'type' => 'mysql', // mysql|pgsql|sqlite|sqlsrv 'host' => 'localhost', 'database' => 'app', 'username' => 'root', 'password' => 'secret', 'charset' => 'utf8mb4', 'collation'=> 'utf8mb4_general_ci', 'prefix' => 'app_', 'logging' => true, 'error' => PDO::ERRMODE_EXCEPTION, 'option' => [PDO::ATTR_CASE => PDO::CASE_NATURAL], // Enterprise options 'readonly' => false, 'features' => [ 'soft_delete' => ['enabled'=>true,'column'=>'deleted_at','mode'=>'timestamp'], 'max_in_params' => 1000 ], 'command' => ['SET SQL_MODE=ANSI_QUOTES'], ]);
Existing PDO
$pdo = new PDO('mysql:host=127.0.0.1;dbname=app;charset=utf8mb4','root','secret',[ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ]); $db = new \ndtan\DBF(['pdo'=>$pdo, 'type'=>'mysql']);
Master/Replica
$db = new \ndtan\DBF([ 'write' => 'mysql://root:secret@10.0.0.10/app', 'read' => 'mysql://reader:secret@10.0.0.11/app', 'routing' => 'auto', // auto|manual|single 'prefix' => 'app_', ]); // Manual route when needed: $one = $db->using('read')->table('users')->first();
Quick Start
$users = $db->table('users') ->select(['id','email']) ->where('status','=','active') ->orderBy('id','desc') ->limit(20) ->get();
Query Builder
Basic select
$db->table('users')->get(); // SELECT * FROM users $db->table('users')->first(); // fetch one row $db->table('users')->exists(); // boolean $db->table('users')->count(); // COUNT(*)
Where conditions
$db->table('users')->where('status','=','active')->get(); $db->table('users')->where('id','>',100)->orWhere('email','=','a@ndtan.net')->get(); $db->table('users')->whereBetween('created_at',['2025-01-01','2025-12-31'])->get(); $db->table('users')->whereIn('id',[1,2,3])->get(); // guarded IN list $db->table('users')->whereNull('deleted_at')->get();
Join / Group / Having / Order / Limit
$db->table('orders') ->select(['orders.id','users.email','SUM(total) AS sum_total']) ->join('users','orders.user_id','=','users.id') ->groupBy(['orders.user_id']) ->having('SUM(total)','>',100) ->orderBy('orders.id','desc') ->limit(50)->offset(0) ->get();
CRUD
// Insert $id = $db->table('users')->insert(['email'=>'a@ndtan.net','status'=>'active']); // Insert many $db->table('order_items')->insertMany([ ['order_id'=>1,'sku'=>'A','qty'=>1], ['order_id'=>1,'sku'=>'B','qty'=>2], ]); // Insert + RETURNING (PG/SQLite) or re-fetch by id (MySQL/SQLSrv) $row = $db->table('users')->insertGet(['email'=>'b@ndtan.net','status'=>'vip'], ['id','email']); // Update $aff = $db->table('users')->where('id','=', $id)->update(['status'=>'vip']); // Delete (soft delete if enabled) $aff = $db->table('users')->where('id','=', $id)->delete();
Upsert
// MySQL: ON DUPLICATE KEY; PG/SQLite: ON CONFLICT; others: safe fallback (tx) $db->table('users')->upsert( ['email'=>'a@ndtan.net','status'=>'vip'], conflict: ['email'], updateColumns: ['status'] );
Aggregates & Pluck
$sum = $db->table('orders')->sum('total'); $avg = $db->table('orders')->avg('total'); $min = $db->table('orders')->min('total'); $max = $db->table('orders')->max('total'); $emails = $db->table('users')->pluck('email'); // ['a@x','b@y',...] $map = $db->table('users')->pluck('email','id'); // [1=>'a@x', 2=>'b@y', ...]
Keyset pagination
$page1 = $db->table('orders')->orderBy('id','desc')->limit(50)->getKeyset(null,'id'); $page2 = $db->table('orders')->orderBy('id','desc')->limit(50)->getKeyset($page1['next'],'id');
Enterprise+ Features
Transactions + Deadlock retry
$db->tx(function(\ndtan\DBF $tx) { $orderId = $tx->table('orders')->insert(['user_id'=>10,'total'=>100,'created_at'=>date('c')]); $tx->table('order_items')->insert(['order_id'=>$orderId,'sku'=>'A','qty'=>1]); }, attempts: 3); // auto-retry on deadlock
Readonly/Maintenance mode
$db->setReadonly(true); // any DML (INSERT/UPDATE/DELETE) will be blocked
Soft Delete guard
// Enable via config: features.soft_delete.enabled=true $db->table('users')->where('id','=',123)->delete(); // set deleted_at $db->table('users')->withTrashed()->first(); // include soft-deleted $db->table('users')->onlyTrashed()->get(); // only soft-deleted $db->table('users')->where('id','=',123)->restore(); // restore $db->table('users')->where('id','=',123)->forceDelete();// hard delete
Middleware / Policy / Metrics
// Middleware: audit $db->use(function(array $ctx, callable $next) { $start = microtime(true); $res = $next($ctx); error_log('SQL '.$ctx['type'].' took '.round((microtime(true)-$start)*1000,2).'ms'); return $res; }); // Policy: block UPDATE on users $db->policy(function(array $ctx){ if (($ctx['table'] ?? null)==='users' && ($ctx['action'] ?? '')==='update') { throw new RuntimeException('Policy blocked'); } }); // Metrics hook $db->setMetrics(function(array $m){ // $m = ['type'=>'select','table'=>'users','ms'=>12.3,'count'=>10, ...] });
Per-query timeout
$db->table('big_table')->timeout(1500)->get(); // 1.5s best-effort (MySQL/PG)
Test Mode
$db->setTestMode(true); // Queries are not executed; SQL & params are still built $rows = $db->table('users')->where('status','=','active')->limit(10)->get(); // [] echo $db->queryString(); print_r($db->queryParams());
Raw SQL
// Positional $rows = $db->raw('SELECT * FROM users WHERE email LIKE ?', ['%ndtan.net']); // Named $rows = $db->raw('SELECT * FROM users WHERE email = :e', ['e'=>'a@ndtan.net']); // DML $aff = $db->raw('UPDATE users SET status=? WHERE id=?', ['vip', 10]);
Security
- Prepared statements for all user inputs
- Identifier quoting per driver to prevent injection via table/column names
- IN-list guard: limit
whereIn
items (default 1000) - Readonly mode to block unintended writes
- Scope/Policy hooks for tenant/RBAC-like checks
DBF is “secure by default”, but you should still harden your DB (users/roles, TLS, firewall, backups, monitoring).
Debugging & Logging
$db->setLogger(function(string $sql, array $params, float $ms) { error_log(sprintf('[SQL %.2fms] %s | %s', $ms, $sql, json_encode($params))); }); $info = $db->info(); // driver, routing, readonly, soft_delete, test_mode...
Tests & CI
Run tests (SQLite :memory:
):
composer install vendor/bin/phpunit
The template repo ships with GitHub Actions (.github/workflows/ci.yml
) running PHPUnit on PHP 8.1–8.4.
Contributing
PRs/issues are welcome! Please:
- Fork → create a feature branch
- Add tests for your change
- Ensure
vendor/bin/phpunit
passes - Describe your change clearly in the PR
License
MIT © Tony Nguyen
Donate
If DBF helps your work, consider buying the author a coffee ☕️
👉 PayPal: https://www.paypal.com/paypalme/copbeo