Skip to content

Commit 0e5b9d9

Browse files
authored
New Rule to use the whereLike clause in Laravel 11.x (#267)
* Add new rule WhereToWhereLikeRector * Add more cases to the WhereToWhereLikeRector rule * Make the WhereToWhereLikeRector rule configurable for Postgres * Update docs and fix errors * Update to Rector 2.0 * Expand rule to use a contract for the Query Builder * Expand to cover StaticCalls in the rule * Update docs
1 parent 11bff1c commit 0e5b9d9

File tree

13 files changed

+552
-2
lines changed

13 files changed

+552
-2
lines changed

docs/rector_rules_overview.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# 72 Rules Overview
1+
# 73 Rules Overview
22

33
## AbortIfRector
44

@@ -1435,3 +1435,35 @@ Convert string validation rules into arrays for Laravel's Validator.
14351435
```
14361436

14371437
<br>
1438+
1439+
## WhereToWhereLikeRector
1440+
1441+
Changes `where` method and static calls to `whereLike` calls in the Eloquent & Query Builder.
1442+
1443+
Can be configured for the Postgres driver with `[WhereToWhereLikeRector::USING_POSTGRES_DRIVER => true]`.
1444+
1445+
:wrench: **configure it!**
1446+
1447+
- class: [`RectorLaravel\Rector\MethodCall\WhereToWhereLikeRector`](../src/Rector/MethodCall/WhereToWhereLikeRector.php)
1448+
1449+
```diff
1450+
-$query->where('name', 'like', 'Rector');
1451+
-$query->orWhere('name', 'like', 'Rector');
1452+
-$query->where('name', 'like binary', 'Rector');
1453+
+$query->whereLike('name', 'Rector');
1454+
+$query->orWhereLike('name', 'Rector');
1455+
+$query->whereLike('name', 'Rector', true);
1456+
```
1457+
1458+
<br>
1459+
1460+
```diff
1461+
-$query->where('name', 'ilike', 'Rector');
1462+
-$query->orWhere('name', 'ilike', 'Rector');
1463+
-$query->where('name', 'like', 'Rector');
1464+
+$query->whereLike('name', 'Rector');
1465+
+$query->orWhereLike('name', 'Rector');
1466+
+$query->whereLike('name', 'Rector', true);
1467+
```
1468+
1469+
<br>
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace RectorLaravel\Rector\MethodCall;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Arg;
9+
use PhpParser\Node\Expr\ConstFetch;
10+
use PhpParser\Node\Expr\MethodCall;
11+
use PhpParser\Node\Expr\StaticCall;
12+
use PhpParser\Node\Identifier;
13+
use PhpParser\Node\Name;
14+
use PhpParser\Node\Scalar\String_;
15+
use PHPStan\Type\ObjectType;
16+
use Rector\Contract\Rector\ConfigurableRectorInterface;
17+
use RectorLaravel\AbstractRector;
18+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\ConfiguredCodeSample;
19+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
20+
use Webmozart\Assert\Assert;
21+
22+
/**
23+
* @see https://github.com/laravel/framework/pull/52147
24+
* @see \RectorLaravel\Tests\Rector\MethodCall\WhereToWhereLikeRector\WhereToWhereLikeRectorTest
25+
* @see \RectorLaravel\Tests\Rector\MethodCall\WhereToWhereLikeRector\WhereToWhereLikeRectorPostgresTest
26+
*/
27+
final class WhereToWhereLikeRector extends AbstractRector implements ConfigurableRectorInterface
28+
{
29+
public const string USING_POSTGRES_DRIVER = 'usingPostgresDriver';
30+
31+
private const array WHERE_LIKE_METHODS = [
32+
'where' => 'whereLike',
33+
'orwhere' => 'orWhereLike',
34+
];
35+
36+
private bool $usingPostgresDriver = false;
37+
38+
public function getRuleDefinition(): RuleDefinition
39+
{
40+
$description = "Changes `where` method and static calls to `whereLike` calls in the Eloquent & Query Builder.\n\n"
41+
. 'Can be configured for the Postgres driver with `[WhereToWhereLikeRector::USING_POSTGRES_DRIVER => true]`.';
42+
43+
return new RuleDefinition(
44+
$description, [
45+
new ConfiguredCodeSample(
46+
<<<'CODE_SAMPLE'
47+
$query->where('name', 'like', 'Rector');
48+
$query->orWhere('name', 'like', 'Rector');
49+
$query->where('name', 'like binary', 'Rector');
50+
CODE_SAMPLE
51+
,
52+
<<<'CODE_SAMPLE'
53+
$query->whereLike('name', 'Rector');
54+
$query->orWhereLike('name', 'Rector');
55+
$query->whereLike('name', 'Rector', true);
56+
CODE_SAMPLE
57+
,
58+
[WhereToWhereLikeRector::USING_POSTGRES_DRIVER => false]
59+
),
60+
new ConfiguredCodeSample(
61+
<<<'CODE_SAMPLE'
62+
$query->where('name', 'ilike', 'Rector');
63+
$query->orWhere('name', 'ilike', 'Rector');
64+
$query->where('name', 'like', 'Rector');
65+
CODE_SAMPLE
66+
,
67+
<<<'CODE_SAMPLE'
68+
$query->whereLike('name', 'Rector');
69+
$query->orWhereLike('name', 'Rector');
70+
$query->whereLike('name', 'Rector', true);
71+
CODE_SAMPLE
72+
,
73+
[WhereToWhereLikeRector::USING_POSTGRES_DRIVER => true]
74+
),
75+
]);
76+
}
77+
78+
/**
79+
* @return array<class-string<Node>>
80+
*/
81+
public function getNodeTypes(): array
82+
{
83+
return [MethodCall::class, StaticCall::class];
84+
}
85+
86+
/**
87+
* @param MethodCall|StaticCall $node
88+
*/
89+
public function refactor(Node $node): ?Node
90+
{
91+
if ($node instanceof StaticCall &&
92+
! $this->isObjectType($node->class, new ObjectType('Illuminate\Database\Eloquent\Model'))) {
93+
return null;
94+
}
95+
96+
if ($node instanceof MethodCall &&
97+
! $this->isObjectType($node->var, new ObjectType('Illuminate\Contracts\Database\Query\Builder'))) {
98+
return null;
99+
}
100+
101+
if (! in_array($this->getLowercaseCallName($node), array_keys(self::WHERE_LIKE_METHODS), true)) {
102+
return null;
103+
}
104+
105+
if (count($node->getArgs()) !== 3) {
106+
return null;
107+
}
108+
109+
$likeParameter = $this->getLikeParameterUsedInQuery($node);
110+
111+
if (! in_array($likeParameter, ['like', 'like binary', 'ilike', 'not like', 'not like binary', 'not ilike'], true)) {
112+
return null;
113+
}
114+
115+
$this->setNewNodeName($node, $likeParameter);
116+
117+
$this->setCaseSensitivity($node, $likeParameter);
118+
119+
// Remove the second argument (the 'like' operator)
120+
unset($node->args[1]);
121+
122+
return $node;
123+
}
124+
125+
public function configure(array $configuration): void
126+
{
127+
if ($configuration === []) {
128+
$this->usingPostgresDriver = false;
129+
130+
return;
131+
}
132+
133+
Assert::keyExists($configuration, self::USING_POSTGRES_DRIVER);
134+
Assert::boolean($configuration[self::USING_POSTGRES_DRIVER]);
135+
$this->usingPostgresDriver = $configuration[self::USING_POSTGRES_DRIVER];
136+
}
137+
138+
private function getLikeParameterUsedInQuery(MethodCall|StaticCall $call): ?string
139+
{
140+
if (! $call->args[1] instanceof Arg) {
141+
return null;
142+
}
143+
144+
if (! $call->args[1]->value instanceof String_) {
145+
return null;
146+
}
147+
148+
return strtolower($call->args[1]->value->value);
149+
}
150+
151+
private function setNewNodeName(MethodCall|StaticCall $call, string $likeParameter): void
152+
{
153+
$newNodeName = self::WHERE_LIKE_METHODS[$this->getLowercaseCallName($call)];
154+
155+
if (str_contains($likeParameter, 'not')) {
156+
$newNodeName = str_replace('Like', 'NotLike', $newNodeName);
157+
}
158+
159+
$call->name = new Identifier($newNodeName);
160+
}
161+
162+
private function setCaseSensitivity(MethodCall|StaticCall $call, string $likeParameter): void
163+
{
164+
// Case sensitive query in MySQL
165+
if (in_array($likeParameter, ['like binary', 'not like binary'], true)) {
166+
$call->args[] = $this->getCaseSensitivityArgument($call);
167+
}
168+
169+
// Case sensitive query in Postgres
170+
if ($this->usingPostgresDriver && in_array($likeParameter, ['like', 'not like'], true)) {
171+
$call->args[] = $this->getCaseSensitivityArgument($call);
172+
}
173+
}
174+
175+
private function getCaseSensitivityArgument(MethodCall|StaticCall $call): Arg
176+
{
177+
if ($call->args[2] instanceof Arg && $call->args[2]->name instanceof Identifier) {
178+
return new Arg(
179+
new ConstFetch(new Name('true')),
180+
name: new Identifier('caseSensitive')
181+
);
182+
}
183+
184+
return new Arg(new ConstFetch(new Name('true')));
185+
}
186+
187+
private function getLowercaseCallName(MethodCall|StaticCall $call): string
188+
{
189+
return strtolower((string) $this->getName($call->name));
190+
}
191+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace Illuminate\Contracts\Database\Query;
4+
5+
if (interface_exists('Illuminate\Contracts\Database\Query\Builder')) {
6+
return;
7+
}
8+
9+
/**
10+
* @mixin \Illuminate\Database\Query\Builder
11+
*/
12+
interface Builder {}

stubs/Illuminate/Database/Query/Builder.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
return;
77
}
88

9-
class Builder
9+
class Builder implements \Illuminate\Contracts\Database\Query\Builder
1010
{
1111
public function publicMethodBelongsToQueryBuilder(): void {}
1212

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
namespace RectorLaravel\Tests\Rector\MethodCall\WhereToWhereLikeRector\Fixture\Default;
4+
5+
use Illuminate\Contracts\Database\Query\Builder;
6+
7+
class Fixture
8+
{
9+
public function run(Builder $query, NonModel $nonQuery)
10+
{
11+
$query->where('name', 'like', 'Rector');
12+
$query->orWhere('name', 'like', 'Rector');
13+
$query->orwhere('name', 'LIKE', 'Rector');
14+
$query->where('name', 'not like', 'Rector');
15+
$query->orwhere('name', 'not like', 'Rector');
16+
$query->orwhere('name', like: 'not like', value: 'Rector');
17+
18+
// Case Sensitivity
19+
$query->where('name', 'like binary', 'Rector');
20+
$query->where('name', 'not like binary', 'Rector');
21+
$query->where('name', like: 'like binary', value: 'Rector');
22+
23+
// Invalid
24+
$nonQuery->where('name', 'like', 'Rector');
25+
}
26+
}
27+
?>
28+
-----
29+
<?php
30+
31+
namespace RectorLaravel\Tests\Rector\MethodCall\WhereToWhereLikeRector\Fixture\Default;
32+
33+
use Illuminate\Contracts\Database\Query\Builder;
34+
35+
class Fixture
36+
{
37+
public function run(Builder $query, NonModel $nonQuery)
38+
{
39+
$query->whereLike('name', 'Rector');
40+
$query->orWhereLike('name', 'Rector');
41+
$query->orWhereLike('name', 'Rector');
42+
$query->whereNotLike('name', 'Rector');
43+
$query->orWhereNotLike('name', 'Rector');
44+
$query->orWhereNotLike('name', value: 'Rector');
45+
46+
// Case Sensitivity
47+
$query->whereLike('name', 'Rector', true);
48+
$query->whereNotLike('name', 'Rector', true);
49+
$query->whereLike('name', value: 'Rector', caseSensitive: true);
50+
51+
// Invalid
52+
$nonQuery->where('name', 'like', 'Rector');
53+
}
54+
}
55+
?>
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
namespace RectorLaravel\Tests\Rector\MethodCall\WhereToWhereLikeRector\Fixture\Default;
4+
5+
use RectorLaravel\Tests\Rector\MethodCall\WhereToWhereLikeRector\Source\Post;
6+
7+
class WithStaticCalls
8+
{
9+
public function run()
10+
{
11+
Post::where('name', 'like', 'Rector');
12+
Post::orWhere('name', 'like', 'Rector');
13+
Post::orwhere('name', 'LIKE', 'Rector');
14+
Post::where('name', 'not like', 'Rector');
15+
Post::orwhere('name', 'not like', 'Rector');
16+
Post::orwhere('name', like: 'not like', value: 'Rector');
17+
18+
// Case Sensitivity
19+
Post::where('name', 'like binary', 'Rector');
20+
Post::where('name', 'not like binary', 'Rector');
21+
Post::where('name', like: 'like binary', value: 'Rector');
22+
23+
// Invalid
24+
NonModel::where('name', 'like', 'Rector');
25+
}
26+
}
27+
?>
28+
-----
29+
<?php
30+
31+
namespace RectorLaravel\Tests\Rector\MethodCall\WhereToWhereLikeRector\Fixture\Default;
32+
33+
use RectorLaravel\Tests\Rector\MethodCall\WhereToWhereLikeRector\Source\Post;
34+
35+
class WithStaticCalls
36+
{
37+
public function run()
38+
{
39+
Post::whereLike('name', 'Rector');
40+
Post::orWhereLike('name', 'Rector');
41+
Post::orWhereLike('name', 'Rector');
42+
Post::whereNotLike('name', 'Rector');
43+
Post::orWhereNotLike('name', 'Rector');
44+
Post::orWhereNotLike('name', value: 'Rector');
45+
46+
// Case Sensitivity
47+
Post::whereLike('name', 'Rector', true);
48+
Post::whereNotLike('name', 'Rector', true);
49+
Post::whereLike('name', value: 'Rector', caseSensitive: true);
50+
51+
// Invalid
52+
NonModel::where('name', 'like', 'Rector');
53+
}
54+
}
55+
?>

0 commit comments

Comments
 (0)