Skip to content

Commit efaa2c7

Browse files
committed
Add ForbidMultipleTableCreationsRule for Laravel
1 parent 78c5ad6 commit efaa2c7

File tree

10 files changed

+196
-19
lines changed

10 files changed

+196
-19
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,13 @@ Forbids using Laravel’s `after()` column modifier in migrations.
5858
Using `after()` can force a full table rewrite or long locks (engine-dependent), which is unsafe for large or production tables.
5959

6060
No configuration is required.
61+
62+
---
63+
64+
### ForbidMultipleTableCreationsRule
65+
66+
Forbids creating more than one table in a single Laravel migration.
67+
68+
A table creation is detected via multiple Schema::create() calls inside the same migration.
69+
70+
No configuration is required.

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"squizlabs/php_codesniffer": "^4.0",
1414
"rector/rector": "^2.3",
1515
"staabm/annotate-pull-request-from-checkstyle": "^1.8",
16-
"illuminate/database": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0"
16+
"illuminate/database": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0",
17+
"illuminate/support": "^12.44"
1718
},
1819
"scripts": {
1920
"test": "phpunit",

extension.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,10 @@ services:
3030

3131
-
3232
class: PhpStanMigrationRules\Rules\Laravel\ForbidAfterRule
33+
tags:
34+
- phpstan.rules.rule
35+
36+
-
37+
class: PhpStanMigrationRules\Rules\Laravel\ForbidMultipleTableCreationsRule
3338
tags:
3439
- phpstan.rules.rule
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpStanMigrationRules\Rules\Laravel;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Expr\StaticCall;
9+
use PhpParser\Node\Identifier;
10+
use PhpParser\Node\Name;
11+
use PHPStan\Analyser\Scope;
12+
use PHPStan\Rules\Rule;
13+
use PHPStan\Rules\RuleErrorBuilder;
14+
15+
/**
16+
* @implements Rule<StaticCall>
17+
*/
18+
final class ForbidMultipleTableCreationsRule implements Rule
19+
{
20+
private const string RULE_IDENTIFIER = 'laravel.schema.multipleTableCreationsForbidden';
21+
22+
/**
23+
* @var array<string, int>
24+
*/
25+
private array $createCallsPerFile = [];
26+
27+
public function getNodeType(): string
28+
{
29+
return StaticCall::class;
30+
}
31+
32+
public function processNode(Node $node, Scope $scope): array
33+
{
34+
$classReflection = $scope->getClassReflection();
35+
if ($classReflection === null) {
36+
return [];
37+
}
38+
39+
if (!$classReflection->isSubclassOf(\Illuminate\Database\Migrations\Migration::class)) {
40+
return [];
41+
}
42+
43+
if (!$this->isSchemaCreateCall($node, $scope)) {
44+
return [];
45+
}
46+
47+
$file = $scope->getFile();
48+
$this->createCallsPerFile[$file] = ($this->createCallsPerFile[$file] ?? 0) + 1;
49+
50+
if ($this->createCallsPerFile[$file] > 1) {
51+
return [
52+
RuleErrorBuilder::message(
53+
'Creating multiple tables in a single Laravel migration is forbidden. '
54+
. 'Each migration should create exactly one table.'
55+
)
56+
->identifier(self::RULE_IDENTIFIER)
57+
->build(),
58+
];
59+
}
60+
61+
return [];
62+
}
63+
64+
private function isSchemaCreateCall(StaticCall $node, Scope $scope): bool
65+
{
66+
if (!$node->name instanceof Identifier || $node->name->toString() !== 'create') {
67+
return false;
68+
}
69+
70+
if (!$node->class instanceof Name) {
71+
return false;
72+
}
73+
74+
$resolved = $scope->resolveName($node->class);
75+
76+
return $resolved === \Illuminate\Support\Facades\Schema::class
77+
|| $resolved === 'Illuminate\\Database\\Schema\\Schema'; // Rare case
78+
}
79+
}

tests/Rules/Laravel/ForbidAfterRuleTest.php

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,7 @@ public function testReportsAfterInsideCreateClosure(): void
4242
[
4343
[
4444
'Using "after()" in migrations is forbidden. It forces a full table rewrite or long locks depending on the engine, which is unsafe for large or production tables.',
45-
25,
46-
],
47-
[
48-
'No error to ignore is reported on line 20.',
49-
20,
45+
16,
5046
],
5147
]
5248
);
@@ -58,8 +54,8 @@ public function testDoesNotReportOutsideLaravelMigration(): void
5854
[__DIR__ . '/fixtures/NonMigrationClass.php'],
5955
[
6056
[
61-
'No error to ignore is reported on line 14.',
62-
14,
57+
'No error to ignore is reported on line 15.',
58+
15,
6359
]
6460
],
6561
);
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpStanMigrationRules\Tests\Rules\Laravel;
6+
7+
use PhpStanMigrationRules\Rules\Laravel\ForbidMultipleTableCreationsRule;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Testing\RuleTestCase;
10+
11+
/**
12+
* @extends RuleTestCase<ForbidMultipleTableCreationsRule>
13+
*/
14+
final class ForbidMultipleTableCreationsRuleTest extends RuleTestCase
15+
{
16+
protected function getRule(): Rule
17+
{
18+
return new ForbidMultipleTableCreationsRule();
19+
}
20+
21+
public function testReportsSecondSchemaCreateInMigration(): void
22+
{
23+
$this->analyse(
24+
[__DIR__ . '/Fixtures/ForbidMultipleTableCreations.php'],
25+
[
26+
[
27+
'Creating multiple tables in a single Laravel migration is forbidden. Each migration should create exactly one table.',
28+
18,
29+
'laravel.schema.multipleTableCreationsForbidden',
30+
],
31+
]
32+
);
33+
}
34+
35+
public function testAllowsSingleSchemaCreateInMigration(): void
36+
{
37+
$this->analyse(
38+
[__DIR__ . '/Fixtures/AllowSingleTableCreation.php'],
39+
[]
40+
);
41+
}
42+
43+
public function testDoesNotReportOutsideLaravelMigration(): void
44+
{
45+
$this->analyse(
46+
[__DIR__ . '/fixtures/NonMigrationClass.php'],
47+
[
48+
[
49+
'No error to ignore is reported on line 14.',
50+
14,
51+
]
52+
],
53+
);
54+
}
55+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpStanMigrationRules\Tests\Rules\Laravel\Fixtures;
6+
7+
use Illuminate\Database\Migrations\Migration;
8+
use Illuminate\Support\Facades\Schema;
9+
10+
final class AllowSingleTableCreation extends Migration
11+
{
12+
public function up(): void
13+
{
14+
Schema::create('users', static function (): void {
15+
});
16+
}
17+
}

tests/Rules/Laravel/fixtures/ForbidAfterInCreate.php

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,13 @@
66

77
use Illuminate\Database\Migrations\Migration;
88
use Illuminate\Database\Schema\Blueprint;
9+
use Illuminate\Support\Facades\Schema;
910

1011
final class ForbidAfterInCreate extends Migration
1112
{
1213
public function up(): void
1314
{
14-
// We not have the whole Laravel installed.
15-
// Which means that we do not have Facade support.
16-
$schema = new class {
17-
public function create(string $table, callable $callback): void
18-
{
19-
/** @phpstan-ignore-next-line */
20-
$callback(new Blueprint($table, 'random', null));
21-
}
22-
};
23-
24-
$schema->create('users', function (Blueprint $table): void {
15+
Schema::create('users', function (Blueprint $table): void {
2516
$table->string('email')->after('username');
2617
});
2718
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpStanMigrationRules\Tests\Rules\Laravel\Fixtures;
6+
7+
use Illuminate\Database\Migrations\Migration;
8+
use Illuminate\Support\Facades\Schema;
9+
10+
final class ForbidMultipleTableCreations extends Migration
11+
{
12+
public function up(): void
13+
{
14+
Schema::create('users', static function (): void {
15+
});
16+
Schema::create('courses', static function (): void {
17+
});
18+
}
19+
}

tests/Rules/Laravel/fixtures/NonMigrationClass.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace PhpStanMigrationRules\Tests\Rules\Laravel\Fixtures;
66

77
use Illuminate\Database\Schema\Blueprint;
8+
use Illuminate\Support\Facades\Schema;
89

910
final class NonMigrationClass
1011
{
@@ -14,5 +15,8 @@ public function run(): void
1415
$table = new Blueprint('users', 'random', null);
1516

1617
$table->string('email')->after('username');
18+
19+
Schema::create('users', static function (): void {
20+
});
1721
}
1822
}

0 commit comments

Comments
 (0)