diff --git a/README.md b/README.md index e5550b5..351c9bc 100644 --- a/README.md +++ b/README.md @@ -108,3 +108,17 @@ Ensures each migration creates at most one table. |---|---| | [Phinx](./src/Rules/Phinx/ForbidMultipleTableCreationsRule.php) | Multiple calls to `create()` on table instances | | [Laravel](./src/Rules/Laravel/ForbidMultipleTableCreationsRule.php) | Multiple `Schema::create()` calls in the same migration | + +--- + +### Rule: `NoDownMethodRule` +Forbids the usage of the `down` method in migrations. +> Useful for teams that prefer forward-only migrations or rely solely on the `change` method for extensive rollback support where possible. + +#### Support + +| Framework | Forbidden usage | +|---|---| +| [Phinx](./src/Rules/Phinx/NoDownMethodRule.php) | `public function down(): void` | +| [Laravel](./src/Rules/Laravel/NoDownMethodRule.php) | `public function down(): void` | + diff --git a/extension.neon b/extension.neon index 0d11d55..b2dec82 100644 --- a/extension.neon +++ b/extension.neon @@ -7,6 +7,7 @@ parametersSchema: forbidEnumColumn: bool() forbidRawSql: bool() forbidMultipleTableCreations: bool() + forbidDown: bool() ]) laravel: structure([ enforceCollation: bool() @@ -14,6 +15,7 @@ parametersSchema: forbidEnumColumn: bool() forbidRawSql: bool() forbidMultipleTableCreations: bool() + forbidDown: bool() ]) ]) @@ -26,12 +28,14 @@ parameters: forbidEnumColumn: false forbidRawSql: false forbidMultipleTableCreations: true + forbidDown: false laravel: enforceCollation: true forbidAfter: true forbidEnumColumn: false forbidRawSql: false forbidMultipleTableCreations: true + forbidDown: false conditionalTags: PhpStanMigrationRules\Rules\Phinx\EnforceCollationRule: @@ -44,6 +48,8 @@ conditionalTags: phpstan.rules.rule: %migrationRules.phinx.forbidRawSql% PhpStanMigrationRules\Rules\Phinx\ForbidMultipleTableCreationsRule: phpstan.rules.rule: %migrationRules.phinx.forbidMultipleTableCreations% + PhpStanMigrationRules\Rules\Phinx\NoDownMethodRule: + phpstan.rules.rule: %migrationRules.phinx.forbidDown% PhpStanMigrationRules\Rules\Laravel\EnforceCollationRule: phpstan.rules.rule: %migrationRules.laravel.enforceCollation% PhpStanMigrationRules\Rules\Laravel\ForbidAfterRule: @@ -54,6 +60,8 @@ conditionalTags: phpstan.rules.rule: %migrationRules.laravel.forbidRawSql% PhpStanMigrationRules\Rules\Laravel\ForbidMultipleTableCreationsRule: phpstan.rules.rule: %migrationRules.laravel.forbidMultipleTableCreations% + PhpStanMigrationRules\Rules\Laravel\NoDownMethodRule: + phpstan.rules.rule: %migrationRules.laravel.forbidDown% services: - @@ -73,6 +81,9 @@ services: - class: PhpStanMigrationRules\Rules\Phinx\ForbidMultipleTableCreationsRule + - + class: PhpStanMigrationRules\Rules\Phinx\NoDownMethodRule + - class: PhpStanMigrationRules\Rules\Laravel\EnforceCollationRule arguments: @@ -89,3 +100,6 @@ services: - class: PhpStanMigrationRules\Rules\Laravel\ForbidMultipleTableCreationsRule + + - + class: PhpStanMigrationRules\Rules\Laravel\NoDownMethodRule diff --git a/src/Rules/Laravel/NoDownMethodRule.php b/src/Rules/Laravel/NoDownMethodRule.php new file mode 100644 index 0000000..4a2259a --- /dev/null +++ b/src/Rules/Laravel/NoDownMethodRule.php @@ -0,0 +1,49 @@ + + */ +final class NoDownMethodRule extends LaravelRule +{ + private const string RULE_IDENTIFIER = 'laravel.schema.noDownMethod'; + + private const string MESSAGE = + 'Forbidden: "down" method. ' + . 'Why: a "down" method enables rollbacks, which can cause data loss and break forward-only migration strategies. ' + . 'Fix: use the "change" method for reversible migrations, or omit the rollback path entirely.'; + + public function getNodeType(): string + { + return ClassMethod::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$this->isLaravelMigration($scope)) { + return []; + } + + if ($node->name->toString() !== 'down') { + return []; + } + + if (!$node->isPublic()) { + return []; + } + + return [ + RuleErrorBuilder::message(self::MESSAGE) + ->identifier(self::RULE_IDENTIFIER) + ->build(), + ]; + } +} diff --git a/src/Rules/Phinx/NoDownMethodRule.php b/src/Rules/Phinx/NoDownMethodRule.php new file mode 100644 index 0000000..e1b0194 --- /dev/null +++ b/src/Rules/Phinx/NoDownMethodRule.php @@ -0,0 +1,49 @@ + + */ +final class NoDownMethodRule extends PhinxRule +{ + private const string RULE_IDENTIFIER = 'phinx.schema.noDownMethod'; + + private const string MESSAGE = + 'Forbidden: "down" method. ' + . 'Why: a "down" method enables rollbacks, which can cause data loss and break forward-only migration strategies. ' + . 'Fix: use the "change" method for reversible migrations, or omit the rollback path entirely.'; + + public function getNodeType(): string + { + return ClassMethod::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$this->isPhinxMigration($scope)) { + return []; + } + + if ($node->name->toString() !== 'down') { + return []; + } + + if (!$node->isPublic()) { + return []; + } + + return [ + RuleErrorBuilder::message(self::MESSAGE) + ->identifier(self::RULE_IDENTIFIER) + ->build(), + ]; + } +} diff --git a/tests/Rules/Laravel/NoDownMethodRuleTest.php b/tests/Rules/Laravel/NoDownMethodRuleTest.php new file mode 100644 index 0000000..e2cda76 --- /dev/null +++ b/tests/Rules/Laravel/NoDownMethodRuleTest.php @@ -0,0 +1,54 @@ + + */ +final class NoDownMethodRuleTest extends RuleTestCase +{ + protected function getRule(): Rule + { + return new NoDownMethodRule(); + } + + public function testReportsDownMethod(): void + { + $this->analyse( + [__DIR__ . '/fixtures/NoDownMethod.php'], + [ + [ + 'Forbidden: "down" method. Why: a "down" method enables rollbacks, which can cause data loss and break forward-only migration strategies. Fix: use the "change" method for reversible migrations, or omit the rollback path entirely.', + 11, + ], + ] + ); + } + + public function testReportsDownMethodInAnonymousClass(): void + { + $this->analyse( + [__DIR__ . '/fixtures/NoDownMethodAnonymous.php'], + [ + [ + 'Forbidden: "down" method. Why: a "down" method enables rollbacks, which can cause data loss and break forward-only migration strategies. Fix: use the "change" method for reversible migrations, or omit the rollback path entirely.', + 11, + ], + ] + ); + } + + public function testDoesNotReportChangeMethod(): void + { + $this->analyse( + [__DIR__ . '/fixtures/WithChangeMethod.php'], + [] + ); + } +} diff --git a/tests/Rules/Laravel/fixtures/NoDownMethod.php b/tests/Rules/Laravel/fixtures/NoDownMethod.php new file mode 100644 index 0000000..a74c226 --- /dev/null +++ b/tests/Rules/Laravel/fixtures/NoDownMethod.php @@ -0,0 +1,15 @@ + + */ +final class NoDownMethodRuleTest extends RuleTestCase +{ + protected function getRule(): Rule + { + return new NoDownMethodRule(); + } + + public function testReportsDownMethod(): void + { + $this->analyse( + [__DIR__ . '/fixtures/NoDownMethod.php'], + [ + [ + 'Forbidden: "down" method. Why: a "down" method enables rollbacks, which can cause data loss and break forward-only migration strategies. Fix: use the "change" method for reversible migrations, or omit the rollback path entirely.', + 11, + ], + ] + ); + } + + public function testDoesNotReportChangeMethod(): void + { + $this->analyse( + [__DIR__ . '/fixtures/WithChangeMethod.php'], + [] + ); + } +} diff --git a/tests/Rules/Phinx/fixtures/NoDownMethod.php b/tests/Rules/Phinx/fixtures/NoDownMethod.php new file mode 100644 index 0000000..d1f2fe2 --- /dev/null +++ b/tests/Rules/Phinx/fixtures/NoDownMethod.php @@ -0,0 +1,15 @@ +