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.
- PHP 8.2+
ext-pdoext-json- a supported PDO driver: MySQL, PostgreSQL, or SQLite
Install the package:
composer require memran/marwa-dbOptional development debug bar:
composer require --dev memran/marwa-debugbarFor work inside this repository:
composer installPrimary entry points:
Marwa\DB\BootstrapMarwa\DB\Connection\ConnectionManagerMarwa\DB\Facades\DBMarwa\DB\Query\BuilderMarwa\DB\ORM\ModelMarwa\DB\Schema\SchemaMarwa\DB\Seeder\SeedRunner
<?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
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::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
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')");
});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.
The query builder is available through DB::table(...) or by instantiating Marwa\DB\Query\Builder directly.
Registers the shared manager used by the facade.
Starts a fluent query against a table.
DB::connection(?string $name = null): ConnectionManagerDB::beginTransaction(string $conn = 'default'): voidDB::commit(string $conn = 'default'): voidDB::rollback(string $conn = 'default'): voidDB::transaction(callable $callback, string $conn = 'default'): mixed
Example:
$result = DB::transaction(function () {
User::create(['name' => 'Alice']);
Order::create(['user_id' => 1, 'total' => 100]);
});use Marwa\DB\Facades\DB;
DB::setManager($manager);
$users = DB::table('users')
->select('id', 'email')
->where('status', '=', 'active')
->orderBy('id', 'desc')
->limit(10)
->get();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'): selfwhereJsonContains(string $column, mixed $value): selfwhereJsonLength(string $column, int $length): selfwhereJsonValue(string $column, string $path, mixed $value): 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');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);The Observable trait provides event hooks for model lifecycle:
Model::onCreating(callable $callback): voidModel::onCreated(callable $callback): voidModel::onUpdating(callable $callback): voidModel::onUpdated(callable $callback): voidModel::onSaving(callable $callback): voidModel::onSaved(callable $callback): voidModel::onDeleting(callable $callback): voidModel::onDeleted(callable $callback): void
Example:
User::onCreated(function ($user) {
Log::info("User created: {$user->id}");
});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): staticfirstOrFail(): staticexists(): boolcount(string $col = '*'): intchunk(int $size, callable $callback): voidchunkById(int $size, callable $callback, string $idCol = 'id'): void
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
The package includes relation classes for eager loading:
HasOne- one-to-one relationshipHasMany- one-to-many relationshipBelongsTo- inverse of HasMany/HasOneBelongsToMany- many-to-many via pivot tableMorphTo- polymorphic (morphTo)MorphMany- polymorphic (morphMany)
Example:
// In User model
public function posts(): HasMany
{
return new HasMany(static::$cm, static::$connection, static::class, Post::class, 'user_id');
}Example:
$user = User::find(1);
if ($user !== null) {
$user->fill([
'email' => 'new@example.com',
])->save();
}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(): ?ModelfirstOrFail(): Modelexists(): boolinsert(array $data): intupdate(array $data): intdelete(): intcount(string $col = '*'): intmax(string $col): mixedmin(string $col): mixedsum(string $col): int|float|nullavg(string $col): ?floatchunk(int $size, callable $callback): voidchunkById(int $size, callable $callback, string $idCol = 'id'): voidgetBaseBuilder(): Marwa\DB\Query\Builder
The schema layer is centered on Marwa\DB\Schema\Schema and Marwa\DB\Schema\Builder.
Initializes the static schema facade. If $cm is omitted, the package reads $GLOBALS['cm'].
Creates a table.
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
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()
Blueprint methods:
comment(string $comment): self- set table comment
Column modifiers are available through ColumnDefinition, including:
nullable()default()unsigned()autoIncrement()comment()- column commentprimary()length()comment()unique()index()primaryKey()
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');
}
};Seeder execution is handled by Marwa\DB\Seeder\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
}
}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
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.
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
The repository includes a Symfony Console entrypoint:
php bin/marwa-db listAvailable 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=UsersTableSeederRun the full test suite:
composer testRun unit tests only:
composer test:unitRun integration tests:
composer test:integrationRun static analysis:
composer run analyseRun syntax linting:
composer lintRun the standard CI gate locally:
composer run ciDB::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.
- 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.
MIT. See LICENSE.