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); +};