Starting with 5.2.0, cakephp/migrations ships two capability interfaces that let migrations declare their style explicitly:
Migrations\ReversibleMigrationInterface— for migrations that define a single reversiblechange()method.Migrations\DirectionalMigrationInterface— for migrations that define separateup()anddown()methods.
A migration implements one of the two interfaces, never both.
Until now, Environment dispatched migrations through method_exists() checks against change(), up(), and down(). This works at runtime but has a few drawbacks:
- A typo such as
function chnage()silently no-ops at run time. The runtime cannot tell whether the migration is reversible or directional, so it just does nothing. - IDEs and static analyzers cannot resolve
change()/up()/down()on a genericMigrationInterface, so refactoring tools and PHPStan narrowing do not work. - Custom runners that wrap
MigrationInterfacehave to use reflection to figure out the migration style.
The capability interfaces are a way out of this without breaking existing code:
Environmentnow dispatches viainstanceoffirst and falls back tomethod_exists()for migrations that have not adopted the interfaces yet.- The interfaces declare their method contracts as PHPDoc
@methodtags (not as real abstract methods), which means addingimplementsto an existing migration is a zero-friction change — your method signature is not validated against an abstract.
::: tip 5.x is a soft window
The PHPDoc-only contract is intentional. The 6.x release is expected to promote the @method tags to real abstract method declarations, at which point a missing or mistyped change() / up() / down() becomes a static error. The 5.next release gives you a runway to adopt the interfaces without breakage; the 6.x release tightens the contract.
:::
If you do nothing, your existing migrations keep working. Environment retains a method_exists() fallback throughout the 5.x cycle.
Adopting the interfaces now is recommended because:
- New bakes already emit the right
implementsclause. - Static analysis and IDE autocomplete start working on your migrations.
- Your app is upgrade-ready when 6.x lands.
Before:
<?php
declare(strict_types=1);
use Migrations\BaseMigration;
class CreateProducts extends BaseMigration
{
public function change(): void
{
$this->table('products')
->addColumn('name', 'string')
->create();
}
}After:
<?php
declare(strict_types=1);
use Migrations\BaseMigration;
use Migrations\ReversibleMigrationInterface;
class CreateProducts extends BaseMigration implements ReversibleMigrationInterface
{
public function change(): void
{
$this->table('products')
->addColumn('name', 'string')
->create();
}
}Before:
<?php
declare(strict_types=1);
use Migrations\BaseMigration;
class BackfillOrderTotals extends BaseMigration
{
public function up(): void
{
$this->execute('UPDATE orders SET total = ...');
}
public function down(): void
{
$this->execute('UPDATE orders SET total = NULL');
}
}After:
<?php
declare(strict_types=1);
use Migrations\BaseMigration;
use Migrations\DirectionalMigrationInterface;
class BackfillOrderTotals extends BaseMigration implements DirectionalMigrationInterface
{
public function up(): void
{
$this->execute('UPDATE orders SET total = ...');
}
public function down(): void
{
$this->execute('UPDATE orders SET total = NULL');
}
}Anonymous migrations get the same treatment:
return new class extends BaseMigration implements ReversibleMigrationInterface
{
public function change(): void
{
}
};cakephp/migrations ships a rector rule that adds the right implements clause to every migration in your config/Migrations/ folder.
Add the following to your rector.php:
use Migrations\Rector\AddMigrationCapabilityInterfaceRector;
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withPaths([
__DIR__ . '/config/Migrations',
])
->withRules([
AddMigrationCapabilityInterfaceRector::class,
]);Then run rector:
vendor/bin/rector process --config=rector.phpWhat the rule does:
- For every class extending
Migrations\BaseMigration(directly or transitively):- If the class defines
change(), addimplements ReversibleMigrationInterface. - If the class defines
up()ordown(), addimplements DirectionalMigrationInterface.
- If the class defines
- Classes that already implement either capability interface are skipped.
- Classes that define both
change()andup()/down()are skipped — these are user errors that need a deliberate decision.
If you also want to normalize visibility and return types on your migration methods (the shape 6.x will expect), compose with rector's built-in sets:
use Migrations\Rector\AddMigrationCapabilityInterfaceRector;
use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\SetList;
return RectorConfig::configure()
->withPaths([
__DIR__ . '/config/Migrations',
])
->withRules([
AddMigrationCapabilityInterfaceRector::class,
])
->withSets([
SetList::TYPE_DECLARATION,
]);::: warning Only point rector at your migrations folder
Migration paths use scoped rector configs by default; pointing rector at src/ or tests/ will apply unrelated transformations. Keep the path list narrow.
:::
- Migrations not on
BaseMigration. Anything still on a legacy PhinxAbstractMigrationfork or a custom base that does not extendBaseMigrationis skipped. Add theimplementsclause by hand. - Dynamically generated migration classes (eval'd test fixtures, factories). Rector cannot see them. Add the
implementsclause at the generation site. - Custom base classes that themselves declare
change()/up()/down(). Rector adds the interface to the base class once. If the base lives in a third-party plugin you do not control, either implement the capability interface on the leaf class or PR the plugin upstream. - Bake-generated migrations from older versions. Bake templates emit the
implementsclause out of the box from 5.next; older bakes do not. Rector cleans those up in one pass.
The 6.x release is expected to:
- Promote the PHPDoc
@methoddeclarations on the capability interfaces to real abstract method declarations. - Remove the
method_exists()fallback inEnvironment.
Running rector now means the 6.x bump is a no-op for your migration files.