Migrations
stellarwp/foundation-migrations manages the history of your application’s database schema. Each migration declares one change in up() and its inverse in down(). Foundation loads migration files, runs pending changes in order under a database lock, and records successful migrations.
Unlike WordPress’s dbDelta(), which does not manage foreign-key constraints, Foundation supports creating, changing, and removing relationships between tables. See foreign keys for the complete workflow.
Create and change a table
Section titled “Create and change a table”Set up migrations
Section titled “Set up migrations”Install the migrations runtime and development CLI. The runtime includes Foundation Database and the WP-CLI integration:
Add your project root to the existing root config.php. The Foundation CLI reads this file, and your application bootstrap supplies the same configuration to its container:
After creating the application container with that configuration, register providers in dependency order in src/App.php:
DatabaseProvider supplies the shared connection. MigrationsProvider configures discovery, history, and migration execution; WPCliProvider enables the wp <prefix> migrate:* commands. Applications that invoke Migrator programmatically can omit WPCliProvider. Register application providers afterward, before resolving migration services.
The default migration directory is db/migrations. Each PHP file returns an anonymous migration object. Migration files need no namespace or Composer autoload mapping; these examples use Plugin\\ mapped to src/ for the application table class. Keep migrations in the production archive even though the generator is a development dependency.
Generate a new table
Section titled “Generate a new table”Create the table class and its first migration together:
The command writes two files:
Your timestamp will differ. Reports_Table supplies the stable table name for application queries; Foundation adds the current WordPress site prefix. The migration supplies the schema. Generation writes PHP files; applying the migration creates the database table.
Define the initial schema
Section titled “Define the initial schema”The generated up() already declares an auto-incrementing id. Add the columns your application needs. For this example, the completed file is db/migrations/20260923000001_create_reports_table.php:
The filename supplies the persistent ID 20260923000001_create_reports_table. The migration owns its historical table name and schema. Foundation validates that unprefixed name and adds the active WordPress site prefix when planning the change. Refactoring or removing the application table class does not change this history.
Preview and apply
Section titled “Preview and apply”Review the generated SQL, apply the pending migration, then check its recorded status:
The first run creates the migration ledger automatically. your-plugin is the configured WP-CLI command prefix.
Foundation loads new migration files automatically from the configured directory. You do not add each migration to a provider list.
Add a column later
Section titled “Add a column later”Generate a new migration for the existing table:
This creates a timestamped file returning an anonymous migration. It selects the table in up() and leaves a placeholder for your changes. The argument names the migration; it does not generate column definitions.
Complete db/migrations/20260924000001_add_published_at.php like this:
The generated down() initially throws IrreversibleMigration. Replace it with the inverse above only if deleting publication times is acceptable; remove its unused exception import and annotation. Otherwise, keep the exception. Forward migration still works, but rollback stops at this migration.
Leave the original migration and Reports_Table unchanged. Preview and apply the new migration with the same commands:
Existing reports have NULL in published_at until your application writes a value.
Roll back a change
Section titled “Roll back a change”To reverse the highest applied migration, inspect its down() and run:
In this example that removes published_at while retaining the reports table. Running migrate:run afterward reapplies the column; values deleted by rollback do not return. Reversing the initial migration drops the complete reports table. Migration commands cover targets, multiple reversals, reset, and refresh.
Run and reverse migrations
Section titled “Run and reverse migrations”The examples use the your-plugin command prefix configured during setup. A complete WordPress application using Foundation’s default prefix runs the same commands as wp nx migrate:*.
Check migration status
Section titled “Check migration status”See which migrations are applied, pending, or recorded in history with a missing declaration:
Status includes each migration’s ID, application timestamp, and optional description. A missing applied file does not block new forward migrations, but it must be restored before reversing that migration.
Preview migration SQL
Section titled “Preview migration SQL”Review the SQL for pending migrations before applying them:
Preview does not change the schema or execute data callbacks. It reports when a data callback will run. The database can change between preview and execution, so review failures even after a successful preview.
Run pending migrations
Section titled “Run pending migrations”Apply all pending migrations in ascending ID order:
Foundation holds the application’s migration lock and records each migration after its schema and data work succeeds. The first run creates the history table. If no migrations are pending, the command completes without changing the schema.
Inspect a command failure
Section titled “Inspect a command failure”Migration commands display exceptions as Error: <message> and exit with status 1. Add --debug when running a command to include the original exception chain and stack trace:
Execution stops at the failed migration. Schema changes may already have committed, so inspect and repair the failure before running it again. --debug changes diagnostic output; the command still executes its requested operation.
Migrate to a specific version
Section titled “Migrate to a specific version”Bring the schema to a selected migration by reversing applied IDs above it, then applying pending IDs through it. The target is the migration’s filename without .php.
A target below the current version can delete data through down(). Preview the target first:
After reviewing the planned changes, apply the target:
IDs determine order across all migration directories. Choose a target that respects your migration dependencies, including older IDs contributed by newly enabled packages. Omit --to, or use --to=latest, to apply every pending migration.
Roll back migrations
Section titled “Roll back migrations”Reverse the highest applied migration ID by running its down() method:
To reverse several applied migrations, specify a count:
To reverse all applied IDs above a specific target, use --to. This leaves the target in place if already applied and never applies pending migrations:
Choose either --step or --to. Rollback counts migration IDs, not deployment batches. Foundation checks that the declarations selected for reversal exist; unrelated missing files do not block the operation.
Reset all migrations
Section titled “Reset all migrations”Reverse every applied migration and leave the migrations pending:
Reset runs down() in descending ID order and removes each successfully reversed entry from history. It does not rerun migrations. All applied declarations must be available, and an irreversible or failed inverse stops the reset.
The command asks for confirmation because the inverses can delete application data. For an already reviewed operation, skip the prompt with --yes:
migrate:rollback --to=0 provides the same reversal. To preview all inverses without executing them or prompting:
Roll back and rerun migrations
Section titled “Roll back and rerun migrations”Rebuild the schema by reversing all applied migrations, then running all migrations again:
Refresh holds one migration lock across reversal and reapplication. Like reset, it requires every applied declaration and working down() methods. It asks for confirmation because reversing migrations can delete data; use --yes after reviewing the operation:
Write schema changes
Section titled “Write schema changes”up() and down() declare schema changes. Foundation evaluates the selected declaration during planning, including previews. Keep queries and application work in the separate data callback.
Choose column types
Section titled “Choose column types”Use the same column methods when creating a table or adding columns to an existing one:
| Method | Storage and typical use |
|---|---|
bigIncrements() |
Unsigned auto-incrementing BIGINT primary key, named id by default |
string( 'name', 191 ) |
Variable-length text, with a default limit of 191 characters |
char( 'currency', 3 ) |
Fixed-length character storage; specify the length explicitly |
text( 'summary' ) |
Text up to 65,535 bytes |
mediumText( 'content' ) |
Text up to 16,777,215 bytes |
longText( 'document' ) |
LONGTEXT storage for larger documents |
smallInteger( 'attempts' ) |
SMALLINT; use unsigned() for nonnegative values |
integer( 'quantity' ), bigInteger( 'external_id' ) |
INT and BIGINT; unsigned convenience methods are also available |
boolean( 'enabled' ) |
Boolean storage |
decimal( 'amount', 12, 4 ) |
Exact numeric storage with explicit precision and scale |
json( 'metadata' ) |
JSON document storage with database validation |
date( 'starts_on' ) |
Calendar date, such as 2026-09-24 |
time( 'opens_at' ) |
Time with whole-second precision, such as 09:30:00 |
dateTime( 'created_at', 6 ) |
DATETIME with optional fractional-second precision from 0 to 6 |
binary( 'token', 16 ) |
Fixed-length BINARY storage, padded with zero bytes |
varBinary( 'payload', 255 ) |
Variable-length VARBINARY storage with a maximum byte length |
Columns are non-nullable unless you call nullable(). Existing modifiers such as default(), comment(), and change() apply to the new types too. A char() length is a storage choice, not application validation that a supplied code has exactly that many characters. useCurrent() and useCurrentOnUpdate() are intended for dateTime() columns.
MySQL stores JSON natively; MariaDB uses text with JSON validation. Foundation preserves that distinction through alterations and renames. For compatibility with MySQL 5.7, supply a JSON value on insertion or make the column nullable instead of declaring a non-null JSON default.
When writing PHP arrays through a Foundation table or fluent query, encode them with json_encode( $value, JSON_THROW_ON_ERROR ). Alternatively, use the shared Doctrine connection and bind the PHP array as Doctrine\DBAL\Types\Types::JSON so Doctrine encodes it. Query results are not automatically decoded; see the JSON write examples.
Store binary values
Section titled “Store binary values”Use binary() for fixed-width values such as a UUID encoded into 16 bytes. Use varBinary() when values have different byte lengths:
Lengths are measured in bytes. BINARY pads shorter values with zero bytes (0x00), which remain present when read back. It does not validate that the application supplied exactly the declared number of bytes. VARBINARY stores values without padding. See MySQL’s binary type documentation.
Use varBinary() when declaring an existing VARBINARY column, including in a reversal that restores that type. To intentionally convert an existing column, add a migration with the desired declaration and change(). Converting to BINARY can add zero bytes that remain in the data when converted back to VARBINARY, so account for padding in the migration’s data handling and reversal.
Change a column
Section titled “Change a column”Declare the complete replacement definition and call change():
Restate any default, nullability, unsigned flag, comment, or other attribute to retain. For this example, a safe inverse can restore the original length only after accounting for values longer than that length.
Rename a column
Section titled “Rename a column”Generate an alteration for the table whose column is changing:
Replace the generated methods with the rename and its inverse:
Remove the generated IrreversibleMigration import and annotation when supplying this inverse. renameColumn() retains the column’s values and complete definition, including its default, nullability, and comment. Indexes and foreign keys continue to refer to the renamed column. Use a distinct destination name that does not already exist. Column renames run before other alterations on that table; use the new name in a change() declaration. When creating a new table, declare columns with their final names.
Renames run in declaration order. You can reuse an old column name after renaming it, including swapping names through a temporary name. If execution stops partway through, inspect which renames completed and repair the partial change before retrying.
Column renames work across the supported database versions. Foundation uses native RENAME COLUMN when available and otherwise uses CHANGE with the existing column definition. Your migration declaration stays the same.
Preview and apply the migration:
Update application queries to use headline when deploying this schema change. Leave the earlier migration that created title unchanged. Running migrate:rollback reverses the rename through down(), preserving the values under title again.
Rename a table
Section titled “Rename a table”Generate a migration without --create or --table, then fill in its methods:
Remove the generated irreversible exception import and annotation. Both arguments are unprefixed names; Foundation applies the current WordPress site prefix. The destination must be unused. The rename preserves existing rows, indexes, and foreign-key relationships.
Preview and apply with wp your-plugin migrate:run --dry-run and wp your-plugin migrate:run. Update the application table class’s unprefixedName() to return 'your_plugin_articles', and use that name in new migrations. Keep all earlier migration filenames and historical table names unchanged. Coordinate these application changes with migration execution; application queries must use the name currently present in the database.
If you recreate the old table while the renamed table still exists, choose different logical foreign-key names for the new table. For example, use current_order on the new table when the archived table retains order. Constraint names must be unique across the database on supported MySQL and MariaDB versions.
Existing foreign keys retain their physical constraint names after a table rename. Continue using their original logical names with dropForeignKey() on the renamed table.
Remove a column
Section titled “Remove a column”Removing a column deletes its values. Adding it again in down() restores its structure, not the deleted data. Keep IrreversibleMigration when no acceptable inverse exists.
Add or replace an index
Section titled “Add or replace an index”Use dropIndex() for removal. To replace an index under the same name, declare both the removal and its replacement:
Relate tables with foreign keys
Section titled “Relate tables with foreign keys”Foreign keys let the database enforce relationships between your application’s tables.
For example, an order item must belong to an existing order. Create the parent table before its dependent table:
The first migration’s generated bigIncrements( 'id' ) supplies the order’s primary key. In the second migration, complete up() with the item columns, supporting index, and relationship:
Keep the generated down() that drops your_plugin_order_items. The later migration ID ensures rollback drops items before orders. When adding a relationship to existing tables, use --table=your_plugin_order_items and $schema->table() instead.
order is the relationship’s stable logical name within the items table. Foundation scopes the physical constraint name to that table, so another table can also use order. Pass the same logical name to dropForeignKey() later. references() takes the historical, unprefixed application table name; Foundation resolves both tables for the current WordPress site.
Generated constraint names contain separate table and logical identity components. This lets dropForeignKey('order') locate the same relationship after a table rename without loading earlier migrations. An existing logical name must be dropped explicitly before adding its replacement.
If you change WordPress’s table prefix, rename the application’s tables and migration ledger together before running migrations again. Keep historical migration declarations unchanged. Preserve existing constraint names: their logical identity component lets later declarations locate the relationship on its current table. Externally renamed constraints require explicit repair before a declaration can address them by their original logical name.
Declare columns and indexes explicitly. The local and referenced column types must match, including integer size and unsignedness. Reference a primary or unique key. Foundation-created tables use InnoDB, which enforces these constraints.
Preview and apply as usual:
The database now rejects an item whose order_id has no matching order. Deleting an order also deletes its items because this example chooses cascadeOnDelete().
Choose deletion and update behavior
Section titled “Choose deletion and update behavior”Without an action method, both deletion and referenced-key updates are restrictive: the database rejects the parent change while child rows reference it.
| Delete action | Referenced-key update action | Effect on child rows |
|---|---|---|
restrictOnDelete() |
restrictOnUpdate() |
Reject the parent change while references exist |
cascadeOnDelete() |
cascadeOnUpdate() |
Delete dependent rows, or propagate the new key |
nullOnDelete() |
nullOnUpdate() |
Set the referencing columns to NULL |
For either null action, declare every local column as nullable():
Changing an existing relationship requires a new migration. Drop and redeclare the same logical name with its complete replacement definition:
Its down() can drop and redeclare the relationship with the original action. To remove a relationship entirely, use only dropForeignKey( 'order' ). This retains its columns and indexes; remove those separately when that migration owns their removal. Remove dependent relationships or child tables before dropping a referenced parent table. Table operations execute in declaration order, including within a single migration. Create referenced parent tables before their children, and reverse that order when removing them.
Reference a composite key
Section titled “Reference a composite key”Supply columns in corresponding order on both sides. For example, given an orders table with a unique key over account_id and order_number, declare matching item columns and their index:
The earlier parent migration must declare those column types and $table->unique( 'account_order', 'account_id', 'order_number' ). Each local value pairs with the referenced column in the same position.
Choose a scaffold
Section titled “Choose a scaffold”Use --create=your_plugin_reports to generate an initial migration for that unprefixed table name. Its up() declares the complete initial table, and its down() drops that table. Use --table=your_plugin_reports for a later alteration. The options are mutually exclusive.
Omitting both produces a generic migration with an empty up() and an irreversible down(). Declare the required schema changes using stable, unprefixed table names. Migration names alone never select table-dropping behavior.
Column declarations
Section titled “Column declarations”| Declaration | Meaning |
|---|---|
bigIncrements('id') |
Unsigned auto-incrementing BIGINT primary key |
string('name', 191) |
VARCHAR with an explicit maximum length |
text('body'), longText('body') |
TEXT or LONGTEXT |
integer('count'), bigInteger('count') |
Integer columns |
unsignedInteger('count'), unsignedBigInteger('count') |
Unsigned integers |
boolean('active') |
Boolean storage |
decimal('amount', 12, 4) |
Exact decimal precision and scale |
binary('token', 16) |
Fixed-length BINARY, padded with zero bytes |
varBinary('payload', 255) |
VARBINARY with an explicit maximum byte length |
dateTime('updated_at', 6) |
DATETIME with fractional precision from 0 to 6 |
Columns support nullable(), notNull(), unsigned(), default(), comment(), and change(). Date/time declarations additionally support useCurrent() and useCurrentOnUpdate(). Use strings for exact decimal defaults. Table declarations support primary(...$columns) and comment().
up() and down() must be pure schema declarations. Foundation evaluates the selected declaration during planning and again if you later execute a previewed migration. Previously applied declarations are not replayed. Do not query the live database, perform application work, or put existence guards in them.
Transform existing data
Section titled “Transform existing data”An anonymous migration can implement MigratesData. Foundation supplies a DataMigrationContext to its data callback. Use quotedTable() to resolve a historical unprefixed name for the active site, and $context->db to run native Doctrine queries:
The context uses the same connection and naming policy as application tables. For operations that need an unquoted physical name, use $context->names->tableName( 'your_plugin_reports' ). Foundation supplies the context for each callback; consumers do not construct or retain it.
This example inherits the default irreversible down(): replacing data has no automatic safe inverse.
Foundation runs this callback after schema changes and before writing history. It must be safe to repeat if the process stops or recording fails. You may use the shared connection’s transactional() for a bounded data operation. Complete that transaction before returning; an open transaction interrupts the migration and is rolled back. DDL remains outside that transaction. Preview reports that a data callback exists but does not execute it. The data contract has no automatic reverse callback: supply schema rollback only when reversing remains safe for the resulting data, or throw IrreversibleMigration.
Deploy and recover
Section titled “Deploy and recover”Run migrations from application code
Section titled “Run migrations from application code”Inject StellarWP\Foundation\Migrations\Migrator into the service that owns installation or upgrades. Once WordPress and the application’s providers are ready, run pending migrations:
Record the installed application version only after this succeeds. Run the same upgrade workflow for each affected WordPress site, switching sites only between complete migration operations.
Use status() to inspect history and preview($target) to inspect planned steps. migrate($target) selects a target, rollback($steps) reverses a count of applied IDs, and rollbackTo($target) reverses only IDs above a target. To reset all applied migrations programmatically:
Use refresh() to reverse and reapply all migrations. Programmatic calls do not prompt for confirmation; the application owns that decision. Preview and execution operations return step objects with the ID, direction, SQL, and whether a forward data callback was involved.
Adopt an existing installation
Section titled “Adopt an existing installation”For a fresh installation, run wp your-plugin migrate:run to create the schema and perform any data transformations. When adopting an existing installation, inspect its schema and data against the migration declarations. Mark each migration whose complete effects are already present:
Marking changes history under the same database advisory lock used for migration execution. It may create the ledger, but does not invoke up(), down(), or data callbacks, plan schema changes, or verify the live database. You own verification of the recorded effects. Each marking command prompts for confirmation; use --yes for an already reviewed operation.
Mark migrations through an existing version
Section titled “Mark migrations through an existing version”When an installation already contains the work of the older migrations but still needs newer ones, mark through the last migration whose complete schema and data effects are present:
The command lists registered migrations through that ID, inclusively, before asking for confirmation. It records pending migrations in ascending ID order and preserves existing history timestamps, including records above the target. Later pending migrations remain available for migrate:run.
The target must exactly match a registered migration ID. A history record whose declaration is missing cannot be used as the target. Choose exactly one selection: a positional ID, --to=<id>, or --all.
Check the result and preview the remaining work before running newer migrations:
Mark all pending migrations as applied
Section titled “Mark all pending migrations as applied”When adopting a complete existing schema, verify that every currently pending migration’s schema and data effects are fulfilled, then record them together:
Check the recorded history and preview the next run before enabling automatic upgrades:
Later migrations continue through the normal migrate:run workflow.
Programmatic callers inject Migrator and use markApplied(string $id): void, markAppliedThrough(string $target): void, markAllApplied(): void, or markPending(string $id): void. The single-ID and through-target methods require a registered ID. Repeating a marking operation preserves existing entries and timestamps. The through-target and all-pending methods record their selected pending IDs atomically in one transaction under the migration lock. A marked migration participates in normal rollback: reversing it invokes its down() declaration, with the same potential for data loss as an executed migration.
Interrupted schema changes
Section titled “Interrupted schema changes”MySQL and MariaDB DDL can commit before a later statement, data callback, or history write fails. Foundation stops on failure and leaves that migration unrecorded. Earlier migrations that completed remain recorded, and earlier statements within the failed migration may already have changed the database.
Foundation plans the current migration against the affected live tables. Alterations preserve unrelated columns, indexes, and relationships; the runner does not replay historical declarations or check unrelated tables against old definitions. Adding an existing object or removing a missing one fails rather than automatically completing an interrupted migration.
LedgerFailure means migration work completed but its history update failed. Fixing the storage error alone does not reconcile the ledger; verify the completed schema and data effects before repairing history. A failed rollback likewise leaves its history entry in place even if some inverse statements committed.
Repair inconsistent history
Section titled “Repair inconsistent history”If the ledger itself is wrong, pause all application upgrade triggers and migration workers for the affected site and take a backup. Restore any missing declarations needed to understand or reverse the affected work, then compare the ledger’s exact IDs with the live schema and each migration’s schema and data effects.
After a manual schema change or a failed history write, verify or finish the migration’s complete schema and data work before recording it:
If its complete inverse has already been performed, verify that recorded dependent migrations remain valid, then remove the history entry:
markPending() accepts a valid migration ID even when its declaration is missing and does nothing if the history entry is absent. A registered migration becomes pending and can run again. A missing migration disappears from status when its history is removed; restore its file before it can run again.
After correcting history, inspect status and preview the next run before resuming upgrades. Keep an operational record of the repair.
Concurrent upgrades
Section titled “Concurrent upgrades”One database advisory lock covers planning, schema execution, data callbacks, and history writes. The lock survives DDL commits and lasts until release or session termination. It has no lease TTL to configure. MigrationAlreadyRunning means another session is migrating this application’s site ledger: defer and retry after it finishes. This is distinct from a database failure.
MigrationInterrupted reports a declaration that cannot be applied to the current schema, an unavailable migration required for rollback, or a lost operation scope/session. Inspect its message and underlying exception before deciding how to repair the failure. The database layer reports AdvisoryLockInterrupted during a data callback; the migrator translates it to MigrationInterrupted when it escapes the run. Catching it inside a callback does not make the run successful. Foundation stops and releases only the original session’s lock where possible. Never change sites or sessions during a migration, even temporarily. Foundation migration exceptions live under StellarWP\Foundation\Migrations\Exceptions and extend MigrationException, which extends StellarWP\Foundation\Database\Exceptions\DatabaseException. Catch MigrationException for shared reporting, and use the specific exception when choosing whether to defer, retry, or stop. Native SQL failures still use Doctrine exceptions; application data callbacks can propagate their own exceptions.
All migration participants must reach the same primary database server. Advisory locks are local to that server; a proxy that moves statements between sessions or servers cannot provide this guarantee. Preview and status are observations and can become stale before a later run.
Configure discovery
Section titled “Configure discovery”Name the migration ledger
Section titled “Name the migration ledger”With foundation.prefix set to your-plugin, the ledger defaults to your_plugin_foundation_migrations before WordPress adds its site prefix. The advisory lock is scoped by database and ledger name, so applications using different ledgers migrate independently. Keep that name stable across releases.
To override the ledger name, add migrations.table only when configured in root config.php:
Use another directory
Section titled “Use another directory”Set the path once in root config.php; generation and runtime discovery use the same setting:
Merge this with your existing configuration. Paths are relative to foundation.root; absolute paths such as __DIR__ . '/db/migrations' also work. An explicitly configured directory must exist when running migrations. Generation creates the directory when writing its first file. An absent default directory means the application has no discovered migrations yet.
Migration directories do not need Composer autoload mappings or a classmap rebuild. Include the PHP files in production archives. Files are application code: keep executable work inside the documented methods, and use a top-level return new class extends Migration declaration.
Package migrations with Strauss
Section titled “Package migrations with Strauss”Include your migration directory in the plugin’s production archive. The Foundation generator reads extra.strauss.namespace_prefix and writes prefixed Foundation imports when configured. Handwritten files, older migrations, or a changed namespace-prefix configuration may still contain imports that need rewriting: include their directory in Strauss’s call-site scan alongside src/. Verify that the packaged migration imports match the packaged Foundation namespace. PHP namespace scoping leaves filename IDs and literal historical table names unchanged.
Group migrations by feature
Section titled “Group migrations by feature”Use a slash in the migration description:
With the default location, the file goes into db/migrations/reports/. Discovery includes subfolders, but execution is still globally ordered across all folders.
Generated filenames use <UTC timestamp>_<lowercase_description>.php, such as 20260924000001_add_published_at.php. Generation chooses a timestamp later than existing generated migrations in the configured tree, so consecutive commands preserve order. Developers still own dependencies when merging independently developed migrations. The filename without .php is the persistent ID; description changes after application are identity changes too. Discovery loads files matching 14 digits, an underscore, and a lowercase description containing letters, numbers, or underscores. Keep helper files under other names.
Contribute migrations explicitly
Section titled “Contribute migrations explicitly”Packages or applications with additional migration sources can contribute objects through a provider:
Import StellarWP\Foundation\Migrations\MigrationsProvider, StellarWP\Foundation\Migrations\ValueObjects\MigrationRegistration, and StellarWP\Foundation\Container\Contracts\Resolver as C in that provider. Package_Migration should extend StellarWP\Foundation\Migrations\Migration, just like generated anonymous migrations. Explicit contributions and discovered migrations share one ordered collection; contribute each migration once. Registrations keep the ID separate from the declaration object, preserving optional data and description capabilities.
Extend Migration and implement up(); override down() when a safe inverse exists. Direct implementation of Contracts\Migration remains supported for declarations that need a different base class, but requires both methods. Optional data and description behavior use separate interfaces. Foundation preserves these extension contracts within 2.x, including inherited method signatures and constructor expectations. Adding a base-class method can collide with consumer methods, so the base class is not an unrestricted extension surface for new Foundation features.
The registration supplies the ID. IDs are compared in ascending byte order. They must be unique, nonblank, unpadded, and no more than 191 bytes; 0 and latest are reserved targets. Choose IDs whose lexical order respects dependencies. Applications using only explicit contributions can omit discovery configuration.
Describe a migration
Section titled “Describe a migration”Add DescribesMigration to a migration to show a readable description in migrate:status:
Customize stubs and test migrations
Section titled “Customize stubs and test migrations”Copy the package stubs into foundation/stubs/database/ to customize generated code. Start with the CLI stub guide. Table namespaces remain configurable through generator settings; migration placement is controlled by migrations.path.
Test create, alteration, rollback, and preview against real database tables. Include failures after an earlier DDL statement and before history recording. Verify that the failed migration remains unrecorded, existing data survives, later migrations do not run, and deliberate repair allows a subsequent run. Register a fresh container per test and use application-specific test table names.