Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add EloquentMagicMethodToQueryBuilderRector rule #132

Merged
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
12 changes: 12 additions & 0 deletions config/sets/laravel-eloquent-magic-method-to-query-builder.php
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\StaticCall\EloquentMagicMethodToQueryBuilderRector;

return static function (RectorConfig $rectorConfig): void {
$rectorConfig->import(__DIR__ . '/../config.php');
$rectorConfig->rule(EloquentMagicMethodToQueryBuilderRector::class);
};
17 changes: 16 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,21 @@ Convert DB Expression `__toString()` calls to `getValue()` method calls.

<br>

## EloquentMagicMethodToQueryBuilderRector

Transform certain magic method calls on Eloquent Models into corresponding Query Builder method calls.

- class: [`RectorLaravel\Rector\StaticCall\EloquentMagicMethodToQueryBuilderRector`](../src/Rector/StaticCall/EloquentMagicMethodToQueryBuilderRector.php)

```diff
-User::find(1);
-User::where('email', 'test@test.com')->first();
+User::query()->find(1);
+User::query()->where('email', 'test@test.com')->first();
```

<br>

## EmptyToBlankAndFilledFuncRector

Replace use of the unsafe `empty()` function with Laravel's safer `blank()` & `filled()` functions.
Expand Down
134 changes: 134 additions & 0 deletions src/Rector/StaticCall/EloquentMagicMethodToQueryBuilderRector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
<?php

declare(strict_types=1);

namespace RectorLaravel\Rector\StaticCall;

use Illuminate\Database\Eloquent\Builder as EloquentQueryBuilder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Query\Builder as QueryBuilder;
use PhpParser\Node;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Identifier;
use Rector\Core\Rector\AbstractRector;
use ReflectionException;
use ReflectionMethod;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

/**
* @see \RectorLaravel\Tests\Rector\StaticCall\EloquentMagicMethodToQueryBuilderRector\EloquentMagicMethodToQueryBuilderRectorTest
*/
final class EloquentMagicMethodToQueryBuilderRector extends AbstractRector
{
public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition('The EloquentMagicMethodToQueryBuilderRule is designed to automatically transform certain magic method calls on Eloquent Models into corresponding Query Builder method calls.', [
new CodeSample(
<<<'CODE_SAMPLE'
use App\Models\User;

$user = User::find(1);
CODE_SAMPLE,
<<<'CODE_SAMPLE'
use App\Models\User;

$user = User::query()->find(1);
CODE_SAMPLE
),
]);
}

/**
* @return array<class-string<Node>>
*/
public function getNodeTypes(): array
{
return [StaticCall::class];
}

/**
* @param StaticCall $node
*/
public function refactor(Node $node): ?Node
{
$resolvedType = $this->nodeTypeResolver->getType($node->class);

// like for variables, example "$namespace"
// @phpstan-ignore-next-line
if (! method_exists($resolvedType, 'getClassName')) {
return null;
}

$className = (string) $resolvedType->getClassName();
$originalClassName = $this->getName($node->class); // like "self" or "App\Models\User"

if (is_null($originalClassName)) {
return null;
}

// does not extend Eloquent Model
if (! is_subclass_of($className, Model::class)) {
return null;
}

if (! $node->name instanceof Identifier) {
return null;
}

$methodName = $node->name->toString();

// if not a magic method
if (! $this->isMagicMethod($className, $methodName)) {
return null;
}

// if method belongs to Eloquent Query Builder or Query Builder
if (! ($this->isPublicMethod(EloquentQueryBuilder::class, $methodName) ||
$this->isPublicMethod(QueryBuilder::class, $methodName)
)) {
return null;
}

$queryMethodCall = $this->nodeFactory->createStaticCall($originalClassName, 'query');

$newNode = $this->nodeFactory->createMethodCall($queryMethodCall, $methodName);
foreach ($node->args as $arg) {
$newNode->args[] = $arg;
}

return $newNode;
}

public function isMagicMethod(string $className, string $methodName): bool
{
try {
$reflectionMethod = new ReflectionMethod($className, $methodName);
} catch (ReflectionException $e) {
return true; // method does not exist => is magic method
}

return false; // not a magic method
}

public function isPublicMethod(string $className, string $methodName): bool
{
try {
$reflectionMethod = new ReflectionMethod($className, $methodName);

// if not public
if (! $reflectionMethod->isPublic()) {
return false;
}

// if static
if ($reflectionMethod->isStatic()) {
return false;
}
} catch (ReflectionException $e) {
return false; // method does not exist => is magic method
}

return true; // method exist
}
}
5 changes: 5 additions & 0 deletions src/Set/LaravelSetList.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,9 @@ final class LaravelSetList implements SetListInterface
* @var string
*/
final public const LARAVEL_FACADE_ALIASES_TO_FULL_NAMES = __DIR__ . '/../../config/sets/laravel-facade-aliases-to-full-names.php';

/**
* @var string
*/
final public const LARAVEL_ELOQUENT_MAGIC_METHOD_TO_QUERY_BUILDER = __DIR__ . '/../../config/sets/laravel-eloquent-magic-method-to-query-builder.php';
}
16 changes: 16 additions & 0 deletions stubs/Illuminate/Database/Eloquent/Builder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Illuminate\Database\Eloquent;

use Illuminate\Database\Query\Builder as QueryBuilder;

if (class_exists('Illuminate\Database\Eloquent\Builder')) {
return;
}

class Builder extends QueryBuilder
{
public function publicMethodBelongsToEloquentQueryBuilder(): void
{
}
}
22 changes: 22 additions & 0 deletions stubs/Illuminate/Database/Query/Builder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace Illuminate\Database\Query;

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

class Builder
{
public function publicMethodBelongsToQueryBuilder(): void
{
}

protected function protectedMethodBelongsToQueryBuilder(): void
{
}

private function privateMethodBelongsToQueryBuilder(): void
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace RectorLaravel\Tests\Rector\StaticCall\EloquentMagicMethodToQueryBuilderRector;

use Illuminate\Database\Eloquent\Model;
use Iterator;
use PHPUnit\Framework\Attributes\DataProvider;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;

final class User extends Model
{
public static function staticMethodBelongsToModel(): void
{
}
}

final class EloquentMagicMethodToQueryBuilderRectorTest 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,47 @@
<?php

namespace RectorLaravel\Tests\Rector\StaticCall\EloquentMagicMethodToQueryBuilderRector\Fixture;

use RectorLaravel\Tests\Rector\StaticCall\EloquentMagicMethodToQueryBuilderRector\User;

class SomeController
{
public function getUser()
{
# eligible
$user = User::publicMethodBelongsToEloquentQueryBuilder(1)->where('xxx', 'xxx')->first();
$user = User::publicMethodBelongsToQueryBuilder(1);

# not eligible
$user = User::privateMethodBelongsToQueryBuilder(1);
$user = User::protectedMethodBelongsToQueryBuilder(1);
$user = User::publicMethodNotBelongsToQueryBuilder(1);
$user = User::query()->publicMethodBelongsToEloquentQueryBuilder(1);
$user = User::query()->publicMethodBelongsToQueryBuilder(1);
$user = User::staticMethodBelongsToModel(1);
}
}
-----
<?php

namespace RectorLaravel\Tests\Rector\StaticCall\EloquentMagicMethodToQueryBuilderRector\Fixture;

use RectorLaravel\Tests\Rector\StaticCall\EloquentMagicMethodToQueryBuilderRector\User;

class SomeController
{
public function getUser()
{
# eligible
$user = User::query()->publicMethodBelongsToEloquentQueryBuilder(1)->where('xxx', 'xxx')->first();
$user = User::query()->publicMethodBelongsToQueryBuilder(1);

# not eligible
$user = User::privateMethodBelongsToQueryBuilder(1);
$user = User::protectedMethodBelongsToQueryBuilder(1);
$user = User::publicMethodNotBelongsToQueryBuilder(1);
$user = User::query()->publicMethodBelongsToEloquentQueryBuilder(1);
$user = User::query()->publicMethodBelongsToQueryBuilder(1);
$user = User::staticMethodBelongsToModel(1);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;

use RectorLaravel\Rector\StaticCall\EloquentMagicMethodToQueryBuilderRector;

return static function (RectorConfig $rectorConfig): void {
$rectorConfig->import(__DIR__ . '/../../../../../config/config.php');
$rectorConfig->importNames(importDocBlockNames: false);
$rectorConfig->importShortClasses(false);
$rectorConfig->rule(EloquentMagicMethodToQueryBuilderRector::class);
};