diff --git a/config/sets/laravel-code-quality.php b/config/sets/laravel-code-quality.php
index d3168e9c1..a8685ab73 100644
--- a/config/sets/laravel-code-quality.php
+++ b/config/sets/laravel-code-quality.php
@@ -20,6 +20,7 @@
use RectorLaravel\Rector\FuncCall\SleepFuncToSleepStaticCallRector;
use RectorLaravel\Rector\FuncCall\ThrowIfAndThrowUnlessExceptionsToUseClassStringRector;
use RectorLaravel\Rector\MethodCall\EloquentOrderByToLatestOrOldestRector;
+use RectorLaravel\Rector\MethodCall\EloquentWhereIdToWhereKeyRector;
use RectorLaravel\Rector\MethodCall\RedirectBackToBackHelperRector;
use RectorLaravel\Rector\MethodCall\RedirectRouteToToRouteHelperRector;
use RectorLaravel\Rector\MethodCall\ReverseConditionableMethodCallRector;
@@ -52,4 +53,5 @@
$rectorConfig->rule(DispatchToHelperFunctionsRector::class);
$rectorConfig->rule(NotFilledBlankFuncCallToBlankFilledFuncCallRector::class);
$rectorConfig->rule(EloquentOrderByToLatestOrOldestRector::class);
+ $rectorConfig->rule(EloquentWhereIdToWhereKeyRector::class);
};
diff --git a/docs/rector_rules_overview.md b/docs/rector_rules_overview.md
index 9c9c85011..1098d1c5a 100644
--- a/docs/rector_rules_overview.md
+++ b/docs/rector_rules_overview.md
@@ -1,4 +1,4 @@
-# 86 Rules Overview
+# 87 Rules Overview
## AbortIfRector
@@ -675,6 +675,30 @@ Changes `orderBy()` to `latest()` or `oldest()`
+## EloquentWhereIdToWhereKeyRector
+
+Refactor model calls to the primary key using the `whereKey` and `whereKeyNot` methods
+
+- class: [`RectorLaravel\Rector\MethodCall\EloquentWhereIdToWhereKeyRector`](../src/Rector/MethodCall/EloquentWhereIdToWhereKeyRector.php)
+
+```diff
+-User::where('id', '=', $user->id)->get();
+-User::where('id', $user->id)->get();
++User::whereKey($user)->get();
++User::whereKey($user)->get();
+```
+
+
+
+```diff
+-User::where('id', '!=', $user->id)->get();
+-User::whereNot('id', $user->id)->get();
++User::whereKeyNot($user)->get();
++User::whereKeyNot($user)->get();
+```
+
+
+
## EloquentWhereRelationTypeHintingParameterRector
Add type hinting to where relation has methods e.g. `whereHas`, `orWhereHas`, `whereDoesntHave`, `orWhereDoesntHave`, `whereHasMorph`, `orWhereHasMorph`, `whereDoesntHaveMorph`, `orWhereDoesntHaveMorph`
diff --git a/src/Rector/MethodCall/EloquentWhereIdToWhereKeyRector.php b/src/Rector/MethodCall/EloquentWhereIdToWhereKeyRector.php
new file mode 100644
index 000000000..9bc51ff2c
--- /dev/null
+++ b/src/Rector/MethodCall/EloquentWhereIdToWhereKeyRector.php
@@ -0,0 +1,165 @@
+id)->get();
+User::where('id', $user->id)->get();
+CODE_SAMPLE,
+ <<<'CODE_SAMPLE'
+User::whereKey($user)->get();
+User::whereKey($user)->get();
+CODE_SAMPLE
+ ),
+ new CodeSample(
+ <<<'CODE_SAMPLE'
+User::where('id', '!=', $user->id)->get();
+User::whereNot('id', $user->id)->get();
+CODE_SAMPLE,
+ <<<'CODE_SAMPLE'
+User::whereKeyNot($user)->get();
+User::whereKeyNot($user)->get();
+CODE_SAMPLE
+ ),
+ ]
+ );
+ }
+
+ /**
+ * @return array>
+ */
+ public function getNodeTypes(): array
+ {
+ return [MethodCall::class, StaticCall::class];
+ }
+
+ /**
+ * @param MethodCall|StaticCall $node
+ */
+ public function refactor(Node $node): ?Node
+ {
+ if (! $node instanceof MethodCall && ! $node instanceof StaticCall) {
+ return null;
+ }
+
+ $isWhere = $this->queryBuilderAnalyzer->isMatchingCall($node, 'where');
+ $isWhereNot = $this->queryBuilderAnalyzer->isMatchingCall($node, 'whereNot');
+
+ if (! $isWhere && ! $isWhereNot) {
+ return null;
+ }
+
+ $args = $node->getArgs();
+ $argCount = count($args);
+
+ if ($argCount === 2) {
+ return $this->refactorTwoArgumentWhere($node, $args, $isWhereNot);
+ }
+
+ if ($argCount === 3 && $isWhere) {
+ return $this->refactorThreeArgumentWhere($node, $args);
+ }
+
+ return null;
+ }
+
+ /**
+ * @param Arg[] $args
+ */
+ private function refactorTwoArgumentWhere(MethodCall|StaticCall $node, array $args, bool $isWhereNot): ?Node
+ {
+ if (! $args[0] instanceof Arg || ! $args[0]->value instanceof String_) {
+ return null;
+ }
+
+ $columnName = $args[0]->value->value;
+ if ($columnName !== 'id') {
+ return null;
+ }
+
+ if (! $args[1] instanceof Arg || ! $args[1]->value instanceof PropertyFetch) {
+ return null;
+ }
+
+ $propertyFetch = $args[1]->value;
+ if (! $this->isName($propertyFetch->name, 'id')) {
+ return null;
+ }
+
+ // where() with 2 args uses '=' operator -> whereKey
+ // whereNot() with 2 args uses '!=' operator -> whereKeyNot
+ $newMethodName = $isWhereNot ? 'whereKeyNot' : 'whereKey';
+ $node->name = new Identifier($newMethodName);
+ $node->args = [new Arg($propertyFetch->var)];
+
+ return $node;
+ }
+
+ /**
+ * @param Arg[] $args
+ */
+ private function refactorThreeArgumentWhere(MethodCall|StaticCall $node, array $args): ?Node
+ {
+ if (! $args[0] instanceof Arg || ! $args[0]->value instanceof String_) {
+ return null;
+ }
+
+ $columnName = $args[0]->value->value;
+ if ($columnName !== 'id') {
+ return null;
+ }
+
+ if (! $args[1] instanceof Arg || ! $args[1]->value instanceof String_) {
+ return null;
+ }
+
+ $operator = $args[1]->value->value;
+ if (! in_array($operator, ['=', '!='], true)) {
+ return null;
+ }
+
+ if (! $args[2] instanceof Arg || ! $args[2]->value instanceof PropertyFetch) {
+ return null;
+ }
+
+ $propertyFetch = $args[2]->value;
+ if (! $this->isName($propertyFetch->name, 'id')) {
+ return null;
+ }
+
+ $newMethodName = $operator === '=' ? 'whereKey' : 'whereKeyNot';
+ $node->name = new Identifier($newMethodName);
+ $node->args = [new Arg($propertyFetch->var)];
+
+ return $node;
+ }
+}
diff --git a/stubs/Illuminate/Database/Eloquent/Builder.php b/stubs/Illuminate/Database/Eloquent/Builder.php
index e914d21c3..e9dccf3f1 100644
--- a/stubs/Illuminate/Database/Eloquent/Builder.php
+++ b/stubs/Illuminate/Database/Eloquent/Builder.php
@@ -13,4 +13,20 @@ class Builder extends QueryBuilder
public function publicMethodBelongsToEloquentQueryBuilder(): void {}
public function excludablePublicMethodBelongsToEloquentQueryBuilder(): void {}
+
+ /**
+ * @return $this
+ */
+ public function where($column, $operator = null, $value = null, $boolean = 'and'): static
+ {
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function whereNot($column, $operator = null, $value = null, $boolean = 'and'): static
+ {
+ return $this;
+ }
}
diff --git a/stubs/Illuminate/Database/Eloquent/Model.php b/stubs/Illuminate/Database/Eloquent/Model.php
index fbdd3d3da..9a51e19bd 100644
--- a/stubs/Illuminate/Database/Eloquent/Model.php
+++ b/stubs/Illuminate/Database/Eloquent/Model.php
@@ -25,6 +25,16 @@ abstract class Model
*/
protected $primaryKey = 'id';
+ /**
+ * Begin querying the model.
+ *
+ * @return \Illuminate\Database\Eloquent\Builder
+ */
+ public static function query()
+ {
+ return new Builder;
+ }
+
/**
* Exists in the Illuminate/Database/Eloquent/Concerns/HasTimestamps trait
* Put here for simplicity
diff --git a/stubs/Illuminate/Database/Query/Builder.php b/stubs/Illuminate/Database/Query/Builder.php
index 2f5925f3c..9f4d340c1 100644
--- a/stubs/Illuminate/Database/Query/Builder.php
+++ b/stubs/Illuminate/Database/Query/Builder.php
@@ -22,6 +22,22 @@ public function orderBy($column, string $direction): static {}
*/
public function orderByDesc($column): static {}
+ /**
+ * @return $this
+ */
+ public function where($column, $operator = null, $value = null, $boolean = 'and'): static
+ {
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function whereNot($column, $operator = null, $value = null, $boolean = 'and'): static
+ {
+ return $this;
+ }
+
protected function protectedMethodBelongsToQueryBuilder(): void {}
private function privateMethodBelongsToQueryBuilder(): void {}
diff --git a/tests/Rector/MethodCall/EloquentWhereIdToWhereKeyRector/EloquentWhereIdToWhereKeyRectorTest.php b/tests/Rector/MethodCall/EloquentWhereIdToWhereKeyRector/EloquentWhereIdToWhereKeyRectorTest.php
new file mode 100644
index 000000000..d642ed6a4
--- /dev/null
+++ b/tests/Rector/MethodCall/EloquentWhereIdToWhereKeyRector/EloquentWhereIdToWhereKeyRectorTest.php
@@ -0,0 +1,31 @@
+doTestFile($filePath);
+ }
+
+ public function provideConfigFilePath(): string
+ {
+ return __DIR__ . '/config/configured_rule.php';
+ }
+}
diff --git a/tests/Rector/MethodCall/EloquentWhereIdToWhereKeyRector/Fixture/chained_methods_fixture.php.inc b/tests/Rector/MethodCall/EloquentWhereIdToWhereKeyRector/Fixture/chained_methods_fixture.php.inc
new file mode 100644
index 000000000..b1a457693
--- /dev/null
+++ b/tests/Rector/MethodCall/EloquentWhereIdToWhereKeyRector/Fixture/chained_methods_fixture.php.inc
@@ -0,0 +1,40 @@
+id)->where('status', 'active')->get();
+
+ $query = User::query()
+ ->where('name', 'John')
+ ->where('id', '=', $user->id)
+ ->where('email', 'LIKE', '%@example.com');
+ }
+}
+
+?>
+-----
+where('status', 'active')->get();
+
+ $query = User::query()
+ ->where('name', 'John')->whereKey($user)
+ ->where('email', 'LIKE', '%@example.com');
+ }
+}
+
+?>
diff --git a/tests/Rector/MethodCall/EloquentWhereIdToWhereKeyRector/Fixture/fixture.php.inc b/tests/Rector/MethodCall/EloquentWhereIdToWhereKeyRector/Fixture/fixture.php.inc
new file mode 100644
index 000000000..834b20398
--- /dev/null
+++ b/tests/Rector/MethodCall/EloquentWhereIdToWhereKeyRector/Fixture/fixture.php.inc
@@ -0,0 +1,33 @@
+id)->get();
+ User::where('id', '!=', $user->id)->get();
+ }
+}
+
+?>
+-----
+get();
+ User::whereKeyNot($user)->get();
+ }
+}
+
+?>
diff --git a/tests/Rector/MethodCall/EloquentWhereIdToWhereKeyRector/Fixture/method_call_fixture.php.inc b/tests/Rector/MethodCall/EloquentWhereIdToWhereKeyRector/Fixture/method_call_fixture.php.inc
new file mode 100644
index 000000000..17abf4f86
--- /dev/null
+++ b/tests/Rector/MethodCall/EloquentWhereIdToWhereKeyRector/Fixture/method_call_fixture.php.inc
@@ -0,0 +1,39 @@
+where('id', '=', $user->id)->get();
+ $query->where('id', '!=', $user->id)->get();
+ }
+}
+
+?>
+-----
+whereKey($user)->get();
+ $query->whereKeyNot($user)->get();
+ }
+}
+
+?>
diff --git a/tests/Rector/MethodCall/EloquentWhereIdToWhereKeyRector/Fixture/skip_non_id_column.php.inc b/tests/Rector/MethodCall/EloquentWhereIdToWhereKeyRector/Fixture/skip_non_id_column.php.inc
new file mode 100644
index 000000000..12b335e1e
--- /dev/null
+++ b/tests/Rector/MethodCall/EloquentWhereIdToWhereKeyRector/Fixture/skip_non_id_column.php.inc
@@ -0,0 +1,29 @@
+id)->get();
+ User::where('name', $user->id)->get();
+
+ // Should NOT transform - different property
+ User::where('id', '=', $user->name)->get();
+ User::where('id', $user->name)->get();
+
+ // Should NOT transform - different operator
+ User::where('id', '>', $user->id)->get();
+ User::where('id', '<', $user->id)->get();
+
+ // Should NOT transform - not a property fetch
+ User::where('id', '=', 123)->get();
+ User::where('id', 123)->get();
+ }
+}
+
+?>
diff --git a/tests/Rector/MethodCall/EloquentWhereIdToWhereKeyRector/Fixture/two_argument_where.php.inc b/tests/Rector/MethodCall/EloquentWhereIdToWhereKeyRector/Fixture/two_argument_where.php.inc
new file mode 100644
index 000000000..fed14d154
--- /dev/null
+++ b/tests/Rector/MethodCall/EloquentWhereIdToWhereKeyRector/Fixture/two_argument_where.php.inc
@@ -0,0 +1,50 @@
+id)->get();
+
+ // 2-argument whereNot() with id
+ User::whereNot('id', $user->id)->get();
+
+ // Chained with 2-argument
+ $query = User::query()
+ ->where('name', 'John')
+ ->where('id', $user->id)
+ ->get();
+ }
+}
+
+?>
+-----
+get();
+
+ // 2-argument whereNot() with id
+ User::whereKeyNot($user)->get();
+
+ // Chained with 2-argument
+ $query = User::query()
+ ->where('name', 'John')->whereKey($user)
+ ->get();
+ }
+}
+
+?>
diff --git a/tests/Rector/MethodCall/EloquentWhereIdToWhereKeyRector/Fixture/typed_query_builder.php.inc b/tests/Rector/MethodCall/EloquentWhereIdToWhereKeyRector/Fixture/typed_query_builder.php.inc
new file mode 100644
index 000000000..49f122dd2
--- /dev/null
+++ b/tests/Rector/MethodCall/EloquentWhereIdToWhereKeyRector/Fixture/typed_query_builder.php.inc
@@ -0,0 +1,37 @@
+where('id', '=', $user->id)->get();
+ }
+}
+
+?>
+-----
+whereKey($user)->get();
+ }
+}
+
+?>
diff --git a/tests/Rector/MethodCall/EloquentWhereIdToWhereKeyRector/Source/User.php b/tests/Rector/MethodCall/EloquentWhereIdToWhereKeyRector/Source/User.php
new file mode 100644
index 000000000..e9b916735
--- /dev/null
+++ b/tests/Rector/MethodCall/EloquentWhereIdToWhereKeyRector/Source/User.php
@@ -0,0 +1,7 @@
+rule(EloquentWhereIdToWhereKeyRector::class);
+};