Smart database schema management for PHP. A modern schema-first migration engine for PHP.
Schemage lets you define your database structure using a clean PHP DSL, compare it against the current database schema, generate SQL automatically, and apply changes through a lightweight CLI.
- Schema-first database definition
- Fluent PHP DSL
- MySQL/MariaDB support
- Automatic schema introspection
- Table and column diffing
- Partial index diffing
- SQL generation
- Migration execution
- Migration history
- Model generation
- Dry-run mode
- Destructive operation protection
- Database reset support
- Snapshot generation
- Foreign key support
- Debug trace mode
composer require eril/schemageGenerate the default configuration file:
php bin/schemage --initThis creates:
Schemage.php
database/schema.php
database/bootstrap.sql
database/snapshots/Generated config example:
<?php
return [
// Schema file path
'schema' => 'database/schema.php',
'bootstrap' => 'bootstrap.php',
// Database connection configuration
'database' => [
'driver' => 'mysql',
'host' => $_ENV['DB_HOST'] ?? '127.0.0.1',
'port' => $_ENV['DB_PORT'] ?? 3306,
'database' => $_ENV['DB_NAME'] ?? 'app',
'username' => $_ENV['DB_USER'] ?? 'root',
'password' => $_ENV['DB_PASS'] ?? '',
],
// Model generator setup
'generator' => [
'models' => [
'enabled' => false,
'namespace' => 'App\\Models',
'path' => '/app/Models',
'extends' => null,
],
],
];Optional bootstrap file:
bootstrap.phpUseful for:
- loading
.env - bootstrapping frameworks
- loading helpers
- custom runtime setup
Create a schema file:
<?php
use Schemage\DSL\Schema;
use Schemage\DSL\Table;
return function (Schema $schema) {
$schema->table('users', function (Table $t) {
$t->id();
$t->string('name');
$t->string('email')
->unique();
$t->timestamps();
});
};Run a dry-run first:
php bin/schemage --dry-runApply the migration:
php bin/schemage --yes$schema->table('users', function ($t) {
$t->id();
$t->string('name');
$t->string('email')
->unique();
$t->int('age')
->default(0);
$t->timestamps();
});$t->id();
$t->string('name');
$t->char('code', 2);
$t->text('body');
$t->longText('content');
$t->int('age');
$t->bigInt('views');
$t->float('rating');
$t->double('score');
$t->decimal('price', 10, 2);
$t->boolean('active');
$t->datetime('published_at');
$t->timestamp('created_at');
$t->json('meta');
$t->uuid('uuid');$t->string('email')
->unique();
$t->string('slug')
->index();
$t->string('name')
->nullable();
$t->int('age')
->default(0);
$t->timestamp('created_at')
->defaultCurrentTimestamp();
$t->timestamp('updated_at')
->defaultRaw('CURRENT_TIMESTAMP');$t->id();
$t->timestamps();
$t->createdAt();
$t->updatedAt();
$t->softDeletes();
$t->rememberToken();Single-column indexes:
$t->string('email')
->unique();
$t->string('slug')
->index();Composite indexes:
$t->unique(['email', 'tenant_id']);
$t->index(['first_name', 'last_name']);Indexes are automatically introspected and partially diffed.
Current V1 behavior:
- missing indexes are automatically added
- removed indexes are automatically dropped
- changed indexes are ignored intentionally with warnings
Example warning:
Index 'email_unique' on table 'users' differs from desired schema and was ignored.Simple inferred relation:
$t->foreign('user_id');Explicit references:
$t->foreign('user_id')
->constrained() // or references('users')
->cascadeOnDelete();Explicit references:
$t->foreign('user_id')
->constrained() // or references('users')
->cascadeOnDelete();ou:
$t->foreign('author_id')
->references('users');Custom referenced column:
$t->uuid('author_id')
->references('users', 'uuid');Available relation actions:
->cascadeOnDelete()
->cascadeOnUpdate()
->restrictOnDelete()
->restrictOnUpdate()
->nullOnDelete()
->nullOnUpdate()In V1, foreign keys are generated when creating new tables only. Foreign key changes after table creation are not automatically migrated yet.
Generate models from your schema:
php bin/schemage --generate-modelsExample generated model:
<?php
namespace App\Models;
class User
{
protected string $table = 'users';
}Generator configuration:
'generator' => [
'models' => [
'enabled' => true,
'namespace' => 'App\\Models',
'path' => '/app/Models',
'extends' => 'App\\Core\\Model',
],
],| Command | Description |
|---|---|
--init |
Initialize a new project |
--yes |
Don't ask for confirmation |
--dry-run |
Preview pending changes |
--force |
Execute dangerous SQL like delete |
--status |
Show migration history |
--sql |
Export SQL for pending schema changes |
--schema-sql |
Export SQL for the entire schema |
--rollback |
Rollback the latest batch |
--fresh |
Drop all tables |
--clear-history |
Clear histries |
--generate-models |
Generate model classes |
Dry-run:
php bin/schemage --dry-runApply migrations without confirmation:
php bin/schemage --yesAllow destructive operations:
php bin/schemage --forceShow migration history:
php bin/schemage --statusReset database:
php bin/schemage --fresh --force --yesReset database and clear history:
php bin/schemage --fresh --force --yes --clear-historyGenerate models:
php bin/schemage --generate-modelsShow stack trace on exceptions:
php bin/schemage --traceExecuted operations are stored in:
schema_migrationsThis table is managed internally by the engine and ignored during schema diffing.
Schemage can generate SQL without executing migrations.
Generate the SQL required to migrate the current database schema:
php bin/schemage --sqlExample:
ALTER TABLE users
ADD COLUMN phone VARCHAR(20);
CREATE TABLE posts (...);This command uses schema introspection and the diff engine.
Generate SQL for the entire schema definition:
php bin/schemage --schema-sqlExample:
CREATE TABLE users (...);
CREATE TABLE posts (...);This command ignores the current database and generates SQL directly from database/schema.php.
bin/schemage --schema-sql > install.sqlor
bin/schemage --sql > migration.sqlnotice not to use php because > stdout is not a tty
This is useful for:
- Manual deployments
- Database reviews
- Backup scripts
- Installation packages
- CI/CD pipelines
By default, destructive operations are blocked.
This includes:
- dropping tables
- dropping columns
- dropping indexes
To allow destructive operations:
php bin/schemage --forceEnable stack traces during execution:
php bin/schemage --traceExample output:
[ERROR] Table users already exists
File: .../Migrator.php:62
Stack trace:
#0 .../Migrator.php:62 run()
#1 .../MigrateCommand.php:88 printPlannedSql()
#2 .../bin/schemage:24 run()Schemage is intentionally conservative.
Current V1 limitations:
- MySQL/MariaDB and SQLite only
- Index diffing is partial and intentionally conservative
- Existing indexes are not automatically modified
- Foreign key changes after table creation are not automatically migrated
- Rename detection is manual
- No PostgreSQL grammar yet
- No ORM/query builder layer yet
Schemage follows a schema-first approach.
The schema file is the source of truth.
Schema DSL
↓
Schema Metadata
↓
Database Introspection
↓
Diff Engine
↓
SQL Generation
↓
Migration ExecutionThe DSL is designed to stay expressive, while the internal architecture keeps metadata, diffing, SQL generation, and execution separated.
MIT