Create and Run Database Migrations in CakePHP 4.x with Phinx
A practical guide to creating, running, and verifying database migrations in CakePHP 4.x using the built-in Phinx integration, with rollback procedures and team workflow rules.
17 Jul 2026, 19:43 UTC

Why Migrations Matter for CakePHP Projects
Schema changes that work on a developer's machine but break in staging or production are a common source of deployment failures. CakePHP 4.x bundles Phinx, a standalone migration library, so you can version-control schema changes and apply them repeatably across environments without writing raw SQL. This guide walks through creating, running, and verifying migrations — plus the recovery steps when something goes wrong.
Prerequisites
- CakePHP 4.x application with a configured database connection in
config/app.phporconfig/app_local.php - Database user with
CREATE,ALTER,DROP, andINDEXprivileges on the target schema - Console access to run
bin/cakecommands from the project root - Version control (Git) to commit migration files
Generate a New Migration
Use the bake console to scaffold a migration class. The command below creates a timestamped file in config/Migrations/ with a change() method that Phinx can reverse automatically for common operations.
bin/cake bake migration CreateArticles title:string body:text published:boolean created:datetime modified:datetime
Where to run: Project root directory. Permissions: Write access to config/Migrations/. Expected result: A file named like 20240115123456_CreateArticles.php appears.
Edit the Migration When Needed
The generated change() method uses Phinx's fluent table API. For reversible operations — createTable, addColumn, addIndex, renameColumn — you can leave it as-is. If you need irreversible changes (dropping a column with data, removing a table, or complex data transformations), replace change() with explicit up() and down() methods.
<?php
declare(strict_types=1);
use Migrations\AbstractMigration;
class CreateArticles extends AbstractMigration
{
public function change(): void
{
$table = $this->table('articles');
$table->addColumn('title', 'string', ['limit' => 255])
->addColumn('body', 'text')
->addColumn('published', 'boolean', ['default' => false])
->addColumn('created', 'datetime')
->addColumn('modified', 'datetime')
->create();
}
}
Decision point: If you later need to drop published and cannot recreate its data, write up() with removeColumn() and down() with addColumn() — Phinx will throw an exception if you keep change() for irreversible operations.
Run Pending Migrations
Apply all unrun migrations in timestamp order:
bin/cake migrations migrate
Target a specific version (useful for partial rollouts or testing):
bin/cake migrations migrate -t 20240115123456
Connection override: If you maintain multiple datasources (e.g., default and reporting), specify the target with -c reporting. Risk: Running on a large table may acquire long locks; schedule during low-traffic windows or use addColumn with the after option to minimize rewrite time.
Check Migration Status
List applied and pending migrations with their timestamps:
bin/cake migrations status
Output shows each migration's version, name, and state (up or down). Verify that the latest migration reads up.
Roll Back When Necessary
Revert the most recent migration:
bin/cake migrations rollback
Roll back to a specific version (all migrations after that version are reverted):
bin/cake migrations rollback -t 20240115123456
When this changes state: Only run rollback when you need to undo a schema change that has already been applied. The operation modifies the database and the phinxlog table.
Seed Reference or Test Data
Create a seed class for lookup tables or development fixtures:
bin/cake bake seed Articles
Edit config/Seeds/ArticlesSeed.php:
<?php
use Migrations\AbstractSeed;
class ArticlesSeed extends AbstractSeed
{
public function run(): void
{
$data = [
['title' => 'First Post', 'body' => 'Content...', 'published' => true, 'created' => date('Y-m-d H:i:s'), 'modified' => date('Y-m-d H:i:s')],
['title' => 'Draft Post', 'body' => 'Draft...', 'published' => false, 'created' => date('Y-m-d H:i:s'), 'modified' => date('Y-m-d H:i:s')],
];
$this->insert('articles', $data);
}
}
Execute seeds:
bin/cake migrations seed
Note: Seeds are not tracked in phinxlog; they run every time you invoke the command. Use them for idempotent reference data, not one-time migrations.
Verify the Result
- Schema match: Run
DESCRIBE articles;(MySQL/MariaDB) or\d articles(PostgreSQL) and confirm columns, types, and indexes match the latest migration. - Round-trip test: Execute
bin/cake migrations rollbackfollowed bybin/cake migrations migrate. Verify foreign keys, indexes, and constraints survive the cycle. - CI/CD gate: Add a pipeline step
bin/cake migrations migrate --no-interactionafter code deploy and before cache warm-up. Assert exit code 0. - Seed validation: After
bin/cake migrations seed, runSELECT * FROM articles LIMIT 5;to confirm data inserted correctly.
Recovery Scenarios
Missing or Corrupted phinxlog Table
If the log table is dropped or damaged, Phinx cannot determine which migrations have run. Reinitialize it:
bin/cake migrations migrate --init
This creates the phinxlog table. You must then manually insert version records for migrations that already ran, using the timestamp from each migration filename:
INSERT INTO phinxlog (version, migration_name, start_time, end_time, breakpoint)
VALUES (20240115123456, 'CreateArticles', NOW(), NOW(), 0);
Risk: Inserting wrong versions causes duplicate-run errors or missed migrations. Cross-check with bin/cake migrations status after repair.
Irreversible Migration Fails Mid-Run
If a migration with explicit up()/down() fails partway, the database may be in an intermediate state. Phinx does not wrap migrations in transactions for DDL statements on some drivers. Restore from the pre-deployment backup, fix the migration logic, and re-run.
Team Workflow Rules
- Commit every migration file to version control.
- Never edit a migration that has already run in any shared environment (staging, production, other developers' machines). Create a new migration for adjustments.
- Review migration files in pull requests — treat them like schema-altering code.
- Test migration performance on staging with production-like data volume before deploying.
Limitations
- Phinx's
change()auto-reversal only supports a subset of operations. Complex alterations require manualup()/down(). - No built-in transaction safety for DDL on MySQL/MariaDB; plan for manual recovery.
- Seeds are not versioned — they are repeatable inserts, not tracked migrations.
Quick Reference
| Task | Command |
|---|---|
| Create migration | bin/cake bake migration Name column:type ... |
| Run all pending | bin/cake migrations migrate |
| Run to version | bin/cake migrations migrate -t <version> |
| Rollback last | bin/cake migrations rollback |
| Rollback to version | bin/cake migrations rollback -t <version> |
| Show status | bin/cake migrations status |
| Create seed | bin/cake bake seed Name |
| Run seeds | bin/cake migrations seed |
| Reinit phinxlog | bin/cake migrations migrate --init |
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.