Skip to content

Commit c664be4

Browse files
Add EloquentMagicMethodToQueryBuilderRector rule (#132)
* Add EloquentMagicMethodToQueryBuilderRector rule * Fix PHPStan errors
1 parent 2af4076 commit c664be4

File tree

9 files changed

+302
-1
lines changed

9 files changed

+302
-1
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Rector\Config\RectorConfig;
6+
7+
use RectorLaravel\Rector\StaticCall\EloquentMagicMethodToQueryBuilderRector;
8+
9+
return static function (RectorConfig $rectorConfig): void {
10+
$rectorConfig->import(__DIR__ . '/../config.php');
11+
$rectorConfig->rule(EloquentMagicMethodToQueryBuilderRector::class);
12+
};

docs/rector_rules_overview.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# 39 Rules Overview
1+
# 40 Rules Overview
22

33
## AddArgumentDefaultValueRector
44

@@ -458,6 +458,21 @@ Convert DB Expression `__toString()` calls to `getValue()` method calls.
458458

459459
<br>
460460

461+
## EloquentMagicMethodToQueryBuilderRector
462+
463+
Transform certain magic method calls on Eloquent Models into corresponding Query Builder method calls.
464+
465+
- class: [`RectorLaravel\Rector\StaticCall\EloquentMagicMethodToQueryBuilderRector`](../src/Rector/StaticCall/EloquentMagicMethodToQueryBuilderRector.php)
466+
467+
```diff
468+
-User::find(1);
469+
-User::where('email', '[email protected]')->first();
470+
+User::query()->find(1);
471+
+User::query()->where('email', '[email protected]')->first();
472+
```
473+
474+
<br>
475+
461476
## EmptyToBlankAndFilledFuncRector
462477

463478
Replace use of the unsafe `empty()` function with Laravel's safer `blank()` & `filled()` functions.
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace RectorLaravel\Rector\StaticCall;
6+
7+
use Illuminate\Database\Eloquent\Builder as EloquentQueryBuilder;
8+
use Illuminate\Database\Eloquent\Model;
9+
use Illuminate\Database\Query\Builder as QueryBuilder;
10+
use PhpParser\Node;
11+
use PhpParser\Node\Expr\StaticCall;
12+
use PhpParser\Node\Identifier;
13+
use Rector\Core\Rector\AbstractRector;
14+
use ReflectionException;
15+
use ReflectionMethod;
16+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
17+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
18+
19+
/**
20+
* @see \RectorLaravel\Tests\Rector\StaticCall\EloquentMagicMethodToQueryBuilderRector\EloquentMagicMethodToQueryBuilderRectorTest
21+
*/
22+
final class EloquentMagicMethodToQueryBuilderRector extends AbstractRector
23+
{
24+
public function getRuleDefinition(): RuleDefinition
25+
{
26+
return new RuleDefinition('The EloquentMagicMethodToQueryBuilderRule is designed to automatically transform certain magic method calls on Eloquent Models into corresponding Query Builder method calls.', [
27+
new CodeSample(
28+
<<<'CODE_SAMPLE'
29+
use App\Models\User;
30+
31+
$user = User::find(1);
32+
CODE_SAMPLE,
33+
<<<'CODE_SAMPLE'
34+
use App\Models\User;
35+
36+
$user = User::query()->find(1);
37+
CODE_SAMPLE
38+
),
39+
]);
40+
}
41+
42+
/**
43+
* @return array<class-string<Node>>
44+
*/
45+
public function getNodeTypes(): array
46+
{
47+
return [StaticCall::class];
48+
}
49+
50+
/**
51+
* @param StaticCall $node
52+
*/
53+
public function refactor(Node $node): ?Node
54+
{
55+
$resolvedType = $this->nodeTypeResolver->getType($node->class);
56+
57+
// like for variables, example "$namespace"
58+
// @phpstan-ignore-next-line
59+
if (! method_exists($resolvedType, 'getClassName')) {
60+
return null;
61+
}
62+
63+
$className = (string) $resolvedType->getClassName();
64+
$originalClassName = $this->getName($node->class); // like "self" or "App\Models\User"
65+
66+
if (is_null($originalClassName)) {
67+
return null;
68+
}
69+
70+
// does not extend Eloquent Model
71+
if (! is_subclass_of($className, Model::class)) {
72+
return null;
73+
}
74+
75+
if (! $node->name instanceof Identifier) {
76+
return null;
77+
}
78+
79+
$methodName = $node->name->toString();
80+
81+
// if not a magic method
82+
if (! $this->isMagicMethod($className, $methodName)) {
83+
return null;
84+
}
85+
86+
// if method belongs to Eloquent Query Builder or Query Builder
87+
if (! ($this->isPublicMethod(EloquentQueryBuilder::class, $methodName) ||
88+
$this->isPublicMethod(QueryBuilder::class, $methodName)
89+
)) {
90+
return null;
91+
}
92+
93+
$queryMethodCall = $this->nodeFactory->createStaticCall($originalClassName, 'query');
94+
95+
$newNode = $this->nodeFactory->createMethodCall($queryMethodCall, $methodName);
96+
foreach ($node->args as $arg) {
97+
$newNode->args[] = $arg;
98+
}
99+
100+
return $newNode;
101+
}
102+
103+
public function isMagicMethod(string $className, string $methodName): bool
104+
{
105+
try {
106+
$reflectionMethod = new ReflectionMethod($className, $methodName);
107+
} catch (ReflectionException $e) {
108+
return true; // method does not exist => is magic method
109+
}
110+
111+
return false; // not a magic method
112+
}
113+
114+
public function isPublicMethod(string $className, string $methodName): bool
115+
{
116+
try {
117+
$reflectionMethod = new ReflectionMethod($className, $methodName);
118+
119+
// if not public
120+
if (! $reflectionMethod->isPublic()) {
121+
return false;
122+
}
123+
124+
// if static
125+
if ($reflectionMethod->isStatic()) {
126+
return false;
127+
}
128+
} catch (ReflectionException $e) {
129+
return false; // method does not exist => is magic method
130+
}
131+
132+
return true; // method exist
133+
}
134+
}

src/Set/LaravelSetList.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,9 @@ final class LaravelSetList implements SetListInterface
107107
* @var string
108108
*/
109109
final public const LARAVEL_FACADE_ALIASES_TO_FULL_NAMES = __DIR__ . '/../../config/sets/laravel-facade-aliases-to-full-names.php';
110+
111+
/**
112+
* @var string
113+
*/
114+
final public const LARAVEL_ELOQUENT_MAGIC_METHOD_TO_QUERY_BUILDER = __DIR__ . '/../../config/sets/laravel-eloquent-magic-method-to-query-builder.php';
110115
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace Illuminate\Database\Eloquent;
4+
5+
use Illuminate\Database\Query\Builder as QueryBuilder;
6+
7+
if (class_exists('Illuminate\Database\Eloquent\Builder')) {
8+
return;
9+
}
10+
11+
class Builder extends QueryBuilder
12+
{
13+
public function publicMethodBelongsToEloquentQueryBuilder(): void
14+
{
15+
}
16+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace Illuminate\Database\Query;
4+
5+
if (class_exists('Illuminate\Database\Query\Builder')) {
6+
return;
7+
}
8+
9+
class Builder
10+
{
11+
public function publicMethodBelongsToQueryBuilder(): void
12+
{
13+
}
14+
15+
protected function protectedMethodBelongsToQueryBuilder(): void
16+
{
17+
}
18+
19+
private function privateMethodBelongsToQueryBuilder(): void
20+
{
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace RectorLaravel\Tests\Rector\StaticCall\EloquentMagicMethodToQueryBuilderRector;
6+
7+
use Illuminate\Database\Eloquent\Model;
8+
use Iterator;
9+
use PHPUnit\Framework\Attributes\DataProvider;
10+
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
11+
12+
final class User extends Model
13+
{
14+
public static function staticMethodBelongsToModel(): void
15+
{
16+
}
17+
}
18+
19+
final class EloquentMagicMethodToQueryBuilderRectorTest extends AbstractRectorTestCase
20+
{
21+
#[DataProvider('provideData')]
22+
public function test(string $filePath): void
23+
{
24+
$this->doTestFile($filePath);
25+
}
26+
27+
public static function provideData(): Iterator
28+
{
29+
return self::yieldFilesFromDirectory(__DIR__ . '/Fixture');
30+
}
31+
32+
public function provideConfigFilePath(): string
33+
{
34+
return __DIR__ . '/config/configured_rule.php';
35+
}
36+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
namespace RectorLaravel\Tests\Rector\StaticCall\EloquentMagicMethodToQueryBuilderRector\Fixture;
4+
5+
use RectorLaravel\Tests\Rector\StaticCall\EloquentMagicMethodToQueryBuilderRector\User;
6+
7+
class SomeController
8+
{
9+
public function getUser()
10+
{
11+
# eligible
12+
$user = User::publicMethodBelongsToEloquentQueryBuilder(1)->where('xxx', 'xxx')->first();
13+
$user = User::publicMethodBelongsToQueryBuilder(1);
14+
15+
# not eligible
16+
$user = User::privateMethodBelongsToQueryBuilder(1);
17+
$user = User::protectedMethodBelongsToQueryBuilder(1);
18+
$user = User::publicMethodNotBelongsToQueryBuilder(1);
19+
$user = User::query()->publicMethodBelongsToEloquentQueryBuilder(1);
20+
$user = User::query()->publicMethodBelongsToQueryBuilder(1);
21+
$user = User::staticMethodBelongsToModel(1);
22+
}
23+
}
24+
-----
25+
<?php
26+
27+
namespace RectorLaravel\Tests\Rector\StaticCall\EloquentMagicMethodToQueryBuilderRector\Fixture;
28+
29+
use RectorLaravel\Tests\Rector\StaticCall\EloquentMagicMethodToQueryBuilderRector\User;
30+
31+
class SomeController
32+
{
33+
public function getUser()
34+
{
35+
# eligible
36+
$user = User::query()->publicMethodBelongsToEloquentQueryBuilder(1)->where('xxx', 'xxx')->first();
37+
$user = User::query()->publicMethodBelongsToQueryBuilder(1);
38+
39+
# not eligible
40+
$user = User::privateMethodBelongsToQueryBuilder(1);
41+
$user = User::protectedMethodBelongsToQueryBuilder(1);
42+
$user = User::publicMethodNotBelongsToQueryBuilder(1);
43+
$user = User::query()->publicMethodBelongsToEloquentQueryBuilder(1);
44+
$user = User::query()->publicMethodBelongsToQueryBuilder(1);
45+
$user = User::staticMethodBelongsToModel(1);
46+
}
47+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Rector\Config\RectorConfig;
6+
7+
use RectorLaravel\Rector\StaticCall\EloquentMagicMethodToQueryBuilderRector;
8+
9+
return static function (RectorConfig $rectorConfig): void {
10+
$rectorConfig->import(__DIR__ . '/../../../../../config/config.php');
11+
$rectorConfig->importNames(importDocBlockNames: false);
12+
$rectorConfig->importShortClasses(false);
13+
$rectorConfig->rule(EloquentMagicMethodToQueryBuilderRector::class);
14+
};

0 commit comments

Comments
 (0)