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

Database Schema Laravel Package

zetacomponents/database-schema

Zeta Components Database Schema provides a database-independent API for defining and manipulating table schemas in PHP, letting you create, alter, and inspect tables portably across supported database backends without writing vendor-specific SQL.

View on GitHub
Deep Wiki
Context7

Getting Started

First, calibrate expectations. zetacomponents/database-schema is a subtree split of the legacy Zeta Components (formerly eZ Components) DatabaseSchema component. It is not a Laravel package: no service provider, no facade, no config file, no migration integration, no tests against modern PHP, 4 stars, 0 dependents, last meaningful activity well over a decade ago. It is a standalone schema introspection/diffing library that predates Composer by a decade and was retrofitted into it.

Day to day, you will not "use" this package — you will reach for it once or twice as a one-off schema archaeology tool on a legacy database, then go back to Laravel's own migration tooling.

Install (in a scratch project or as a throwaway dev dependency):

composer require --dev zetacomponents/database-schema
composer dump-autoload -o   # classmap autoloading; -o avoids missed classes

The classes are ezc-prefixed (ezcDbSchema, ezcDbSchemaTable, …) and live in a handful of multi-class files, so Composer's classmap is what resolves them. If class_exists('ezcDbSchema') returns false after install, run composer dump-autoload -o — a stale non-optimized classmap is the single most common first-run failure.

Where to look first, in order:

  1. ezcDbSchema — the static facade. This is 90% of the API surface you will touch.
  2. ezcDbSchemaHandlerManager — lists which formats and DB backends are actually wired up in your install.
  3. ezcDbSchemaTable / ezcDbSchemaField / ezcDbSchemaIndex / ezcDbSchemaForeignKey — the in-memory schema model.
  4. ezcDbSchemaComparator + ezcDbSchemaDiff — the only genuinely differentiated feature.

First use case — snapshot a legacy database to a portable file:

<?php
require __DIR__ . '/vendor/autoload.php';

$db = ezcDbFactory::create('mysql://user:[email protected]/legacy_app');
$schema = ezcDbSchema::createFromDb($db);

// Dump to a format that is diffable in git
$schema->writeToFile('xml', __DIR__ . '/schema/legacy-2024-06-01.xml');

foreach ($schema->getTables() as $name => $table) {
    fwrite(STDERR, $name . ' (' . count($table->fields) . " columns)\n");
}

That is the whole happy path: create a DB handler, read the schema, write it somewhere. Everything else in the package is variations on that.


Implementation Patterns

Pattern 1: Snapshot-and-diff as a console command

The only workflow that justifies the dependency in a Laravel codebase is "compare a live schema against a committed reference file." Wrap it in a console command and never let it near a web request.

// app/Console/Commands/SchemaDiff.php
class SchemaDiff extends Command
{
    protected $signature = 'legacy:schema-diff {reference : path to schema xml}';
    protected $description = 'Diff the live DB against a committed schema snapshot';

    public function handle(): int
    {
        $dsn = sprintf(
            'mysql://%s:%s@%s:%s/%s',
            config('database.connections.mysql.username'),
            config('database.connections.mysql.password'),
            config('database.connections.mysql.host'),
            config('database.connections.mysql.port'),
            config('database.connections.mysql.database'),
        );

        $current = ezcDbSchema::createFromDb(ezcDbFactory::create($dsn));
        $target  = ezcDbSchema::createFromFile('xml', $this->argument('reference'));

        $diff = ezcDbSchemaComparator::compareSchemas($current, $target);

        // Serialize the diff for human review instead of applying it
        $diff->writeToFile('xml', storage_path('app/schema-diff.xml'));

        $this->info('Diff written to storage/app/schema-diff.xml');
        return self::SUCCESS;
    }
}

Note the deliberate choice: always write the diff out and read it, never call applyToDb() in a script you might run twice.

Pattern 2: Format conversion as the interchange layer

The package's formats are xml, php (a PHP array file), array (returns a plain array), and the DB backends. The php/array formats are the only ones usable without a live connection, which makes them the right choice for CI.

// DB -> php array file (committed to git, no DB needed to read it back)
$schema = ezcDbSchema::createFromDb($db);
$schema->writeToFile('php', database_path('schema/reference.php'));

// php array file -> XML for a human-readable review artifact
$schema = ezcDbSchema::createFromFile('php', database_path('schema/reference.php'));
$schema->writeToFile('xml', storage_path('app/reference.xml'));

Use this to make "the schema" an artifact in version control rather than a database state, then diff it with plain git diff. That is often more useful than the package's own diff engine, because git diff never silently drops a column.

Pattern 3: Build a schema programmatically

When you need to emit DDL for a database Laravel's grammar can't express (or for a fixture DB), construct the model by hand:

$schema = ezcDbSchema::createNew();

$table = new ezcDbSchemaTable(
    ['id'    => ezcDbSchema::createNewField('integer', 0, false, null, true)],
    ['primary' => new ezcDbSchemaIndex(['id' => new ezcDbSchemaIndexField()], true)]
);
$schema->addTable('audit_log', $table);

$schema->writeToDb($db);

This is verbose and the constructor signatures are easy to get wrong — read ezcDbSchemaTable::__construct in src/ before relying on it.

Pattern 4: Reading the model directly

For anything non-trivial, skip the diff engine and walk the model yourself. ezcDbSchemaTable exposes ->fields, ->indexes, ->foreignKeys as public arrays; ezcDbSchemaField has ->type, ->length, ->notNull, ->default, ->autoIncrement. Iterating these and doing your own comparison gives you full control over which differences matter and lets you normalize noise (index name casing, default literal formatting) before comparing.

Integration tips

  • Register the command only in Console/Kernel; keep zetacomponents/database-schema in require-dev.
  • Never resolve an ezcDbHandler from the Laravel container — there is no integration. Construct one via ezcDbFactory::create($dsn) from config('database.connections.*').
  • If you need this inside a test suite, use the php/array formats so tests need no database. A DB-backed reader test is a flaky test.
  • If you find yourself wanting to apply the diff in a deploy pipeline, stop and write real Laravel migrations instead. This package has no ordering guarantees, no rollback story, and no data migration concept.

Gotchas and Tips

It is effectively abandoned. There is no maintainer, no issue triage, and no PHP 8 CI. The code predates modern PHP idioms and will likely emit deprecation notices or hard errors on PHP 8.1+ (implicit nullable parameters, deprecated dynamic properties, strftime-era code). Pin PHP 7.4 in a dedicated container, or vendor-patch. Do not add it to a project you expect to upgrade PHP on.

License is NOASSERTION on GitHub. Zeta Components shipped under Apache-2.0 — permissive and commercially safe — but GitHub's detector failed to classify it. Open the LICENSE/NOTICE files in the repo and confirm before shipping. Apache-2.0 also carries attribution and NOTICE-file obligations; if you vendor any of the source rather than Composer-installing it, keep the notice. This is a legal-review flag, not a blocker, but it is a real step.

Round-tripping is lossy. The type system normalizes backend-specific types into a small generic set (integer, float, decimal, text, clob, blob, boolean, date, datetime, time). MySQL unsigned, ENUM, SET, tinyint(1), collation, charset, ON UPDATE CURRENT_TIMESTAMP, generated columns, and expression indexes are frequently dropped or flattened. Reading a MySQL schema and writing it back to MySQL does not reproduce the original DDL. Treat every snapshot as a summary, never as a definition of record.

The diff is not a migration. ezcDbSchemaComparator::compareSchemas() returns an ezcDbSchemaDiff; applying it to a DB will drop tables and columns with no confirmation and no data preservation. It also doesn't model the order in which foreign keys must be dropped and recreated. Run it against a throwaway database first, always.

Handler availability varies. Call ezcDbSchemaHandlerManager::getSupportedFormats() (and the equivalent reader/writer listing) at runtime rather than assuming a format exists. Some backends have only a reader or only a writer registered in a given install.

Autoloading is the most common support question. The split repos use classmap autoloading over multi-class files. Symptoms of a stale classmap: Class 'ezcDbSchema' not found on a fresh composer install. Fix: composer dump-autoload -o. If you're on an opcache'd production box, also restart PHP-FPM.

DSN parsing quirks. ezcDbFactory expects a URL-style DSN. Passwords containing @, :, or / break it — urlencode() them, or construct the handler with the array-style config form if your version supports it. Laravel's config values are not pre-encoded.

No connection pooling or lazy connections. ezcDbFactory::create() opens a connection immediately. In a long-running worker you'll accumulate connections; create one handler, reuse it, and unset() it when done.

Extension points, if you must fork. The reader/writer handlers implement a small interface (loadFromFile, loadFromString, saveToFile, saveToString) and register through ezcDbSchemaHandlerManager. Adding a custom format (say, JSON) is genuinely small — implement the interface, register it, done. This is the one place where the old design holds up. Before forking anything, check whether you need the package at all: laravel-migrations-generator, doctrine/dbal, and php artisan schema:dump cover most of what this was used for, are maintained, and integrate with the framework.

Bottom line for a Laravel developer: this is a forensic tool, not a dependency. Budget an hour to snapshot a legacy schema and generate a reference file, commit that file, then uninstall the package and move on.

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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings