diff --git a/docs/rector_rules_overview.md b/docs/rector_rules_overview.md index 76deac53..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 @@ -460,15 +460,35 @@ Convert DB Expression `__toString()` calls to `getValue()` method calls. ## EloquentMagicMethodToQueryBuilderRector -Transform certain magic method calls on Eloquent Models into corresponding Query Builder method calls. +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 --User::find(1); --User::where('email', 'test@test.com')->first(); -+User::query()->find(1); -+User::query()->where('email', 'test@test.com')->first(); + 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` + +- class: [`RectorLaravel\Rector\MethodCall\EloquentWhereRelationTypeHintingParameterRector`](../src/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector.php) + +```diff +-User::whereHas('posts', function ($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\Query\Builder $query) { + $query->where('is_published', true); + }); ```
diff --git a/src/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector.php b/src/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector.php new file mode 100644 index 00000000..4087d00c --- /dev/null +++ b/src/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector.php @@ -0,0 +1,142 @@ +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\Query\Builder $query) { + $query->where('is_published', true); +}); + +$query->whereHas('posts', function (\Illuminate\Contracts\Database\Query\Builder $query) { + $query->where('is_published', true); +}); +CODE_SAMPLE + ), + ] + ); + } + + public function getNodeTypes(): array + { + return [Node\Expr\MethodCall::class, Node\Expr\StaticCall::class]; + } + + public function refactor(Node $node): ?Node + { + if (! $node instanceof Node\Expr\MethodCall && ! $node instanceof Node\Expr\StaticCall) { + return null; + } + + if ($this->isWhereRelationMethodWithClosureOrArrowFunction($node)) { + $this->changeClosureParamType($node); + + return $node; + } + + return null; + } + + private function isWhereRelationMethodWithClosureOrArrowFunction( + Node\Expr\MethodCall|Node\Expr\StaticCall $node + ): bool { + if (! $this->expectedObjectTypeAndMethodCall($node)) { + 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()[$position]->value ?? null) instanceof Node\Expr\Closure && + ! ($node->getArgs()[$position]->value ?? null) instanceof Node\Expr\ArrowFunction + ) { + return false; + } + + return true; + } + + 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()[$position] +->value; + + if (! isset($closure->getParams()[0])) { + return; + } + + $param = $closure->getParams()[0]; + + if ($param->type instanceof Node\Name) { + return; + } + + $param->type = new Node\Name\FullyQualified('Illuminate\Contracts\Database\Query\Builder'); + } + + private function expectedObjectTypeAndMethodCall(Node\Expr\MethodCall|Node\Expr\StaticCall $node): bool + { + 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', + '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..bf809532 --- /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\Query\Builder $query) { + $query->where('is_published', true); +}); + +$query->orWhereHas('posts', function (\Illuminate\Contracts\Database\Query\Builder $query) { + $query->where('is_published', true); +}); + +$query->whereDoesntHave('posts', function (\Illuminate\Contracts\Database\Query\Builder $query) { + $query->where('is_published', true); +}); + +$query->orWhereDoesntHave('posts', function (\Illuminate\Contracts\Database\Query\Builder $query) { + $query->where('is_published', true); +}); + +$query->whereHasMorph('posts', [], function (\Illuminate\Contracts\Database\Query\Builder $query) { + $query->where('is_published', true); +}); + +$query->orWhereHasMorph('posts', [], function (\Illuminate\Contracts\Database\Query\Builder $query) { + $query->where('is_published', true); +}); + +$query->whereDoesntHaveMorph('posts', [], function (\Illuminate\Contracts\Database\Query\Builder $query) { + $query->where('is_published', true); +}); + +$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 new file mode 100644 index 00000000..f784bcd3 --- /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\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 new file mode 100644 index 00000000..6512cfe9 --- /dev/null +++ b/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture3.php.inc @@ -0,0 +1,29 @@ +where('is_published', true); +}); + +?> +----- +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); + +?> 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); +};