memran / marwa-db
Lightweight, framework-agnostic PHP database toolkit: connections, query builder, ORM, schema, migrations, seeders, debug panel.
Requires
- php: ^8.2
- ext-json: *
- ext-pdo: *
- fakerphp/faker: ^1.23
- psr/log: ^3.0
- symfony/console: ^7.0
- symfony/string: ^7.3
Requires (Dev)
- memran/marwa-debugbar: 1.1.0
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^10.5.62
README
memran/marwa-db is a framework-agnostic PHP database toolkit built on PDO. It provides:
- connection management with pooling and retry support
- a fluent query builder
- an Active Record style ORM
- schema and migration helpers
- seeder discovery and execution
- query logging, a built-in debug panel, and optional
memran/marwa-debugbarintegration
The package is intended for plain PHP applications, small frameworks, and custom stacks that want database tooling without a full framework dependency.
Requirements
- PHP 8.2+
ext-pdoext-json- a supported PDO driver: MySQL, PostgreSQL, or SQLite
Installation
Install the package:
composer require memran/marwa-db
Optional development debug bar:
composer require --dev memran/marwa-debugbar
For work inside this repository:
composer install
Package Overview
Primary entry points:
Marwa\DB\BootstrapMarwa\DB\Connection\ConnectionManagerMarwa\DB\Facades\DBMarwa\DB\Query\BuilderMarwa\DB\ORM\ModelMarwa\DB\Schema\SchemaMarwa\DB\Seeder\SeedRunner
Quick Start
<?php require __DIR__ . '/vendor/autoload.php'; use Marwa\DB\Bootstrap; use Marwa\DB\Facades\DB; use Marwa\DB\ORM\Model; use Marwa\DB\Schema\Schema; $config = [ 'default' => [ 'driver' => 'sqlite', 'database' => __DIR__ . '/database.sqlite', 'debug' => true, ], ]; $manager = Bootstrap::init($config, enableDebugPanel: true); DB::setManager($manager); Model::setConnectionManager($manager); Schema::init($manager);
At this point you can:
- build queries through
DB::table(...) - configure models with
Model::setConnectionManager(...) - run schema operations with
Schema::create(...)andSchema::drop(...) - render debugging output with
echo $manager->renderDebugBar()whenmemran/marwa-debugbaris installed
Configuration
The package expects a named connection array. The simplest configuration looks like this:
return [ 'default' => [ 'driver' => 'mysql', 'host' => '127.0.0.1', 'port' => 3306, 'database' => 'app', 'username' => 'root', 'password' => '', 'charset' => 'utf8mb4', 'options' => [], 'debug' => false, ], ];
SQLite example:
return [ 'default' => [ 'driver' => 'sqlite', 'database' => __DIR__ . '/../database/app.sqlite', 'debug' => true, ], ];
Supported top-level connection fields:
driverhostportdatabaseusernamepasswordcharsetoptionsdebug
Bootstrap and Connection Management
Bootstrap::init(array $dbConfig, ?LoggerInterface $logger = null, bool $enableDebugPanel = false): ConnectionManager
Creates the ConnectionManager, optionally enables debugging helpers, and stores the manager globally in $GLOBALS['cm'].
use Marwa\DB\Bootstrap; $manager = Bootstrap::init($config, enableDebugPanel: true);
When enableDebugPanel is true:
- the built-in
DebugPanelis attached QueryLoggeris attachedmemran/marwa-debugbaris attached automatically if installed
ConnectionManager
Common public methods:
getPdo(?string $name = 'default'): PDOgetConnection(?string $name = 'default'): PDOtransaction(Closure $callback, ?string $connectionName = null): mixedsetDebugPanel(?DebugPanel $panel): voidgetDebugPanel(): ?DebugPanelsetDebugBar(?object $debugBar): voidgetDebugBar(): ?objectrenderDebugBar(): stringsetQueryLogger(?QueryLogger $queryLogger): voidgetQueryLogger(): ?QueryLoggerisDebug(string $name = 'default'): boolgetDriver(string $name = 'default'): stringpickReplica(array $replicas): PDO
Example:
$pdo = $manager->getPdo(); $manager->transaction(function (PDO $connection): void { $connection->exec("INSERT INTO users (name) VALUES ('Alice')"); });
Global Query Instrumentation
Query logging is captured below the query builder layer. Any SQL executed through the PDO returned by ConnectionManager::getPdo() is loggable:
- query builder statements
- ORM/model statements
- raw
PDO::query(...) - raw
PDO::exec(...) - prepared statements created through
PDO::prepare(...)->execute(...)
This is package-level behavior. Application code does not need a separate wrapper around raw PDO usage.
Query Builder
The query builder is available through DB::table(...) or by instantiating Marwa\DB\Query\Builder directly.
DB::setManager(ConnectionManager $cm): void
Registers the shared manager used by the facade.
DB::table(string $table, string $conn = 'default'): Builder
Starts a fluent query against a table.
use Marwa\DB\Facades\DB; DB::setManager($manager); $users = DB::table('users') ->select('id', 'email') ->where('status', '=', 'active') ->orderBy('id', 'desc') ->limit(10) ->get();
Query builder methods
Selection and table:
table(string $table): selffrom(string $table): selfselect(string ...$columns): selfselectRaw(string $expression, array $bindings = []): self
Filtering and ordering:
where(string $column, string $operator, mixed $value, string $boolean = 'and'): selforWhere(string $column, string $operator, mixed $value): selfwhereIn(string $column, array $values, bool $not = false, string $boolean = 'and'): selfwhereNotIn(string $column, array $values, string $boolean = 'and'): selfwhereNull(string $column, string $boolean = 'and'): selfwhereNotNull(string $column, string $boolean = 'and'): selforderBy(string $column, string $direction = 'asc'): selflimit(int $n): selfoffset(int $n): self
Reading:
get(int $fetchMode = PDO::FETCH_ASSOC): arrayfirst(int $fetchMode = PDO::FETCH_ASSOC): array|object|nullvalue(string $column): mixedpluck(string $column): Collectioncount(string $column = '*'): intmax(string $column): mixedmin(string $column): mixedsum(string $column): int|float|nullavg(string $column): ?floatpaginate(int $perPage = 15, int $page = 1, int $fetchMode = PDO::FETCH_ASSOC): array
Writing:
insert(array $data): intupdate(array $data): intdelete(): int
Debugging helpers:
toSql(): stringgetBindings(): arrayclear(): void
Example:
$total = DB::table('orders') ->where('status', '=', 'paid') ->sum('amount');
ORM
Extend Marwa\DB\ORM\Model to define your models.
use Marwa\DB\ORM\Model; final class User extends Model { protected static ?string $table = 'users'; protected static array $fillable = ['name', 'email']; }
Register the connection manager once:
User::setConnectionManager($manager);
Common model API
Setup:
setTable(string $table): voidsetConnectionManager(ConnectionManager $cm, string $connection = 'default'): voidtable(): string
Query entry points:
query(): Marwa\DB\ORM\QueryBuilderwhere(string $col, string $op, mixed $val): Marwa\DB\ORM\QueryBuilderall(): arrayfind(int|string $id): ?staticfindOrFail(int|string $id): static
Writes:
create(array $attributes): staticsave(): booldelete(): boolforceDelete(): boolrestore(): booldestroy(int|array $ids): intrefresh(): static
Attribute and serialization helpers:
fill(array $attributes): staticgetDirty(): arraygetKey(): int|string|nullgetKeyName(): stringgetAttribute(string $key): mixedtoArray(): arraytoJson(int $options = JSON_UNESCAPED_UNICODE): string
Scopes and soft-delete toggles:
addGlobalScope(Closure $scope, ?string $identifier = null): voidwithoutGlobalScope(string $identifier): staticwithTrashed(): staticonlyTrashed(): static
Example:
$user = User::find(1); if ($user !== null) { $user->fill([ 'email' => 'new@example.com', ])->save(); }
ORM Query Builder
Marwa\DB\ORM\QueryBuilder is returned by Model::query() and hydrates records into model instances.
Common methods:
select(string ...$cols): selfselectRaw(string $expr, array $bindings = []): selfwhere(string $col, string $op, mixed $val): selfwhereIn(string $col, array $values): selforderBy(string $col, string $dir = 'asc'): selflimit(int $n): selfoffset(int $n): selfwith(string ...$relations): selfget(): arrayfirst(): ?Modelinsert(array $data): intupdate(array $data): intdelete(): intcount(string $col = '*'): intmax(string $col): mixedmin(string $col): mixedsum(string $col): int|float|nullavg(string $col): ?floatgetBaseBuilder(): Marwa\DB\Query\Builder
Schema Builder
The schema layer is centered on Marwa\DB\Schema\Schema and Marwa\DB\Schema\Builder.
Schema::init(?ConnectionManager $cm = null, ?string $connectionName = null): void
Initializes the static schema facade. If $cm is omitted, the package reads $GLOBALS['cm'].
Schema::create(string $table, callable $callback): void
Creates a table.
Schema::drop(string $table): void
Drops a table.
Example:
use Marwa\DB\Schema\Schema; Schema::init($manager); Schema::create('posts', static function ($table): void { $table->increments('id'); $table->string('title'); $table->text('body'); $table->timestamps(); });
For instance-based use:
Builder::useConnectionManager(ConnectionManager $cm): BuilderBuilder::create(string $table, Closure $callback): voidBuilder::table(string $table, Closure $callback): voidBuilder::drop(string $table): voidBuilder::rename(string $from, string $to): void
Blueprint column helpers
Common schema methods on the table blueprint:
increments()bigIncrements()uuid()uuidPrimary()string()text()mediumText()longText()integer()tinyInteger()smallInteger()bigInteger()boolean()decimal()float()double()date()dateTime()timestamp()timestamps()softDeletes()json()jsonb()binary()enum()set()foreignId()primary()unique()index()foreign()
Column modifiers are available through ColumnDefinition, including:
nullable()default()unsigned()autoIncrement()primary()length()comment()unique()index()primaryKey()
Migrations
Migration helpers are available through the CLI and through Marwa\DB\Schema\MigrationRepository.
Common MigrationRepository methods:
ensureTable(): voidmigrate(): introllbackLastBatch(): introllbackAll(): intgetRanWithDetails(): arraygetMigrationFiles(): array
Migration files generated by the package return an anonymous class extending Marwa\DB\CLI\AbstractMigration.
Example generated structure:
<?php use Marwa\DB\CLI\AbstractMigration; use Marwa\DB\Schema\Schema; return new class extends AbstractMigration { public function up(): void { Schema::create('users', function ($table) { $table->increments('id'); $table->string('name'); $table->timestamps(); }); } public function down(): void { Schema::drop('users'); } };
Seeders
Seeder execution is handled by Marwa\DB\Seeder\SeedRunner.
SeedRunner
Constructor:
new SeedRunner( cm: $manager, logger: null, connection: 'default', seedPath: __DIR__ . '/database/seeders', seedNamespace: 'Database\\Seeders', );
Public methods:
runAll(bool $wrapInTransaction = true, ?array $only = null, array $except = []): voidrunOne(string $fqcn, bool $wrapInTransaction = true): voiddiscoverSeeders(): array
Example:
use Marwa\DB\Seeder\SeedRunner; $runner = new SeedRunner($manager); $runner->runAll();
Seeder classes implement Marwa\DB\Seeder\Seeder:
use Marwa\DB\Seeder\Seeder; final class UsersTableSeeder implements Seeder { public function run(): void { // seed logic } }
Debugging and Query Inspection
Built-in debug panel
Enable it during bootstrap:
$manager = Bootstrap::init($config, enableDebugPanel: true);
Render the built-in panel:
echo $manager->getDebugPanel()?->render();
Public DebugPanel methods:
addQuery(string $sql, array $bindings, float $timeMs, string $connection = 'default', ?string $error = null): voidall(): arrayclear(): voidrender(): string
Optional memran/marwa-debugbar
If memran/marwa-debugbar is installed as a dev dependency, Bootstrap::init(..., enableDebugPanel: true) will attach it automatically.
Render through the manager:
echo $manager->renderDebugBar();
Render through the package helper:
echo \Marwa\DB\Support\db_debugbar();
The helper reads the global manager from $GLOBALS['cm'] when no explicit manager is passed.
Query logger
Marwa\DB\Logger\QueryLogger stores query records in memory and can mirror them to a PSR-3 logger.
Public methods:
log(string $sql, array $bindings, float $timeMs, string $connection, ?string $error = null): voidall(): arrayclear(): void
CLI
The repository includes a Symfony Console entrypoint:
php bin/marwa-db list
Available commands:
migratemigrate:rollbackmigrate:refreshmigrate:statusmake:migrationmake:seederdb:seed
Examples:
php bin/marwa-db migrate php bin/marwa-db migrate:status php bin/marwa-db make:migration create_users_table php bin/marwa-db make:seeder UsersTableSeeder php bin/marwa-db db:seed php bin/marwa-db db:seed --only=UsersTableSeeder
Testing and Quality
Run the full test suite:
composer test
Run unit tests only:
composer test:unit
Run integration tests:
composer test:integration
Run static analysis:
composer run analyse
Run syntax linting:
composer lint
Run the standard CI gate locally:
composer run ci
Integration Notes
DB::setManager(...)must be called before using theDBfacade.Model::setConnectionManager(...)must be called before using the ORM.Schema::init(...)must be called before using the static schema facade unless you rely on the global manager created byBootstrap::init(...).- Query logging is automatic for all SQL executed through
ConnectionManager::getPdo(). memran/marwa-debugbaris optional and intended for development use.
Security Notes
- Do not commit production credentials.
- Keep debug tooling disabled outside trusted environments.
- Prefer configuration loaded from environment-aware application code.
- Treat rendered debug output as sensitive because it may contain SQL, bindings, request state, and exception details.
License
MIT. See LICENSE.