Skip to content

Migrations

Foundation migrations apply ordered database changes and record each successful run in a WordPress-backed ledger. Prefer the bundled WP-CLI command during deployment so initialization, locking, execution, and status reporting follow one path.

Install the generator as a development dependency:

composer require --dev stellarwp/foundation-cli

Generate the application provider before its tables and migrations:

vendor/bin/foundation make:database-provider
vendor/bin/foundation make:database-table Reports_Table
vendor/bin/foundation make:database-migration Create_Reports_Table

The generators use the project’s Composer namespace and create this feature structure by default:

src/Database/
  Provider.php
  Migrations/
    Create_Reports_Table.php
  Tables/
    Reports_Table.php

When src/Database/Provider.php exists, the table and migration generators add their container registrations automatically. Register that provider in the application’s ordered provider list as shown in Database configuration.

Project-specific stubs can override the defaults at:

foundation/stubs/database/provider.stub
foundation/stubs/database/table.stub
foundation/stubs/database/table-migration.stub
foundation/stubs/database/migration.stub

The generated src/Database/Tables/Reports_Table.php owns its physical name and desired schema. Database::tableName() applies the current WordPress table prefix.

<?php declare(strict_types=1);

namespace YourPlugin\Database\Tables;

use StellarWP\Foundation\Database\Contracts\Database;
use StellarWP\Foundation\Database\Contracts\Table;
use StellarWP\Foundation\Database\Table\TableDefinition;

final readonly class Reports_Table implements Table {

	public const string ID    = 'reports_table';
	public const string TABLE = 'reports';

	public function __construct(
		private Database $database
	) {
	}

	public function id(): string {
		return self::ID;
	}

	public function name(): string {
		return $this->database->tableName( self::TABLE );
	}

	public function definition(): TableDefinition {
		return TableDefinition::for( $this )
			->bigIncrements( 'id' )
			->string( 'status', 20 )->default( 'draft' )
			->longText( 'payload' )
			->dateTime( 'created_at' )
			->dateTime( 'updated_at' )->nullable()
			->index( 'status', 'status' );
	}
}

The generated src/Database/Migrations/Create_Reports_Table.php passes the table object to Schema. The schema service uses dbDelta() and verifies the resulting definition before the migration is recorded as successful.

<?php declare(strict_types=1);

namespace YourPlugin\Database\Migrations;

use StellarWP\Foundation\Database\Contracts\Migration;
use StellarWP\Foundation\Database\Contracts\Schema;
use YourPlugin\Database\Tables\Reports_Table;

final readonly class Create_Reports_Table implements Migration {

	public const string ID = '2026_08_21_120000_create_reports_table';

	public function __construct(
		private Reports_Table $table
	) {
	}

	public function id(): string {
		return self::ID;
	}

	public function up( Schema $schema ): void {
		$schema->createOrUpdate( $this->table );
	}

	public function down( Schema $schema ): void {
		$schema->drop( $this->table );
	}
}

Migration IDs are permanent, byte-exact identifiers. The generator prefixes them with a sortable timestamp so migration history is easy to inspect, but execution follows provider contribution order rather than sorting by ID. Register providers and migrations in dependency order, and do not change an ID after the migration has been deployed.

For later schema changes, update the table’s desired definition and create a new migration that applies it. Use Schema::execute() for data changes or schema operations that dbDelta() cannot express reliably.

Create or reconcile Foundation’s migration ledger and lock table before running migrations:

wp your-plugin migrate --initialize

Run this idempotent command during every deployment. Replace your-plugin with the configured command prefix; applications using the default prefix run wp nx migrate --initialize.

wp your-plugin migrate --run

Running the command without an operation displays migration status:

wp your-plugin migrate

The runner acquires the configured migration lock, executes pending migrations in provider contribution order, and records each successful migration in one batch.

Roll back the latest applied batch:

wp your-plugin migrate --rollback

Roll back every configured migration and run them again:

wp your-plugin migrate --refresh --yes

Drop only Foundation’s migration ledger when intentionally resetting migration history:

wp your-plugin migrate --drop-store --yes

WP-CLI is the preferred deployment interface. For controlled environments that cannot invoke WP-CLI, resolve the same Migrator service from the application container:

use StellarWP\Foundation\Database\Migration\Migrator;

$migrator = $container->get( Migrator::class );

$migrator->initialize();
$result = $migrator->run();

The programmatic API follows the same ledger and lock rules as the command. Do not run migrations during every normal WordPress request.

Use wpunit tests for table definitions, schema reconciliation, and migrations that execute against WordPress. Use integration when the test proves contributions from multiple providers, and use wpcli for the real migration command lifecycle.

Create and remove application tables within the test lifecycle so tests exercise the real wpdb and dbDelta() behavior rather than a PHP fake.