Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Dbal Fluid Schema Builder Laravel Package

thecodingmachine/dbal-fluid-schema-builder

A fluent, object-oriented PHP API for building Doctrine DBAL database schemas. Define tables, columns, indexes, and foreign keys with readable chained methods, making schema creation and migrations cleaner, more expressive, and less error-prone in any PHP project.

Deep Wiki
Context7

Getting Started

This is not a Laravel package — there is no service provider, no config file, no facade, no publishable assets. It is a framework-agnostic wrapper around doctrine/dbal's SchemaManager that gives you a chained, readable way to express DDL. That framing should drive your whole evaluation of it.

composer require thecodingmachine/dbal-fluid-schema-builder

The first thing to check after install is what DBAL major it pulled. The package's last release is January 2024, so it targets DBAL 3.x. Laravel 11 dropped its own doctrine/dbal dependency (and getDoctrineConnection() with it), so on L11+ you must wire a connection yourself and pin doctrine/dbal:^3.6 in composer.json to avoid a DBAL 4 upgrade silently breaking the package.

Where to look first, in order:

  1. README.md — short, shows the fluent shape.
  2. src/FluidSchema.php, src/FluidTable.php, src/FluidColumn.php — the actual API surface.
  3. tests/ — this is the real documentation; the tests show every column type, index, and FK permutation and what SQL each produces.

Wiring a connection from Laravel config (do this lazily in a command/migration, never at boot):

use Doctrine\DBAL\DriverManager;
use TheCodingMachine\FluidSchema\FluidSchema;

function fluidSchema(): FluidSchema
{
    $cfg = config('database.connections.'.config('database.default'));

    $params = [
        'driver'   => 'pdo_mysql',           // pdo_pgsql, pdo_sqlite
        'host'     => $cfg['host'],
        'port'     => $cfg['port'],
        'dbname'   => $cfg['database'],      // note: dbname, not database
        'user'     => $cfg['username'],      // note: user, not username
        'password' => $cfg['password'],
        'charset'  => $cfg['charset'] ?? 'utf8mb4',
    ];

    return new FluidSchema(DriverManager::getConnection($params));
}

For SQLite (:memory: in tests) use ['driver' => 'pdo_sqlite', 'path' => $cfg['database']].

First use case: write one migration that creates a table with a foreign key and two indexes, then run php artisan migrate and inspect the generated SQL. If the generated names and types match what your Eloquent models expect, the package is viable for you; if not, stop here and keep Schema::create.

public function up(): void
{
    fluidSchema()->createTable('invoices')
        ->column('id')->integer()->autoIncrement()->primaryKey()
        ->column('customer_id')->integer()->notNull()
        ->column('number')->string(32)->notNull()
        ->column('total_cents')->bigInteger()->notNull()->default(0)
        ->column('notes')->text()->nullable()
        ->column('issued_at')->datetime()->notNull()
        ->index(['customer_id'])
        ->uniqueIndex(['number'])
        ->foreignKey('customer_id')->references('id')->on('customers')->onDelete('cascade')
        ->save();
}

(The fluent objects buffer the changes; the terminal call — save()/commit() depending on release — flushes them to the SchemaManager. Confirm the exact terminal method in FluidSchema.php before committing; the rest of the method names follow the column(...)->type()->modifier() shape.)

Implementation Patterns

Additive alter migrations. This is where the fluid syntax is genuinely nicer than raw DDL and safer than Laravel's ->change(). Fetch an existing table and append to it:

public function up(): void
{
    fluidSchema()->getTable('invoices')
        ->column('paid_at')->datetime()->nullable()
        ->column('stripe_id')->string(64)->nullable()
        ->index(['stripe_id'])
        ->save();
}

Idempotent "ensure schema" scripts. Because you hold a real DBAL connection, you get SchemaManager introspection for free, which is the pattern that justifies the dependency in a Laravel app — e.g. a multi-tenant provisioning command that must be re-runnable:

$sm = $dbal->createSchemaManager();

if (! $sm->tablesExist(['tenant_settings'])) {
    $schema->createTable('tenant_settings')->/* ... */->save();
}

Do the same with $sm->introspectTable('invoices')->hasColumn('paid_at') for column-level guards. Laravel's Schema::hasColumn() is a string check against a cached listing; DBAL's introspection is richer and gives you index/foreign-key metadata too.

Shared schema definitions across frameworks. The real reason this package exists: if the same schema is created by a Symfony app (Doctrine) and a Laravel app, put the fluent definitions in a shared composer package and call them from both sides. In a pure-Laravel shop this is dead weight.

Domain macros via subclassing. There is no Schema::macro() here — it's plain PHP. Subclass FluidSchema/FluidTable to add your house conventions:

class AppFluidSchema extends FluidSchema
{
    // add timestamps(), softDeletes(), money('total') helpers
}

Keep them thin wrappers over column()->datetime()->nullable() etc., and unit-test the generated SQL rather than the object graph.

Testing. Run the whole thing against pdo_sqlite with :memory: in Pest/PHPUnit. Assert on $sm->introspectTable('invoices')->getColumns() rather than on SQL strings — SQLite's ALTER TABLE rewrite path means string assertions are brittle while structural assertions are stable.

In migrations, keep down() honest. The builder is forward-only. Write the dropTable('invoices') / dropColumn calls by hand; don't rely on an auto-reverse.

Gotchas and Tips

  • Two schema toolchains is a smell. Laravel already has a good schema builder. Adopt this only if you need Doctrine interop (entity mapping, cross-framework shared schema, or richer introspection). Otherwise you have .env-driven table prefixes, Schema:: macros, and IDE completion on one side and a bare DBAL connection on the other.
  • Table prefixes are ignored. DB_PREFIX is applied by Laravel's grammar, not by DBAL. If you use prefixes, prefix manually or the migration will target unprefixed tables.
  • Auto-generated constraint names differ. DBAL emits hashed names like FK_5A8A6C8B... / UNIQ_..., not Laravel's invoices_number_unique. Anything that greps for constraint names (ops runbooks, raw DROP FOREIGN KEY statements, dump-diff tooling) will break. Pass explicit names to index()/foreignKey().
  • DDL transactions are engine-dependent. PostgreSQL rolls back a failed multi-statement migration; MySQL commits DDL implicitly and leaves you half-migrated. Write table creation in one call and check SHOW CREATE TABLE after the first run.
  • timestamps() doesn't exist. You add created_at/updated_at as nullable datetimes yourself. Eloquent will still populate them, but useCurrent() has no equivalent — use ->default('CURRENT_TIMESTAMP') only if you've verified the platform accepts it, otherwise leave nullable.
  • Defaults on TEXT/JSON columns fail on MySQL. ->default(null) is not the same as ->nullable(); use ->nullable() and a null column default will follow.
  • DBAL version lockdown. Do not let Composer float to DBAL 4. If another dependency (Doctrine ORM, an older Laravel ->change() implementation) also requires DBAL, resolve the constraint before writing migrations, not after.
  • No transaction around the fluid object. If a fluent call throws mid-chain, nothing has executed; if save() partially executes, you're on your own. For multi-table changes on MySQL, wrap in an idempotent command with introspection guards rather than a transaction.
  • Debugging: attach DBAL's logging middleware (Doctrine\DBAL\Logging\Middleware) or call $sm->getCreateTableSQL() on a temp table to see exactly what will run before you run it. This is the single fastest way to diagnose "why is my column the wrong type."
  • Lazy connections only. Never instantiate the FluidSchema/DBAL connection in a service provider — Octane workers and long-lived queue processes will hold stale handles. Build it inside the migration or command method.
  • Don't reach for it for data. It's schema-only. Keep DB::table(...) for backfills; mixing seeding into a fluid migration makes rollbacks untestable.
  • SQLite testing caveat: DBAL emulates ALTER TABLE by recreating the table on older SQLite versions. Structural assertions pass, data-preservation assertions may not — test those against a real MySQL/Postgres container in CI.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource