Skip to content

Commit

Permalink
Adds EloquentWhereRelationTypeHintingParameterRector rule
Browse files Browse the repository at this point in the history
  • Loading branch information
peterfox committed Sep 17, 2023
1 parent 1e576f3 commit 8288d2d
Show file tree
Hide file tree
Showing 8 changed files with 342 additions and 1 deletion.
22 changes: 21 additions & 1 deletion docs/rector_rules_overview.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 39 Rules Overview
# 40 Rules Overview

## AddArgumentDefaultValueRector

Expand Down Expand Up @@ -458,6 +458,26 @@ Convert DB Expression `__toString()` calls to `getValue()` method calls.

<br>

## 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);
});
```

<br>

## EmptyToBlankAndFilledFuncRector

Replace use of the unsafe `empty()` function with Laravel's safer `blank()` & `filled()` functions.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
<?php

declare(strict_types=1);

namespace RectorLaravel\Rector\MethodCall;

use PhpParser\Node;
use PHPStan\Type\ObjectType;
use Rector\Core\Rector\AbstractRector;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

/**
* @see \RectorLaravel\Tests\Rector\MethodCall\EloquentWhereRelationTypeHintingParameterRector\EloquentWhereRelationTypeHintingParameterRectorTest
*/
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',
[
new CodeSample(
<<<'CODE_SAMPLE'
User::whereHas('posts', function ($query) {
$query->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',
]
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace RectorLaravel\Tests\Rector\MethodCall\EloquentWhereRelationTypeHintingParameterRector;

use Iterator;
use PHPUnit\Framework\Attributes\DataProvider;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;

final class EloquentWhereRelationTypeHintingParameterRectorTest extends AbstractRectorTestCase
{
#[DataProvider('provideData')]
public function test(string $filePath): void
{
$this->doTestFile($filePath);
}

public static function provideData(): Iterator
{
return self::yieldFilesFromDirectory(__DIR__ . '/Fixture');
}

public function provideConfigFilePath(): string
{
return __DIR__ . '/config/configured_rule.php';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

namespace RectorLaravel\Tests\Rector\MethodCall\EloquentWhereRelationTypeHintingParameterRector\Fixture;

/** @var \Illuminate\Database\Eloquent\Builder $query */
$query->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);
});

?>
-----
<?php

namespace RectorLaravel\Tests\Rector\MethodCall\EloquentWhereRelationTypeHintingParameterRector\Fixture;

/** @var \Illuminate\Database\Eloquent\Builder $query */
$query->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);
});

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace RectorLaravel\Tests\Rector\MethodCall\EloquentWhereRelationTypeHintingParameterRector\Fixture;

/** @var \Illuminate\Database\Eloquent\Builder $query */
$query->whereHas('posts', fn ($query) =>
$query->where('is_published', true)
);

?>
-----
<?php

namespace RectorLaravel\Tests\Rector\MethodCall\EloquentWhereRelationTypeHintingParameterRector\Fixture;

/** @var \Illuminate\Database\Eloquent\Builder $query */
$query->whereHas('posts', fn (\Illuminate\Contracts\Database\Eloquent\Builder $query) =>
$query->where('is_published', true)
);

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace RectorLaravel\Tests\Rector\MethodCall\EloquentWhereRelationTypeHintingParameterRector\Fixture;

class User extends \Illuminate\Database\Eloquent\Model
{

}

/** @var \Illuminate\Database\Eloquent\Builder $query */
User::whereHas('posts', function ($query) {
$query->where('is_published', true);
});

?>
-----
<?php

namespace RectorLaravel\Tests\Rector\MethodCall\EloquentWhereRelationTypeHintingParameterRector\Fixture;

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

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace RectorLaravel\Tests\Rector\MethodCall\EloquentWhereRelationTypeHintingParameterRector\Fixture;

$obj->whereHas('posts', fn ($query) =>
$query->where('is_published', true)
);

RandomClass::whereHas('posts', fn ($query) =>
$query->where('is_published', true)
);

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use RectorLaravel\Rector\MethodCall\EloquentWhereRelationTypeHintingParameterRector;

return static function (RectorConfig $rectorConfig): void {
$rectorConfig->import(__DIR__ . '/../../../../../config/config.php');

$rectorConfig->rule(EloquentWhereRelationTypeHintingParameterRector::class);
};

0 comments on commit 8288d2d

Please sign in to comment.