Arxaron — a secure, robust, high-performance PHP database library built directly on mysqli (no PDO), with a fluent query builder, active-record models, transactions, and optional Redis (predis) caching.

Maintainers

Package info

github.com/sobhanmohammadi-dev/arxaron

pkg:composer/sobhanmohammadi/arxaron

Transparency log

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.0.1 2026-08-01 10:58 UTC

This package is auto-updated.

Last update: 2026-08-01 11:06:23 UTC


README

Package: sobhanmohammadi/arxaron

A secure, robust, and performant database library for PHP 8.3, 8.4, and 8.5 — built directly on mysqli (never PDO), with a fluent query builder, an optional active-record layer, real transactions with savepoints, and first-class Redis query caching via predis/predis.

📖 See docs/GUIDE.md for the complete, in-depth usage guide — every method, every config option, transactions, caching, security model, troubleshooting, and a full end-to-end example. This README is just a quick start.

Why this exists

  • mysqli-only. No PDO anywhere in the stack, including for prepared statements — everything goes through mysqli_stmt.
  • Security by construction. All values are bound as prepared-statement parameters. Table/column names are validated against a strict whitelist pattern and backtick-quoted. update()/delete() refuse to run without a WHERE clause unless you explicitly opt out.
  • Robust. Automatic, bounded reconnect-and-retry on transient errors ("MySQL server has gone away"), nested transactions via SAVEPOINT, deadlock-aware transaction retries, and connection error messages that never leak credentials.
  • Fast. Prepared statement reuse per call, buffered or streamed (cursor()) result fetching, and an optional Redis cache layer with tag-based invalidation that fails open (a cache outage never breaks your app — it just stops caching).
  • PHP 8.3 – 8.5 compatible. Uses only stable, non-deprecated APIs (readonly properties, enums-free, match, named arguments) so it runs unmodified across the 8.3/8.4/8.5 line.

Installation

composer require sobhanmohammadi/arxaron

# Optional, only if you want Redis-backed caching:
composer require predis/predis

Quick start

use Arxaron\Config;
use Arxaron\Database;

Database::addConnection(Config::fromArray([
    'host'     => '127.0.0.1',
    'username' => 'app',
    'password' => getenv('DB_PASSWORD'),
    'database' => 'shop',
    'charset'  => 'utf8mb4',
]));

// Optional: enable Redis-backed query caching (uses predis/predis).
Database::useRedisCache(new Predis\Client([
    'scheme' => 'tcp',
    'host'   => '127.0.0.1',
    'port'   => 6379,
]));

// Fluent query builder
$activeUsers = Database::table('users')
    ->select('id', 'name', 'email')
    ->where('active', '=', 1)
    ->orderBy('created_at', 'DESC')
    ->limit(20)
    ->get();

foreach ($activeUsers as $row) {
    echo $row['name'], "\n";
}

// Raw parameterized SQL (positional or named placeholders)
$row = Database::query('SELECT * FROM users WHERE email = :email', [
    'email' => 'ada@example.com',
])->first();

Query builder

Database::table('orders')
    ->select('orders.id', 'orders.total', 'customers.name')
    ->join('customers', 'orders.customer_id', '=', 'customers.id')
    ->where('orders.status', '=', 'paid')
    ->whereBetween('orders.created_at', $start, $end)
    ->whereIn('orders.region', ['EU', 'UK'])
    ->orWhere('orders.priority', '=', 'high')
    ->groupBy('customers.id')
    ->having('SUM(orders.total)', '>', 1000)
    ->orderBy('orders.total', 'DESC')
    ->paginate(page: 2, perPage: 25)
    ->get();

Grouped conditions:

Database::table('products')
    ->where('active', '=', 1)
    ->whereGroup(function ($q) {
        $q->where('price', '<', 10)->orWhere('on_sale', '=', 1);
    })
    ->get();

Writes:

$id = Database::table('users')->insert([
    'name'  => 'Ada Lovelace',
    'email' => 'ada@example.com',
]);

Database::table('users')->insertMany([
    ['name' => 'Grace', 'email' => 'grace@example.com'],
    ['name' => 'Alan',  'email' => 'alan@example.com'],
]);

Database::table('users')->where('id', '=', $id)->update(['active' => 0]);
Database::table('users')->where('id', '=', $id)->delete();

update() and delete() throw a QueryException if you forget a WHERE clause — use ->whereRaw('1=1') to explicitly confirm a full-table operation.

Transactions

Database::transaction(function ($conn) {
    $conn->execute('UPDATE accounts SET balance = balance - ? WHERE id = ?', [100, 1]);
    $conn->execute('UPDATE accounts SET balance = balance + ? WHERE id = ?', [100, 2]);
});

Nested transaction() calls automatically use SAVEPOINTs. Deadlocks (MySQL error 1213) and lock-wait timeouts (1205) at the outermost level are retried automatically with a small randomized backoff.

Redis query caching

$topProducts = Database::table('products')
    ->where('active', '=', 1)
    ->orderBy('sales', 'DESC')
    ->limit(10)
    ->cache(ttlSeconds: 60) // cached under a key tagged with "products"
    ->get();

// Any insert/update/delete on `products` automatically invalidates
// everything cached under that tag.

Cache reads/writes are fail-open: if Redis is unreachable, Arxaron transparently falls back to querying MySQL directly rather than throwing.

Active record (optional)

use Arxaron\Model;

final class User extends Model
{
    protected static string $table = 'users';
    protected static array $fillable = ['name', 'email'];
    protected static ?int $cacheTtl = 30;
}

$user = User::create(['name' => 'Ada', 'email' => 'ada@example.com']);
$user = User::find(1);
$user->name = 'Ada Lovelace';
$user->save();
$user->delete();

foreach (User::query()->where('active', '=', 1)->get() as $row) {
    // ...
}

Streaming large result sets

foreach (Database::connection()->cursor('SELECT * FROM events') as $row) {
    // processed one row at a time, not buffered in memory
}

Multiple connections

Database::addConnection(Config::fromArray([...]), name: 'analytics');
Database::table('events', connection: 'analytics')->get();

Configuration reference (Config)

Option Default Notes
host Prefix with p: (or set persistent: true) for a persistent connection.
username / password
database
port 3306
socket null Unix socket path, if used instead of TCP.
charset utf8mb4
ssl_enabled false Plus ssl_key, ssl_cert, ssl_ca_cert, ssl_ca_path, ssl_cipher.
connect_timeout 5 Seconds.
read_timeout 0 (system default) Seconds.
strict_mode true Sets strict sql_mode + isolation level on connect.
isolation_level REPEATABLE READ One of the four standard SQL levels.
max_reconnect_attempts 2 Bounded automatic retry on transient connection errors.

Security notes

  • Every value ever placed into SQL is sent via mysqli_stmt::bind_param. There is no string-concatenation path for values.
  • Table/column identifiers passed as plain strings are validated against ^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)?$ and backtick-quoted (with embedded backticks doubled) before use.
  • whereRaw() / selectRaw() are explicit, clearly named escape hatches for expressions that can't be expressed otherwise (e.g. COUNT(*)); callers are responsible for never interpolating untrusted input into them.
  • Connection errors are sanitized so (using password: YES/NO)-style fragments never leak into logs or exceptions.
  • Cached values are serialize()d but unserialize()d with allowed_classes => false, preventing PHP object-injection even if a cache backend were ever compromised.

Requirements

  • PHP 8.3, 8.4, or 8.5
  • ext-mysqli
  • predis/predis ^2.2 or ^3.0 (only if you use Redis caching)

License

MIT