soluble/dbwrapper
Soluble DbWrapper provides a lightweight, fluent wrapper around Laravel’s database layer, simplifying common query and persistence tasks while keeping familiar Query Builder conventions. It helps reduce boilerplate for CRUD, filtering, and reusable database logic in PHP applications.
Install
composer require soluble/dbwrapper
The package is a thin PDO wrapper — no Laravel service provider, no facade, no config file. You wire it up yourself, which is exactly the point.
Where to look first
src/DbWrapper.php — the static entry point (DbWrapper::connect(...)) and the instance API.src/Connection/PdoConnection.php — the actual PDO implementation; every method you'll call day-to-day lives through here.First use case
The canonical "hello world" is a one-off script or an artisan command hitting a database where pulling in Eloquent or even DB:: feels heavy:
use Soluble\DbWrapper\DbWrapper;
$db = DbWrapper::connect(
'mysql:host=127.0.0.1;dbname=legacy;charset=utf8mb4',
'user',
'secret'
);
$rows = $db->fetchAll('SELECT id, name FROM users WHERE active = ?', [1]);
If that runs, you've seen 80% of what the package does. The remaining 20% is transactions, insert/update/delete helpers, and PSR-3 logging.
Bind it as a singleton in a service provider
Don't call connect() on every request — the value of the wrapper is a reusable connection object.
// AppServiceProvider::register()
$this->app->singleton('legacy.db', function ($app) {
return DbWrapper::connect(
config('legacy.dsn'),
config('legacy.user'),
config('legacy.pass'),
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
});
Then resolve it via app('legacy.db') in commands, jobs, and services. This is the pattern that makes the package earn its keep: a second, isolated connection to a database you don't want Eloquent managing.
Raw writes with the built-in helpers
The wrapper's insert / update / delete methods are thin sugar over prepared statements — no query builder, no model, no mass-assignment logic. That's a feature when you're writing to a legacy schema you don't control:
$db->insert('audit_log', [
'actor' => $userId,
'action' => 'login',
'at' => date('Y-m-d H:i:s'),
]);
$db->update('users', ['last_seen' => $now], 'id = :id', ['id' => $userId]);
Transactions around multi-step work
$db->beginTransaction();
try {
$db->execute('UPDATE accounts SET balance = balance - ? WHERE id = ?', [$amt, $from]);
$db->execute('UPDATE accounts SET balance = balance + ? WHERE id = ?', [$amt, $to]);
$db->commit();
} catch (\Throwable $e) {
$db->rollBack();
throw $e;
}
Use this for the cross-database transfer case — pulling from Eloquent's DB and writing to the legacy DB is the archetypal reason to reach for this package.
Wire in a PSR-3 logger for observability
The wrapper supports a logger (PSR-3). In a Laravel app this is the cheapest way to get query visibility from an otherwise opaque connection:
$db->setLogger(app('log'));
Now slow or failing queries land in your normal log channels instead of disappearing.
Testing: swap in SQLite
Because the public API is PDO-shaped, your test suite can hand the same DbWrapper an in-memory SQLite connection. Structure your code to take the wrapper as a constructor dependency rather than calling DbWrapper::connect() inside business logic, and tests become:
$db = DbWrapper::connect('sqlite::memory:');
$db->execute('CREATE TABLE ...');
Use alongside Eloquent, not instead of it
Keep Eloquent for your first-party domain. Reach for soluble/dbwrapper when: the connection is to a third-party/legacy DB, you don't want model classes, the query is genuinely raw, or you're writing a CLI/worker where bootstrapping Laravel's DB stack is disproportionate.
It's a 2019 package — audit before adopting. Last release January 2019, 7 stars, zero dependents. It's small, MIT, and unlikely to have deep bugs, but verify it runs on your PHP version (PHP 8 typed-property / deprecation issues are plausible) before betting production on it. If it doesn't fit, a ~100-line PDO subclass reproduces most of the API.
No Laravel integration ships with it. No facade, no config/dbwrapper.php, no service provider. Any Laravel-flavored convenience is on you — which is fine, but don't expect php artisan vendor:publish to do anything.
Parameter style is PDO's. Positional ? and named :name both work, but don't mix them in one statement. Named parameters in the helpers ($db->update(...)) require the WHERE clause to use the same style as the $params keys.
No query builder. Everything is a string. Concatenating user input into SQL bypasses any safety net — always pass values through the params array. The "minimalist" in the tagline means zero hand-holding here.
Set PDO options at connect time. Because there's no config layer, driver behavior (error mode, emulated prepares, fetch mode, persistent connections) is decided entirely by the $options array you pass to connect(). Set PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION explicitly — the default is silent failure, and a wrapper that returns false on error is a debugging nightmare.
Catch PDOException, not package-specific exceptions. Errors bubble up as PDO exceptions. Wrap calls in your own domain exceptions if you want them to mean anything at the application layer.
Transactions are per-wrapper-instance. If you resolve the connection twice (e.g., DbWrapper::connect() in two places), you get two PDO handles and two independent transaction scopes. Register it as a singleton, or you'll silently lose atomicity. Laravel's DB::transaction() will not cover this connection.
Extension point: the Connection interface. If you need a non-PDO driver or a decorator (retry logic, metrics), implement the package's Connection interface and hand it to DbWrapper. This is the intended seam — subclassing PdoConnection also works but couples you to its internals.
Debugging tip: when a query silently does nothing, first confirm you're on the instance with the logger attached, then check ERRMODE. Nine out of ten "the wrapper is broken" reports are a missing exception mode or a transaction started on a different instance.
Pin the version. With a single 2019 release and no maintainer activity, "soluble/dbwrapper": "^1.0" plus a lockfile is safer than a floating constraint, in case a stale branch ever gets tagged.
How can I help you explore Laravel packages today?