Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions config/sets/laravel-code-quality.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -52,4 +53,5 @@
$rectorConfig->rule(DispatchToHelperFunctionsRector::class);
$rectorConfig->rule(NotFilledBlankFuncCallToBlankFilledFuncCallRector::class);
$rectorConfig->rule(EloquentOrderByToLatestOrOldestRector::class);
$rectorConfig->rule(EloquentWhereIdToWhereKeyRector::class);
};
26 changes: 25 additions & 1 deletion docs/rector_rules_overview.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 86 Rules Overview
# 87 Rules Overview

## AbortIfRector

Expand Down Expand Up @@ -675,6 +675,30 @@ Changes `orderBy()` to `latest()` or `oldest()`

<br>

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

<br>

```diff
-User::where('id', '!=', $user->id)->get();
-User::whereNot('id', $user->id)->get();
+User::whereKeyNot($user)->get();
+User::whereKeyNot($user)->get();
```

<br>

## EloquentWhereRelationTypeHintingParameterRector

Add type hinting to where relation has methods e.g. `whereHas`, `orWhereHas`, `whereDoesntHave`, `orWhereDoesntHave`, `whereHasMorph`, `orWhereHasMorph`, `whereDoesntHaveMorph`, `orWhereDoesntHaveMorph`
Expand Down
165 changes: 165 additions & 0 deletions src/Rector/MethodCall/EloquentWhereIdToWhereKeyRector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
<?php

declare(strict_types=1);

namespace RectorLaravel\Rector\MethodCall;

use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Identifier;
use PhpParser\Node\Scalar\String_;
use RectorLaravel\AbstractRector;
use RectorLaravel\NodeAnalyzer\QueryBuilderAnalyzer;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

/**
* @see \RectorLaravel\Tests\Rector\MethodCall\EloquentWhereIdToWhereKeyRector\EloquentWhereIdToWhereKeyRectorTest
*/
final class EloquentWhereIdToWhereKeyRector extends AbstractRector
{
public function __construct(
private readonly QueryBuilderAnalyzer $queryBuilderAnalyzer
) {}

public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition(
'Refactor model calls to the primary key using the `whereKey` and `whereKeyNot` methods',
[
new CodeSample(
<<<'CODE_SAMPLE'
User::where('id', '=', $user->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<class-string<Node>>
*/
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;
}
}
16 changes: 16 additions & 0 deletions stubs/Illuminate/Database/Eloquent/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
10 changes: 10 additions & 0 deletions stubs/Illuminate/Database/Eloquent/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions stubs/Illuminate/Database/Query/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace RectorLaravel\Tests\Rector\MethodCall\EloquentWhereIdToWhereKeyRector;

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

final class EloquentWhereIdToWhereKeyRectorTest extends AbstractRectorTestCase
{
public static function provideData(): Iterator
{
return self::yieldFilesFromDirectory(__DIR__ . '/Fixture');
}

/**
* @test
*/
#[DataProvider('provideData')]
public function test(string $filePath): void
{
$this->doTestFile($filePath);
}

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

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

use RectorLaravel\Tests\Rector\MethodCall\EloquentWhereIdToWhereKeyRector\Source\User;

class ChainedMethodsFixture
{
public function run($user)
{
User::where('id', '=', $user->id)->where('status', 'active')->get();

$query = User::query()
->where('name', 'John')
->where('id', '=', $user->id)
->where('email', 'LIKE', '%@example.com');
}
}

?>
-----
<?php

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

use RectorLaravel\Tests\Rector\MethodCall\EloquentWhereIdToWhereKeyRector\Source\User;

class ChainedMethodsFixture
{
public function run($user)
{
User::whereKey($user)->where('status', 'active')->get();

$query = User::query()
->where('name', 'John')->whereKey($user)
->where('email', 'LIKE', '%@example.com');
}
}

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

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

use RectorLaravel\Tests\Rector\MethodCall\EloquentWhereIdToWhereKeyRector\Source\User;

class SomeClass
{
public function run($user)
{
User::where('id', '=', $user->id)->get();
User::where('id', '!=', $user->id)->get();
}
}

?>
-----
<?php

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

use RectorLaravel\Tests\Rector\MethodCall\EloquentWhereIdToWhereKeyRector\Source\User;

class SomeClass
{
public function run($user)
{
User::whereKey($user)->get();
User::whereKeyNot($user)->get();
}
}

?>
Loading