This project uses CakePHP Migrations (powered by Phinx) for all database schema changes.
All migrations live in config/Migrations/. File naming: YYYYMMDDHHMMSS_DescriptiveName.php.
# Check current status
innkeeper exec bin/cake migrations status
# Run all pending migrations
innkeeper exec bin/cake migrations migrate
# Rollback the last migration
innkeeper exec bin/cake migrations rollback
# Rollback to a specific version
innkeeper exec "bin/cake migrations rollback -t <TIMESTAMP>"# Generate a migration file
innkeeper exec "bin/cake bake migration CreateExamples"
innkeeper exec "bin/cake bake migration AddPriceToExamples"Edit the generated file in config/Migrations/, then run it:
innkeeper exec bin/cake migrations migrateAll migrations must extend Migrations\BaseMigration (not the old Phinx\Migration\AbstractMigration or Migrations\AbstractMigration). This is the CakePHP Migrations 5.x base class.
<?php
declare(strict_types=1);
use Migrations\BaseMigration;
class CreateExamples extends BaseMigration
{
public function change(): void
{
$this->table('examples')
->addColumn('name', 'string', ['limit' => 100, 'null' => false])
->addColumn('description', 'text', ['null' => true, 'default' => null])
->addColumn('is_active', 'boolean', ['default' => true, 'null' => false])
->addColumn('created', 'datetime', ['null' => true, 'default' => null])
->addColumn('modified', 'datetime', ['null' => true, 'default' => null])
->create();
}
}See config/Migrations/20260405120000_CreateExampleTables.php for a full working example.
Migration class names should clearly describe the change:
CreateExamples— new tableAddPriceToExamples— adding column(s)RemoveDeprecatedColumnsFromExamples— removing columnsAddIndexToExamplesCreatedDate— adding an index
This is the most important rule. When working on a feature that hasn't been committed yet, do not keep adding new migration files for incremental changes to the same tables. Instead: roll back, edit the existing migration, and re-run.
- Each migration is permanent once committed. During development, they are disposable.
- Multiple small migrations touching the same tables create bloat and a confusing history.
- One clean migration per logical feature is easier to review and reason about.
You create 20260401120000_CreateExamples.php. Later you realize you need two more columns.
Wrong — adding a second migration:
20260401120000_CreateExamples.php ← creates table
20260401150000_AddMoreColumnsToExamples.php ← adds 2 columns
Right — consolidate into one:
# 1. Roll back
innkeeper exec "bin/cake migrations rollback -t <timestamp_before_first>"
# 2. Edit the original migration to include all columns
# 3. Re-run
innkeeper exec bin/cake migrations migrate- The first migration has already been committed / merged / deployed — never edit a shipped migration.
- The changes are logically unrelated (e.g., one for Users, another for Products).
- You are intentionally splitting a large change for incremental deployment.
A "ghost" entry appears when a migration file is deleted without rolling it back first. bin/cake migrations status shows it as ** MISSING **.
Always roll back before deleting:
innkeeper exec bin/cake migrations rollback- Delete or edit the migration file
- Re-run if needed
If a ghost does occur, remove the row from the phinxlog table:
mysql -h 127.0.0.1 -P <PORT> -u <USER> -p<PASS> <DB> \
-e "DELETE FROM phinxlog WHERE migration_name = 'GhostMigrationClassName';"(Adjust connection details to match your config/app_local.php.)
- One migration per logical feature during development — consolidate before committing.
- Roll back → edit → re-run for uncommitted migrations.
- Never edit a committed / deployed migration.
- Always roll back before deleting a migration file to avoid ghost phinxlog entries.
- Name migrations descriptively — the class name should summarize the change.