From 8288d2d6b4a909d5b8eecd6207c1b5fa5f0dc064 Mon Sep 17 00:00:00 2001 From: Peter Fox Date: Sun, 17 Sep 2023 16:51:17 +0100 Subject: [PATCH 1/6] Adds EloquentWhereRelationTypeHintingParameterRector rule --- docs/rector_rules_overview.md | 22 ++- ...hereRelationTypeHintingParameterRector.php | 139 ++++++++++++++++++ ...RelationTypeHintingParameterRectorTest.php | 28 ++++ .../Fixture/fixture.php.inc | 77 ++++++++++ .../Fixture/fixture2.php.inc | 21 +++ .../Fixture/fixture3.php.inc | 31 ++++ .../Fixture/skip_non_query_object.php.inc | 13 ++ .../config/configured_rule.php | 12 ++ 8 files changed, 342 insertions(+), 1 deletion(-) create mode 100644 src/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector.php create mode 100644 tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/EloquentWhereRelationTypeHintingParameterRectorTest.php create mode 100644 tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture.php.inc create mode 100644 tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture2.php.inc create mode 100644 tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture3.php.inc create mode 100644 tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/skip_non_query_object.php.inc create mode 100644 tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/config/configured_rule.php diff --git a/docs/rector_rules_overview.md b/docs/rector_rules_overview.md index bd84a5e0..464d985f 100644 --- a/docs/rector_rules_overview.md +++ b/docs/rector_rules_overview.md @@ -1,4 +1,4 @@ -# 39 Rules Overview +# 40 Rules Overview ## AddArgumentDefaultValueRector @@ -458,6 +458,26 @@ Convert DB Expression `__toString()` calls to `getValue()` method calls.
+## EloquentWhereRelationTypeHintingParameterRector + +Add type hinting to where relation has methods e.g. whereHas, orWhereHas, whereDoesntHave, orWhereDoesntHave, whereHasMorph, orWhereHasMorph, whereDoesntHaveMorph, orWhereDoesntHaveMorph + +- class: [`RectorLaravel\Rector\MethodCall\EloquentWhereRelationTypeHintingParameterRector`](../src/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector.php) + +```diff +-User::whereHas('posts', function ($query) { ++User::whereHas('posts', function (\Illuminate\Contracts\Database\Eloquent\Builder $query) { + $query->where('is_published', true); + }); + +-$query->whereHas('posts', function ($query) { ++$query->whereHas('posts', function (\Illuminate\Contracts\Database\Eloquent\Builder $query) { + $query->where('is_published', true); + }); +``` + +
+ ## EmptyToBlankAndFilledFuncRector Replace use of the unsafe `empty()` function with Laravel's safer `blank()` & `filled()` functions. diff --git a/src/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector.php b/src/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector.php new file mode 100644 index 00000000..8efd820a --- /dev/null +++ b/src/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector.php @@ -0,0 +1,139 @@ +where('is_published', true); +}); + +$query->whereHas('posts', function ($query) { + $query->where('is_published', true); +}); +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +User::whereHas('posts', function (\Illuminate\Contracts\Database\Eloquent\Builder $query) { + $query->where('is_published', true); +}); + +$query->whereHas('posts', function (\Illuminate\Contracts\Database\Eloquent\Builder $query) { + $query->where('is_published', true); +}); +CODE_SAMPLE + ), + ] + ); + } + + public function getNodeTypes(): array + { + return [Node\Expr\MethodCall::class, Node\Expr\StaticCall::class]; + } + + /** + * @param Node\Expr\MethodCall|Node\Expr\StaticCall $node + * @return Node\Expr\MethodCall|Node\Expr\StaticCall|null + */ + public function refactor(Node $node): ?Node + { + if ($this->isWhereHasClosureOrArrowFunction($node)) { + $this->changeClosureParamType($node); + } + + return $node; + } + + private function isWhereHasClosureOrArrowFunction(Node $node): bool + { + if (! $this->expectedObjectTypeAndMethodCall($node)) { + return false; + } + + if (! isset($node->args[1])) { + return false; + } + + if (! $node->args[1]->value instanceof Node\Expr\Closure && ! $node->args[1]->value instanceof Node\Expr\ArrowFunction) { + return false; + } + + return true; + } + + /** + * @param Node\Expr\MethodCall|Node\Expr\StaticCall $node + */ + private function changeClosureParamType(Node $node): void + { + $closure = $node->getArgs()[1] +->value; + + if (! isset($closure->params[0])) { + return; + } + + $param = $closure->params[0]; + + if ($param->type instanceof Node\Name) { + return; + } + + $param->type = new Node\Name\FullyQualified('Illuminate\Contracts\Database\Eloquent\Builder'); + } + + private function expectedObjectTypeAndMethodCall(Node $node): bool + { + return ($node instanceof Node\Expr\MethodCall && $this->isObjectType( + $node->var, + new ObjectType('Illuminate\Database\Eloquent\Builder') + ) && $this->isNames( + $node->name, + [ + 'whereHas', + 'orWhereHas', + 'whereDoesntHave', + 'orWhereDoesntHave', + 'whereHasMorph', + 'orWhereHasMorph', + 'whereDoesntHaveMorph', + 'orWhereDoesntHaveMorph', + ] + )) || + ($node instanceof Node\Expr\StaticCall && $this->isObjectType( + $node->class, + new ObjectType('Illuminate\Database\Eloquent\Model') + ) && $this->isNames( + $node->name, + [ + 'whereHas', + 'orWhereHas', + 'whereDoesntHave', + 'orWhereDoesntHave', + 'whereHasMorph', + 'orWhereHasMorph', + 'whereDoesntHaveMorph', + 'orWhereDoesntHaveMorph', + ] + )); + } +} diff --git a/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/EloquentWhereRelationTypeHintingParameterRectorTest.php b/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/EloquentWhereRelationTypeHintingParameterRectorTest.php new file mode 100644 index 00000000..e2e85038 --- /dev/null +++ b/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/EloquentWhereRelationTypeHintingParameterRectorTest.php @@ -0,0 +1,28 @@ +doTestFile($filePath); + } + + public static function provideData(): Iterator + { + return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture.php.inc b/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture.php.inc new file mode 100644 index 00000000..76b80678 --- /dev/null +++ b/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture.php.inc @@ -0,0 +1,77 @@ +whereHas('posts', function ($query) { + $query->where('is_published', true); +}); + +$query->orWhereHas('posts', function ($query) { + $query->where('is_published', true); +}); + +$query->whereDoesntHave('posts', function ($query) { + $query->where('is_published', true); +}); + +$query->orWhereDoesntHave('posts', function ($query) { + $query->where('is_published', true); +}); + +$query->whereHasMorph('posts', function ($query) { + $query->where('is_published', true); +}); + +$query->orWhereHasMorph('posts', function ($query) { + $query->where('is_published', true); +}); + +$query->whereDoesntHaveMorph('posts', function ($query) { + $query->where('is_published', true); +}); + +$query->orWhereDoesntHaveMorph('posts', function ($query) { + $query->where('is_published', true); +}); + +?> +----- +whereHas('posts', function (\Illuminate\Contracts\Database\Eloquent\Builder $query) { + $query->where('is_published', true); +}); + +$query->orWhereHas('posts', function (\Illuminate\Contracts\Database\Eloquent\Builder $query) { + $query->where('is_published', true); +}); + +$query->whereDoesntHave('posts', function (\Illuminate\Contracts\Database\Eloquent\Builder $query) { + $query->where('is_published', true); +}); + +$query->orWhereDoesntHave('posts', function (\Illuminate\Contracts\Database\Eloquent\Builder $query) { + $query->where('is_published', true); +}); + +$query->whereHasMorph('posts', function (\Illuminate\Contracts\Database\Eloquent\Builder $query) { + $query->where('is_published', true); +}); + +$query->orWhereHasMorph('posts', function (\Illuminate\Contracts\Database\Eloquent\Builder $query) { + $query->where('is_published', true); +}); + +$query->whereDoesntHaveMorph('posts', function (\Illuminate\Contracts\Database\Eloquent\Builder $query) { + $query->where('is_published', true); +}); + +$query->orWhereDoesntHaveMorph('posts', function (\Illuminate\Contracts\Database\Eloquent\Builder $query) { + $query->where('is_published', true); +}); + +?> diff --git a/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture2.php.inc b/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture2.php.inc new file mode 100644 index 00000000..71f822e8 --- /dev/null +++ b/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture2.php.inc @@ -0,0 +1,21 @@ +whereHas('posts', fn ($query) => + $query->where('is_published', true) +); + +?> +----- +whereHas('posts', fn (\Illuminate\Contracts\Database\Eloquent\Builder $query) => + $query->where('is_published', true) +); + +?> diff --git a/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture3.php.inc b/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture3.php.inc new file mode 100644 index 00000000..6c908d75 --- /dev/null +++ b/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture3.php.inc @@ -0,0 +1,31 @@ +where('is_published', true); +}); + +?> +----- +where('is_published', true); +}); + +?> diff --git a/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/skip_non_query_object.php.inc b/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/skip_non_query_object.php.inc new file mode 100644 index 00000000..40b02090 --- /dev/null +++ b/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/skip_non_query_object.php.inc @@ -0,0 +1,13 @@ +whereHas('posts', fn ($query) => + $query->where('is_published', true) +); + +RandomClass::whereHas('posts', fn ($query) => + $query->where('is_published', true) +); + +?> diff --git a/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/config/configured_rule.php b/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/config/configured_rule.php new file mode 100644 index 00000000..648875bb --- /dev/null +++ b/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/config/configured_rule.php @@ -0,0 +1,12 @@ +import(__DIR__ . '/../../../../../config/config.php'); + + $rectorConfig->rule(EloquentWhereRelationTypeHintingParameterRector::class); +}; From dd0e9913708407b3410711a2e150ff04519624a9 Mon Sep 17 00:00:00 2001 From: Peter Fox Date: Sun, 17 Sep 2023 17:30:57 +0100 Subject: [PATCH 2/6] Improvements to code via PHPStan --- ...hereRelationTypeHintingParameterRector.php | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector.php b/src/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector.php index 8efd820a..17148dc0 100644 --- a/src/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector.php +++ b/src/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector.php @@ -50,49 +50,49 @@ public function getNodeTypes(): array return [Node\Expr\MethodCall::class, Node\Expr\StaticCall::class]; } - /** - * @param Node\Expr\MethodCall|Node\Expr\StaticCall $node - * @return Node\Expr\MethodCall|Node\Expr\StaticCall|null - */ public function refactor(Node $node): ?Node { - if ($this->isWhereHasClosureOrArrowFunction($node)) { + if (! $node instanceof Node\Expr\MethodCall && ! $node instanceof Node\Expr\StaticCall) { + return null; + } + + if ($this->isWhereRelationMethodWithClosureOrArrowFunction($node)) { $this->changeClosureParamType($node); + + return $node; } - return $node; + return null; } - private function isWhereHasClosureOrArrowFunction(Node $node): bool - { + private function isWhereRelationMethodWithClosureOrArrowFunction( + Node\Expr\MethodCall|Node\Expr\StaticCall $node + ): bool { if (! $this->expectedObjectTypeAndMethodCall($node)) { return false; } - if (! isset($node->args[1])) { - return false; - } - - if (! $node->args[1]->value instanceof Node\Expr\Closure && ! $node->args[1]->value instanceof Node\Expr\ArrowFunction) { + if ( + ! ($node->getArgs()[1]->value ?? null) instanceof Node\Expr\Closure && + ! ($node->getArgs()[1]->value ?? null) instanceof Node\Expr\ArrowFunction + ) { return false; } return true; } - /** - * @param Node\Expr\MethodCall|Node\Expr\StaticCall $node - */ - private function changeClosureParamType(Node $node): void + private function changeClosureParamType(Node\Expr\MethodCall|Node\Expr\StaticCall $node): void { + /** @var Node\Expr\ArrowFunction|Node\Expr\Closure $closure */ $closure = $node->getArgs()[1] ->value; - if (! isset($closure->params[0])) { + if (! isset($closure->getParams()[0])) { return; } - $param = $closure->params[0]; + $param = $closure->getParams()[0]; if ($param->type instanceof Node\Name) { return; @@ -101,7 +101,7 @@ private function changeClosureParamType(Node $node): void $param->type = new Node\Name\FullyQualified('Illuminate\Contracts\Database\Eloquent\Builder'); } - private function expectedObjectTypeAndMethodCall(Node $node): bool + private function expectedObjectTypeAndMethodCall(Node\Expr\MethodCall|Node\Expr\StaticCall $node): bool { return ($node instanceof Node\Expr\MethodCall && $this->isObjectType( $node->var, From 0bec96efb5c3f7430c11c6d07cbf5987e5c2be61 Mon Sep 17 00:00:00 2001 From: Peter Fox Date: Mon, 18 Sep 2023 09:01:42 +0100 Subject: [PATCH 3/6] Remove redundant line --- .../Fixture/fixture3.php.inc | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture3.php.inc b/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture3.php.inc index 6c908d75..036026fa 100644 --- a/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture3.php.inc +++ b/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture3.php.inc @@ -7,7 +7,6 @@ class User extends \Illuminate\Database\Eloquent\Model } -/** @var \Illuminate\Database\Eloquent\Builder $query */ User::whereHas('posts', function ($query) { $query->where('is_published', true); }); @@ -23,7 +22,6 @@ class User extends \Illuminate\Database\Eloquent\Model } -/** @var \Illuminate\Database\Eloquent\Builder $query */ User::whereHas('posts', function (\Illuminate\Contracts\Database\Eloquent\Builder $query) { $query->where('is_published', true); }); From d7b0e1d425138fa08bb61e72aaaed8eb37e13b7e Mon Sep 17 00:00:00 2001 From: Peter Fox Date: Mon, 18 Sep 2023 12:43:53 +0100 Subject: [PATCH 4/6] Fix issues --- ...hereRelationTypeHintingParameterRector.php | 69 +++++++++++-------- 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/src/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector.php b/src/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector.php index 17148dc0..4308d14e 100644 --- a/src/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector.php +++ b/src/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector.php @@ -18,7 +18,7 @@ class EloquentWhereRelationTypeHintingParameterRector extends AbstractRector public function getRuleDefinition(): RuleDefinition { return new RuleDefinition( - 'Add type hinting to where relation has methods e.g. whereHas, orWhereHas, whereDoesntHave, orWhereDoesntHave, whereHasMorph, orWhereHasMorph, whereDoesntHaveMorph, orWhereDoesntHaveMorph', + 'Add type hinting to where relation has methods e.g. `whereHas`, `orWhereHas`, `whereDoesntHave`, `orWhereDoesntHave`, `whereHasMorph`, `orWhereHasMorph`, `whereDoesntHaveMorph`, `orWhereDoesntHaveMorph`', [ new CodeSample( <<<'CODE_SAMPLE' @@ -32,11 +32,11 @@ public function getRuleDefinition(): RuleDefinition CODE_SAMPLE , <<<'CODE_SAMPLE' -User::whereHas('posts', function (\Illuminate\Contracts\Database\Eloquent\Builder $query) { +User::whereHas('posts', function (\Illuminate\Contracts\Database\Query\Builder $query) { $query->where('is_published', true); }); -$query->whereHas('posts', function (\Illuminate\Contracts\Database\Eloquent\Builder $query) { +$query->whereHas('posts', function (\Illuminate\Contracts\Database\Query\Builder $query) { $query->where('is_published', true); }); CODE_SAMPLE @@ -72,9 +72,20 @@ private function isWhereRelationMethodWithClosureOrArrowFunction( return false; } + // Morph methods have the closure in the 3rd position, others use the 2nd. + $position = $this->isNames( + $node->name, + [ + 'whereHasMorph', + 'orWhereHasMorph', + 'whereDoesntHaveMorph', + 'orWhereDoesntHaveMorph', + ] + ) ? 2 : 1; + if ( - ! ($node->getArgs()[1]->value ?? null) instanceof Node\Expr\Closure && - ! ($node->getArgs()[1]->value ?? null) instanceof Node\Expr\ArrowFunction + ! ($node->getArgs()[$position]->value ?? null) instanceof Node\Expr\Closure && + ! ($node->getArgs()[$position]->value ?? null) instanceof Node\Expr\ArrowFunction ) { return false; } @@ -84,8 +95,19 @@ private function isWhereRelationMethodWithClosureOrArrowFunction( private function changeClosureParamType(Node\Expr\MethodCall|Node\Expr\StaticCall $node): void { + // Morph methods have the closure in the 3rd position, others use the 2nd. + $position = $this->isNames( + $node->name, + [ + 'whereHasMorph', + 'orWhereHasMorph', + 'whereDoesntHaveMorph', + 'orWhereDoesntHaveMorph', + ] + ) ? 2 : 1; + /** @var Node\Expr\ArrowFunction|Node\Expr\Closure $closure */ - $closure = $node->getArgs()[1] + $closure = $node->getArgs()[$position] ->value; if (! isset($closure->getParams()[0])) { @@ -98,31 +120,22 @@ private function changeClosureParamType(Node\Expr\MethodCall|Node\Expr\StaticCal return; } - $param->type = new Node\Name\FullyQualified('Illuminate\Contracts\Database\Eloquent\Builder'); + $param->type = new Node\Name\FullyQualified('Illuminate\Contracts\Database\Query\Builder'); } private function expectedObjectTypeAndMethodCall(Node\Expr\MethodCall|Node\Expr\StaticCall $node): bool { - return ($node instanceof Node\Expr\MethodCall && $this->isObjectType( - $node->var, - new ObjectType('Illuminate\Database\Eloquent\Builder') - ) && $this->isNames( - $node->name, - [ - 'whereHas', - 'orWhereHas', - 'whereDoesntHave', - 'orWhereDoesntHave', - 'whereHasMorph', - 'orWhereHasMorph', - 'whereDoesntHaveMorph', - 'orWhereDoesntHaveMorph', - ] - )) || - ($node instanceof Node\Expr\StaticCall && $this->isObjectType( - $node->class, - new ObjectType('Illuminate\Database\Eloquent\Model') - ) && $this->isNames( + return match (true) { + $node instanceof Node\Expr\MethodCall && $this->isObjectType( + $node->var, + new ObjectType('Illuminate\Contracts\Database\Query\Builder') + ) => true, + $node instanceof Node\Expr\StaticCall && $this->isObjectType( + $node->class, + new ObjectType('Illuminate\Database\Eloquent\Model') + ) => true, + default => false, + } && $this->isNames( $node->name, [ 'whereHas', @@ -134,6 +147,6 @@ private function expectedObjectTypeAndMethodCall(Node\Expr\MethodCall|Node\Expr\ 'whereDoesntHaveMorph', 'orWhereDoesntHaveMorph', ] - )); + ); } } From 7fcd6bbb5632a08f6cf26d11389f3045a9e55633 Mon Sep 17 00:00:00 2001 From: Peter Fox Date: Mon, 18 Sep 2023 19:08:27 +0100 Subject: [PATCH 5/6] Fixes and further tests --- docs/rector_rules_overview.md | 6 +- ...hereRelationTypeHintingParameterRector.php | 60 ++++++++----------- .../Fixture/fixture.php.inc | 28 ++++----- .../Fixture/fixture2.php.inc | 6 +- .../Fixture/fixture3.php.inc | 2 +- .../Fixture/skip_non_closure_argument.php.inc | 20 +++++++ 6 files changed, 66 insertions(+), 56 deletions(-) create mode 100644 tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/skip_non_closure_argument.php.inc diff --git a/docs/rector_rules_overview.md b/docs/rector_rules_overview.md index 464d985f..c40f7512 100644 --- a/docs/rector_rules_overview.md +++ b/docs/rector_rules_overview.md @@ -460,18 +460,18 @@ Convert DB Expression `__toString()` calls to `getValue()` method calls. ## EloquentWhereRelationTypeHintingParameterRector -Add type hinting to where relation has methods e.g. whereHas, orWhereHas, whereDoesntHave, orWhereDoesntHave, whereHasMorph, orWhereHasMorph, whereDoesntHaveMorph, orWhereDoesntHaveMorph +Add type hinting to where relation has methods e.g. `whereHas`, `orWhereHas`, `whereDoesntHave`, `orWhereDoesntHave`, `whereHasMorph`, `orWhereHasMorph`, `whereDoesntHaveMorph`, `orWhereDoesntHaveMorph` - class: [`RectorLaravel\Rector\MethodCall\EloquentWhereRelationTypeHintingParameterRector`](../src/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector.php) ```diff -User::whereHas('posts', function ($query) { -+User::whereHas('posts', function (\Illuminate\Contracts\Database\Eloquent\Builder $query) { ++User::whereHas('posts', function (\Illuminate\Contracts\Database\Query\Builder $query) { $query->where('is_published', true); }); -$query->whereHas('posts', function ($query) { -+$query->whereHas('posts', function (\Illuminate\Contracts\Database\Eloquent\Builder $query) { ++$query->whereHas('posts', function (\Illuminate\Contracts\Database\Query\Builder $query) { $query->where('is_published', true); }); ``` diff --git a/src/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector.php b/src/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector.php index 4308d14e..4087d00c 100644 --- a/src/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector.php +++ b/src/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector.php @@ -74,14 +74,9 @@ private function isWhereRelationMethodWithClosureOrArrowFunction( // Morph methods have the closure in the 3rd position, others use the 2nd. $position = $this->isNames( - $node->name, - [ - 'whereHasMorph', - 'orWhereHasMorph', - 'whereDoesntHaveMorph', - 'orWhereDoesntHaveMorph', - ] - ) ? 2 : 1; + $node->name, + ['whereHasMorph', 'orWhereHasMorph', 'whereDoesntHaveMorph', 'orWhereDoesntHaveMorph'] + ) ? 2 : 1; if ( ! ($node->getArgs()[$position]->value ?? null) instanceof Node\Expr\Closure && @@ -97,15 +92,10 @@ private function changeClosureParamType(Node\Expr\MethodCall|Node\Expr\StaticCal { // Morph methods have the closure in the 3rd position, others use the 2nd. $position = $this->isNames( - $node->name, - [ - 'whereHasMorph', - 'orWhereHasMorph', - 'whereDoesntHaveMorph', - 'orWhereDoesntHaveMorph', - ] - ) ? 2 : 1; - + $node->name, + ['whereHasMorph', 'orWhereHasMorph', 'whereDoesntHaveMorph', 'orWhereDoesntHaveMorph'] + ) ? 2 : 1; + /** @var Node\Expr\ArrowFunction|Node\Expr\Closure $closure */ $closure = $node->getArgs()[$position] ->value; @@ -127,26 +117,26 @@ private function expectedObjectTypeAndMethodCall(Node\Expr\MethodCall|Node\Expr\ { return match (true) { $node instanceof Node\Expr\MethodCall && $this->isObjectType( - $node->var, - new ObjectType('Illuminate\Contracts\Database\Query\Builder') - ) => true, + $node->var, + new ObjectType('Illuminate\Contracts\Database\Query\Builder') + ) => true, $node instanceof Node\Expr\StaticCall && $this->isObjectType( - $node->class, - new ObjectType('Illuminate\Database\Eloquent\Model') - ) => true, + $node->class, + new ObjectType('Illuminate\Database\Eloquent\Model') + ) => true, default => false, } && $this->isNames( - $node->name, - [ - 'whereHas', - 'orWhereHas', - 'whereDoesntHave', - 'orWhereDoesntHave', - 'whereHasMorph', - 'orWhereHasMorph', - 'whereDoesntHaveMorph', - 'orWhereDoesntHaveMorph', - ] - ); + $node->name, + [ + 'whereHas', + 'orWhereHas', + 'whereDoesntHave', + 'orWhereDoesntHave', + 'whereHasMorph', + 'orWhereHasMorph', + 'whereDoesntHaveMorph', + 'orWhereDoesntHaveMorph', + ] + ); } } diff --git a/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture.php.inc b/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture.php.inc index 76b80678..bf809532 100644 --- a/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture.php.inc +++ b/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture.php.inc @@ -2,7 +2,7 @@ namespace RectorLaravel\Tests\Rector\MethodCall\EloquentWhereRelationTypeHintingParameterRector\Fixture; -/** @var \Illuminate\Database\Eloquent\Builder $query */ +/** @var \Illuminate\Contracts\Database\Query\Builder $query */ $query->whereHas('posts', function ($query) { $query->where('is_published', true); }); @@ -19,19 +19,19 @@ $query->orWhereDoesntHave('posts', function ($query) { $query->where('is_published', true); }); -$query->whereHasMorph('posts', function ($query) { +$query->whereHasMorph('posts', [], function ($query) { $query->where('is_published', true); }); -$query->orWhereHasMorph('posts', function ($query) { +$query->orWhereHasMorph('posts', [], function ($query) { $query->where('is_published', true); }); -$query->whereDoesntHaveMorph('posts', function ($query) { +$query->whereDoesntHaveMorph('posts', [], function ($query) { $query->where('is_published', true); }); -$query->orWhereDoesntHaveMorph('posts', function ($query) { +$query->orWhereDoesntHaveMorph('posts', [], function ($query) { $query->where('is_published', true); }); @@ -41,36 +41,36 @@ $query->orWhereDoesntHaveMorph('posts', function ($query) { namespace RectorLaravel\Tests\Rector\MethodCall\EloquentWhereRelationTypeHintingParameterRector\Fixture; -/** @var \Illuminate\Database\Eloquent\Builder $query */ -$query->whereHas('posts', function (\Illuminate\Contracts\Database\Eloquent\Builder $query) { +/** @var \Illuminate\Contracts\Database\Query\Builder $query */ +$query->whereHas('posts', function (\Illuminate\Contracts\Database\Query\Builder $query) { $query->where('is_published', true); }); -$query->orWhereHas('posts', function (\Illuminate\Contracts\Database\Eloquent\Builder $query) { +$query->orWhereHas('posts', function (\Illuminate\Contracts\Database\Query\Builder $query) { $query->where('is_published', true); }); -$query->whereDoesntHave('posts', function (\Illuminate\Contracts\Database\Eloquent\Builder $query) { +$query->whereDoesntHave('posts', function (\Illuminate\Contracts\Database\Query\Builder $query) { $query->where('is_published', true); }); -$query->orWhereDoesntHave('posts', function (\Illuminate\Contracts\Database\Eloquent\Builder $query) { +$query->orWhereDoesntHave('posts', function (\Illuminate\Contracts\Database\Query\Builder $query) { $query->where('is_published', true); }); -$query->whereHasMorph('posts', function (\Illuminate\Contracts\Database\Eloquent\Builder $query) { +$query->whereHasMorph('posts', [], function (\Illuminate\Contracts\Database\Query\Builder $query) { $query->where('is_published', true); }); -$query->orWhereHasMorph('posts', function (\Illuminate\Contracts\Database\Eloquent\Builder $query) { +$query->orWhereHasMorph('posts', [], function (\Illuminate\Contracts\Database\Query\Builder $query) { $query->where('is_published', true); }); -$query->whereDoesntHaveMorph('posts', function (\Illuminate\Contracts\Database\Eloquent\Builder $query) { +$query->whereDoesntHaveMorph('posts', [], function (\Illuminate\Contracts\Database\Query\Builder $query) { $query->where('is_published', true); }); -$query->orWhereDoesntHaveMorph('posts', function (\Illuminate\Contracts\Database\Eloquent\Builder $query) { +$query->orWhereDoesntHaveMorph('posts', [], function (\Illuminate\Contracts\Database\Query\Builder $query) { $query->where('is_published', true); }); diff --git a/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture2.php.inc b/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture2.php.inc index 71f822e8..f784bcd3 100644 --- a/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture2.php.inc +++ b/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture2.php.inc @@ -2,7 +2,7 @@ namespace RectorLaravel\Tests\Rector\MethodCall\EloquentWhereRelationTypeHintingParameterRector\Fixture; -/** @var \Illuminate\Database\Eloquent\Builder $query */ +/** @var \Illuminate\Contracts\Database\Query\Builder $query */ $query->whereHas('posts', fn ($query) => $query->where('is_published', true) ); @@ -13,8 +13,8 @@ $query->whereHas('posts', fn ($query) => namespace RectorLaravel\Tests\Rector\MethodCall\EloquentWhereRelationTypeHintingParameterRector\Fixture; -/** @var \Illuminate\Database\Eloquent\Builder $query */ -$query->whereHas('posts', fn (\Illuminate\Contracts\Database\Eloquent\Builder $query) => +/** @var \Illuminate\Contracts\Database\Query\Builder $query */ +$query->whereHas('posts', fn (\Illuminate\Contracts\Database\Query\Builder $query) => $query->where('is_published', true) ); diff --git a/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture3.php.inc b/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture3.php.inc index 036026fa..6512cfe9 100644 --- a/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture3.php.inc +++ b/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture3.php.inc @@ -22,7 +22,7 @@ class User extends \Illuminate\Database\Eloquent\Model } -User::whereHas('posts', function (\Illuminate\Contracts\Database\Eloquent\Builder $query) { +User::whereHas('posts', function (\Illuminate\Contracts\Database\Query\Builder $query) { $query->where('is_published', true); }); diff --git a/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/skip_non_closure_argument.php.inc b/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/skip_non_closure_argument.php.inc new file mode 100644 index 00000000..54bacbfb --- /dev/null +++ b/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/skip_non_closure_argument.php.inc @@ -0,0 +1,20 @@ +whereHas('posts'); +$query->whereHas('posts', null); +$query->whereHasMorph('posts', '', null); + +class User extends \Illuminate\Database\Eloquent\Model +{ + +} + +User::whereHas('posts'); +User::whereHas('posts', null); +User::whereHasMorph('posts', ''); +User::whereHasMorph('posts', '', null); + +?> From db6c1cf51e173a82320d4d148081681d0cef7334 Mon Sep 17 00:00:00 2001 From: Peter Fox Date: Mon, 18 Sep 2023 22:00:59 +0100 Subject: [PATCH 6/6] Squashed commit of the following: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit c664be4794f5e7e46699e52ffc882a284cde7938 Author: Clément Birklé Date: Mon Sep 18 22:16:17 2023 +0200 Add EloquentMagicMethodToQueryBuilderRector rule (#132) * Add EloquentMagicMethodToQueryBuilderRector rule * Fix PHPStan errors commit 2af4076fcfb74e9f1443c9e5cf050e7861f20e81 Author: Andrew Miller Date: Tue Sep 19 05:14:23 2023 +0900 Remove MethodCallRename rule from Laravel80 ruleset (#134) It references the commit where the function was "renamed", but the next commit in the laravel/framework repository is re-adding the function. https://github.com/laravel/framework/commit/8e0914e847e8b161a1968a68f9f80003e23136db As `assertExactJson()` and `assertSimilarJson()` have very different expectations, I think this could change could inadvertently alter tests to be less accurate. Co-authored-by: Anthony Clark --- ...eloquent-magic-method-to-query-builder.php | 12 ++ config/sets/laravel80.php | 2 - docs/rector_rules_overview.md | 17 ++- ...loquentMagicMethodToQueryBuilderRector.php | 134 ++++++++++++++++++ src/Set/LaravelSetList.php | 5 + .../Illuminate/Database/Eloquent/Builder.php | 16 +++ stubs/Illuminate/Database/Query/Builder.php | 22 +++ ...entMagicMethodToQueryBuilderRectorTest.php | 36 +++++ .../Fixture/fixture.php.inc | 47 ++++++ .../config/configured_rule.php | 14 ++ 10 files changed, 302 insertions(+), 3 deletions(-) create mode 100644 config/sets/laravel-eloquent-magic-method-to-query-builder.php create mode 100644 src/Rector/StaticCall/EloquentMagicMethodToQueryBuilderRector.php create mode 100644 stubs/Illuminate/Database/Eloquent/Builder.php create mode 100644 stubs/Illuminate/Database/Query/Builder.php create mode 100644 tests/Rector/StaticCall/EloquentMagicMethodToQueryBuilderRector/EloquentMagicMethodToQueryBuilderRectorTest.php create mode 100644 tests/Rector/StaticCall/EloquentMagicMethodToQueryBuilderRector/Fixture/fixture.php.inc create mode 100644 tests/Rector/StaticCall/EloquentMagicMethodToQueryBuilderRector/config/configured_rule.php diff --git a/config/sets/laravel-eloquent-magic-method-to-query-builder.php b/config/sets/laravel-eloquent-magic-method-to-query-builder.php new file mode 100644 index 00000000..2e7010e7 --- /dev/null +++ b/config/sets/laravel-eloquent-magic-method-to-query-builder.php @@ -0,0 +1,12 @@ +import(__DIR__ . '/../config.php'); + $rectorConfig->rule(EloquentMagicMethodToQueryBuilderRector::class); +}; diff --git a/config/sets/laravel80.php b/config/sets/laravel80.php index 5b5075d6..7450bda3 100644 --- a/config/sets/laravel80.php +++ b/config/sets/laravel80.php @@ -60,7 +60,5 @@ new MethodCallRename('Illuminate\Contracts\Queue\ShouldQueue', 'timeoutAt', 'retryUntil'), # https://github.com/laravel/framework/commit/f9374fa5fb0450721fb2f90e96adef9d409b112c new MethodCallRename('Illuminate\Testing\TestResponse', 'decodeResponseJson', 'json'), - # https://github.com/laravel/framework/commit/fd662d4699776a94e7ead2a42e82c340363fc5a6 - new MethodCallRename('Illuminate\Testing\TestResponse', 'assertExactJson', 'assertSimilarJson'), ]); }; diff --git a/docs/rector_rules_overview.md b/docs/rector_rules_overview.md index c40f7512..b1eadece 100644 --- a/docs/rector_rules_overview.md +++ b/docs/rector_rules_overview.md @@ -1,4 +1,4 @@ -# 40 Rules Overview +# 41 Rules Overview ## AddArgumentDefaultValueRector @@ -458,6 +458,21 @@ Convert DB Expression `__toString()` calls to `getValue()` method calls.
+## EloquentMagicMethodToQueryBuilderRector + +The EloquentMagicMethodToQueryBuilderRule is designed to automatically transform certain magic method calls on Eloquent Models into corresponding Query Builder method calls. + +- class: [`RectorLaravel\Rector\StaticCall\EloquentMagicMethodToQueryBuilderRector`](../src/Rector/StaticCall/EloquentMagicMethodToQueryBuilderRector.php) + +```diff + use App\Models\User; + +-$user = User::find(1); ++$user = User::query()->find(1); +``` + +
+ ## EloquentWhereRelationTypeHintingParameterRector Add type hinting to where relation has methods e.g. `whereHas`, `orWhereHas`, `whereDoesntHave`, `orWhereDoesntHave`, `whereHasMorph`, `orWhereHasMorph`, `whereDoesntHaveMorph`, `orWhereDoesntHaveMorph` diff --git a/src/Rector/StaticCall/EloquentMagicMethodToQueryBuilderRector.php b/src/Rector/StaticCall/EloquentMagicMethodToQueryBuilderRector.php new file mode 100644 index 00000000..3f4b3f2e --- /dev/null +++ b/src/Rector/StaticCall/EloquentMagicMethodToQueryBuilderRector.php @@ -0,0 +1,134 @@ +find(1); +CODE_SAMPLE + ), + ]); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [StaticCall::class]; + } + + /** + * @param StaticCall $node + */ + public function refactor(Node $node): ?Node + { + $resolvedType = $this->nodeTypeResolver->getType($node->class); + + // like for variables, example "$namespace" + // @phpstan-ignore-next-line + if (! method_exists($resolvedType, 'getClassName')) { + return null; + } + + $className = (string) $resolvedType->getClassName(); + $originalClassName = $this->getName($node->class); // like "self" or "App\Models\User" + + if (is_null($originalClassName)) { + return null; + } + + // does not extend Eloquent Model + if (! is_subclass_of($className, Model::class)) { + return null; + } + + if (! $node->name instanceof Identifier) { + return null; + } + + $methodName = $node->name->toString(); + + // if not a magic method + if (! $this->isMagicMethod($className, $methodName)) { + return null; + } + + // if method belongs to Eloquent Query Builder or Query Builder + if (! ($this->isPublicMethod(EloquentQueryBuilder::class, $methodName) || + $this->isPublicMethod(QueryBuilder::class, $methodName) + )) { + return null; + } + + $queryMethodCall = $this->nodeFactory->createStaticCall($originalClassName, 'query'); + + $newNode = $this->nodeFactory->createMethodCall($queryMethodCall, $methodName); + foreach ($node->args as $arg) { + $newNode->args[] = $arg; + } + + return $newNode; + } + + public function isMagicMethod(string $className, string $methodName): bool + { + try { + $reflectionMethod = new ReflectionMethod($className, $methodName); + } catch (ReflectionException $e) { + return true; // method does not exist => is magic method + } + + return false; // not a magic method + } + + public function isPublicMethod(string $className, string $methodName): bool + { + try { + $reflectionMethod = new ReflectionMethod($className, $methodName); + + // if not public + if (! $reflectionMethod->isPublic()) { + return false; + } + + // if static + if ($reflectionMethod->isStatic()) { + return false; + } + } catch (ReflectionException $e) { + return false; // method does not exist => is magic method + } + + return true; // method exist + } +} diff --git a/src/Set/LaravelSetList.php b/src/Set/LaravelSetList.php index ad10a1e5..f01c35a5 100644 --- a/src/Set/LaravelSetList.php +++ b/src/Set/LaravelSetList.php @@ -107,4 +107,9 @@ final class LaravelSetList implements SetListInterface * @var string */ final public const LARAVEL_FACADE_ALIASES_TO_FULL_NAMES = __DIR__ . '/../../config/sets/laravel-facade-aliases-to-full-names.php'; + + /** + * @var string + */ + final public const LARAVEL_ELOQUENT_MAGIC_METHOD_TO_QUERY_BUILDER = __DIR__ . '/../../config/sets/laravel-eloquent-magic-method-to-query-builder.php'; } diff --git a/stubs/Illuminate/Database/Eloquent/Builder.php b/stubs/Illuminate/Database/Eloquent/Builder.php new file mode 100644 index 00000000..7169443b --- /dev/null +++ b/stubs/Illuminate/Database/Eloquent/Builder.php @@ -0,0 +1,16 @@ +doTestFile($filePath); + } + + public static function provideData(): Iterator + { + return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/tests/Rector/StaticCall/EloquentMagicMethodToQueryBuilderRector/Fixture/fixture.php.inc b/tests/Rector/StaticCall/EloquentMagicMethodToQueryBuilderRector/Fixture/fixture.php.inc new file mode 100644 index 00000000..cd25aa79 --- /dev/null +++ b/tests/Rector/StaticCall/EloquentMagicMethodToQueryBuilderRector/Fixture/fixture.php.inc @@ -0,0 +1,47 @@ +where('xxx', 'xxx')->first(); + $user = User::publicMethodBelongsToQueryBuilder(1); + + # not eligible + $user = User::privateMethodBelongsToQueryBuilder(1); + $user = User::protectedMethodBelongsToQueryBuilder(1); + $user = User::publicMethodNotBelongsToQueryBuilder(1); + $user = User::query()->publicMethodBelongsToEloquentQueryBuilder(1); + $user = User::query()->publicMethodBelongsToQueryBuilder(1); + $user = User::staticMethodBelongsToModel(1); + } +} +----- +publicMethodBelongsToEloquentQueryBuilder(1)->where('xxx', 'xxx')->first(); + $user = User::query()->publicMethodBelongsToQueryBuilder(1); + + # not eligible + $user = User::privateMethodBelongsToQueryBuilder(1); + $user = User::protectedMethodBelongsToQueryBuilder(1); + $user = User::publicMethodNotBelongsToQueryBuilder(1); + $user = User::query()->publicMethodBelongsToEloquentQueryBuilder(1); + $user = User::query()->publicMethodBelongsToQueryBuilder(1); + $user = User::staticMethodBelongsToModel(1); + } +} diff --git a/tests/Rector/StaticCall/EloquentMagicMethodToQueryBuilderRector/config/configured_rule.php b/tests/Rector/StaticCall/EloquentMagicMethodToQueryBuilderRector/config/configured_rule.php new file mode 100644 index 00000000..6841bc57 --- /dev/null +++ b/tests/Rector/StaticCall/EloquentMagicMethodToQueryBuilderRector/config/configured_rule.php @@ -0,0 +1,14 @@ +import(__DIR__ . '/../../../../../config/config.php'); + $rectorConfig->importNames(importDocBlockNames: false); + $rectorConfig->importShortClasses(false); + $rectorConfig->rule(EloquentMagicMethodToQueryBuilderRector::class); +};