Skip to content
34 changes: 33 additions & 1 deletion docs/rector_rules_overview.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 72 Rules Overview
# 73 Rules Overview

## AbortIfRector

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

<br>

## WhereToWhereLikeRector

Changes `where` method and static calls to `whereLike` calls in the Eloquent & Query Builder.

Can be configured for the Postgres driver with `[WhereToWhereLikeRector::USING_POSTGRES_DRIVER => true]`.

:wrench: **configure it!**

- class: [`RectorLaravel\Rector\MethodCall\WhereToWhereLikeRector`](../src/Rector/MethodCall/WhereToWhereLikeRector.php)

```diff
-$query->where('name', 'like', 'Rector');
-$query->orWhere('name', 'like', 'Rector');
-$query->where('name', 'like binary', 'Rector');
+$query->whereLike('name', 'Rector');
+$query->orWhereLike('name', 'Rector');
+$query->whereLike('name', 'Rector', true);
```

<br>

```diff
-$query->where('name', 'ilike', 'Rector');
-$query->orWhere('name', 'ilike', 'Rector');
-$query->where('name', 'like', 'Rector');
+$query->whereLike('name', 'Rector');
+$query->orWhereLike('name', 'Rector');
+$query->whereLike('name', 'Rector', true);
```

<br>
191 changes: 191 additions & 0 deletions src/Rector/MethodCall/WhereToWhereLikeRector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
<?php

declare(strict_types=1);

namespace RectorLaravel\Rector\MethodCall;

use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\String_;
use PHPStan\Type\ObjectType;
use Rector\Contract\Rector\ConfigurableRectorInterface;
use RectorLaravel\AbstractRector;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\ConfiguredCodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
use Webmozart\Assert\Assert;

/**
* @see https://github.com/laravel/framework/pull/52147
* @see \RectorLaravel\Tests\Rector\MethodCall\WhereToWhereLikeRector\WhereToWhereLikeRectorTest
* @see \RectorLaravel\Tests\Rector\MethodCall\WhereToWhereLikeRector\WhereToWhereLikeRectorPostgresTest
*/
final class WhereToWhereLikeRector extends AbstractRector implements ConfigurableRectorInterface
{
public const string USING_POSTGRES_DRIVER = 'usingPostgresDriver';

private const array WHERE_LIKE_METHODS = [
'where' => 'whereLike',
'orwhere' => 'orWhereLike',
];

private bool $usingPostgresDriver = false;

public function getRuleDefinition(): RuleDefinition
{
$description = "Changes `where` method and static calls to `whereLike` calls in the Eloquent & Query Builder.\n\n"
. 'Can be configured for the Postgres driver with `[WhereToWhereLikeRector::USING_POSTGRES_DRIVER => true]`.';

return new RuleDefinition(
$description, [
new ConfiguredCodeSample(
<<<'CODE_SAMPLE'
$query->where('name', 'like', 'Rector');
$query->orWhere('name', 'like', 'Rector');
$query->where('name', 'like binary', 'Rector');
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
$query->whereLike('name', 'Rector');
$query->orWhereLike('name', 'Rector');
$query->whereLike('name', 'Rector', true);
CODE_SAMPLE
,
[WhereToWhereLikeRector::USING_POSTGRES_DRIVER => false]
),
new ConfiguredCodeSample(
<<<'CODE_SAMPLE'
$query->where('name', 'ilike', 'Rector');
$query->orWhere('name', 'ilike', 'Rector');
$query->where('name', 'like', 'Rector');
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
$query->whereLike('name', 'Rector');
$query->orWhereLike('name', 'Rector');
$query->whereLike('name', 'Rector', true);
CODE_SAMPLE
,
[WhereToWhereLikeRector::USING_POSTGRES_DRIVER => true]
),
]);
}

/**
* @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 StaticCall &&
! $this->isObjectType($node->class, new ObjectType('Illuminate\Database\Eloquent\Model'))) {
return null;
}

if ($node instanceof MethodCall &&
! $this->isObjectType($node->var, new ObjectType('Illuminate\Contracts\Database\Query\Builder'))) {
return null;
}

if (! in_array($this->getLowercaseCallName($node), array_keys(self::WHERE_LIKE_METHODS), true)) {
return null;
}

if (count($node->getArgs()) !== 3) {
return null;
}

$likeParameter = $this->getLikeParameterUsedInQuery($node);

if (! in_array($likeParameter, ['like', 'like binary', 'ilike', 'not like', 'not like binary', 'not ilike'], true)) {
return null;
}

$this->setNewNodeName($node, $likeParameter);

$this->setCaseSensitivity($node, $likeParameter);

// Remove the second argument (the 'like' operator)
unset($node->args[1]);

return $node;
}

public function configure(array $configuration): void
{
if ($configuration === []) {
$this->usingPostgresDriver = false;

return;
}

Assert::keyExists($configuration, self::USING_POSTGRES_DRIVER);
Assert::boolean($configuration[self::USING_POSTGRES_DRIVER]);
$this->usingPostgresDriver = $configuration[self::USING_POSTGRES_DRIVER];
}

private function getLikeParameterUsedInQuery(MethodCall|StaticCall $call): ?string
{
if (! $call->args[1] instanceof Arg) {
return null;
}

if (! $call->args[1]->value instanceof String_) {
return null;
}

return strtolower($call->args[1]->value->value);
}

private function setNewNodeName(MethodCall|StaticCall $call, string $likeParameter): void
{
$newNodeName = self::WHERE_LIKE_METHODS[$this->getLowercaseCallName($call)];

if (str_contains($likeParameter, 'not')) {
$newNodeName = str_replace('Like', 'NotLike', $newNodeName);
}

$call->name = new Identifier($newNodeName);
}

private function setCaseSensitivity(MethodCall|StaticCall $call, string $likeParameter): void
{
// Case sensitive query in MySQL
if (in_array($likeParameter, ['like binary', 'not like binary'], true)) {
$call->args[] = $this->getCaseSensitivityArgument($call);
}

// Case sensitive query in Postgres
if ($this->usingPostgresDriver && in_array($likeParameter, ['like', 'not like'], true)) {
$call->args[] = $this->getCaseSensitivityArgument($call);
}
}

private function getCaseSensitivityArgument(MethodCall|StaticCall $call): Arg
{
if ($call->args[2] instanceof Arg && $call->args[2]->name instanceof Identifier) {
return new Arg(
new ConstFetch(new Name('true')),
name: new Identifier('caseSensitive')
);
}

return new Arg(new ConstFetch(new Name('true')));
}

private function getLowercaseCallName(MethodCall|StaticCall $call): string
{
return strtolower((string) $this->getName($call->name));
}
}
12 changes: 12 additions & 0 deletions stubs/Illuminate/Contracts/Database/Query/Builder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace Illuminate\Contracts\Database\Query;

if (interface_exists('Illuminate\Contracts\Database\Query\Builder')) {
return;
}

/**
* @mixin \Illuminate\Database\Query\Builder
*/
interface Builder {}
2 changes: 1 addition & 1 deletion stubs/Illuminate/Database/Query/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
return;
}

class Builder
class Builder implements \Illuminate\Contracts\Database\Query\Builder
{
public function publicMethodBelongsToQueryBuilder(): void {}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace RectorLaravel\Tests\Rector\MethodCall\WhereToWhereLikeRector\Fixture\Default;

use Illuminate\Contracts\Database\Query\Builder;

class Fixture
{
public function run(Builder $query, NonModel $nonQuery)
{
$query->where('name', 'like', 'Rector');
$query->orWhere('name', 'like', 'Rector');
$query->orwhere('name', 'LIKE', 'Rector');
$query->where('name', 'not like', 'Rector');
$query->orwhere('name', 'not like', 'Rector');
$query->orwhere('name', like: 'not like', value: 'Rector');

// Case Sensitivity
$query->where('name', 'like binary', 'Rector');
$query->where('name', 'not like binary', 'Rector');
$query->where('name', like: 'like binary', value: 'Rector');

// Invalid
$nonQuery->where('name', 'like', 'Rector');
}
}
?>
-----
<?php

namespace RectorLaravel\Tests\Rector\MethodCall\WhereToWhereLikeRector\Fixture\Default;

use Illuminate\Contracts\Database\Query\Builder;

class Fixture
{
public function run(Builder $query, NonModel $nonQuery)
{
$query->whereLike('name', 'Rector');
$query->orWhereLike('name', 'Rector');
$query->orWhereLike('name', 'Rector');
$query->whereNotLike('name', 'Rector');
$query->orWhereNotLike('name', 'Rector');
$query->orWhereNotLike('name', value: 'Rector');

// Case Sensitivity
$query->whereLike('name', 'Rector', true);
$query->whereNotLike('name', 'Rector', true);
$query->whereLike('name', value: 'Rector', caseSensitive: true);

// Invalid
$nonQuery->where('name', 'like', 'Rector');
}
}
?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace RectorLaravel\Tests\Rector\MethodCall\WhereToWhereLikeRector\Fixture\Default;

use RectorLaravel\Tests\Rector\MethodCall\WhereToWhereLikeRector\Source\Post;

class WithStaticCalls
{
public function run()
{
Post::where('name', 'like', 'Rector');
Post::orWhere('name', 'like', 'Rector');
Post::orwhere('name', 'LIKE', 'Rector');
Post::where('name', 'not like', 'Rector');
Post::orwhere('name', 'not like', 'Rector');
Post::orwhere('name', like: 'not like', value: 'Rector');

// Case Sensitivity
Post::where('name', 'like binary', 'Rector');
Post::where('name', 'not like binary', 'Rector');
Post::where('name', like: 'like binary', value: 'Rector');

// Invalid
NonModel::where('name', 'like', 'Rector');
}
}
?>
-----
<?php

namespace RectorLaravel\Tests\Rector\MethodCall\WhereToWhereLikeRector\Fixture\Default;

use RectorLaravel\Tests\Rector\MethodCall\WhereToWhereLikeRector\Source\Post;

class WithStaticCalls
{
public function run()
{
Post::whereLike('name', 'Rector');
Post::orWhereLike('name', 'Rector');
Post::orWhereLike('name', 'Rector');
Post::whereNotLike('name', 'Rector');
Post::orWhereNotLike('name', 'Rector');
Post::orWhereNotLike('name', value: 'Rector');

// Case Sensitivity
Post::whereLike('name', 'Rector', true);
Post::whereNotLike('name', 'Rector', true);
Post::whereLike('name', value: 'Rector', caseSensitive: true);

// Invalid
NonModel::where('name', 'like', 'Rector');
}
}
?>
Loading
Loading