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