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.
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:
ezcDbSchema — the static facade. This is 90% of the API surface you will touch.ezcDbSchemaHandlerManager — lists which formats and DB backends are actually wired up in your install.ezcDbSchemaTable / ezcDbSchemaField / ezcDbSchemaIndex / ezcDbSchemaForeignKey — the in-memory schema model.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.
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.
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.
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.
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.
Console/Kernel; keep zetacomponents/database-schema in require-dev.ezcDbHandler from the Laravel container — there is no integration. Construct one via ezcDbFactory::create($dsn) from config('database.connections.*').php/array formats so tests need no database. A DB-backed reader test is a flaky test.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.
How can I help you explore Laravel packages today?