From c664be4794f5e7e46699e52ffc882a284cde7938 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Birkl=C3=A9?= Date: Mon, 18 Sep 2023 22:16:17 +0200 Subject: [PATCH] Add EloquentMagicMethodToQueryBuilderRector rule (#132) * Add EloquentMagicMethodToQueryBuilderRector rule * Fix PHPStan errors --- ...eloquent-magic-method-to-query-builder.php | 12 ++ docs/rector_rules_overview.md | 17 ++- ...loquentMagicMethodToQueryBuilderRector.php | 134 ++++++++++++++++++ src/Set/LaravelSetList.php | 5 + .../Illuminate/Database/Eloquent/Builder.php | 16 +++ stubs/Illuminate/Database/Query/Builder.php | 22 +++ ...entMagicMethodToQueryBuilderRectorTest.php | 36 +++++ .../Fixture/fixture.php.inc | 47 ++++++ .../config/configured_rule.php | 14 ++ 9 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 config/sets/laravel-eloquent-magic-method-to-query-builder.php create mode 100644 src/Rector/StaticCall/EloquentMagicMethodToQueryBuilderRector.php create mode 100644 stubs/Illuminate/Database/Eloquent/Builder.php create mode 100644 stubs/Illuminate/Database/Query/Builder.php create mode 100644 tests/Rector/StaticCall/EloquentMagicMethodToQueryBuilderRector/EloquentMagicMethodToQueryBuilderRectorTest.php create mode 100644 tests/Rector/StaticCall/EloquentMagicMethodToQueryBuilderRector/Fixture/fixture.php.inc create mode 100644 tests/Rector/StaticCall/EloquentMagicMethodToQueryBuilderRector/config/configured_rule.php diff --git a/config/sets/laravel-eloquent-magic-method-to-query-builder.php b/config/sets/laravel-eloquent-magic-method-to-query-builder.php new file mode 100644 index 00000000..2e7010e7 --- /dev/null +++ b/config/sets/laravel-eloquent-magic-method-to-query-builder.php @@ -0,0 +1,12 @@ +import(__DIR__ . '/../config.php'); + $rectorConfig->rule(EloquentMagicMethodToQueryBuilderRector::class); +}; diff --git a/docs/rector_rules_overview.md b/docs/rector_rules_overview.md index bd84a5e0..76deac53 100644 --- a/docs/rector_rules_overview.md +++ b/docs/rector_rules_overview.md @@ -1,4 +1,4 @@ -# 39 Rules Overview +# 40 Rules Overview ## AddArgumentDefaultValueRector @@ -458,6 +458,21 @@ Convert DB Expression `__toString()` calls to `getValue()` method calls.
+## 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(); +``` + +
+ ## EmptyToBlankAndFilledFuncRector Replace use of the unsafe `empty()` function with Laravel's safer `blank()` & `filled()` functions. diff --git a/src/Rector/StaticCall/EloquentMagicMethodToQueryBuilderRector.php b/src/Rector/StaticCall/EloquentMagicMethodToQueryBuilderRector.php new file mode 100644 index 00000000..3f4b3f2e --- /dev/null +++ b/src/Rector/StaticCall/EloquentMagicMethodToQueryBuilderRector.php @@ -0,0 +1,134 @@ +find(1); +CODE_SAMPLE + ), + ]); + } + + /** + * @return array> + */ + 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 + } +} diff --git a/src/Set/LaravelSetList.php b/src/Set/LaravelSetList.php index ad10a1e5..f01c35a5 100644 --- a/src/Set/LaravelSetList.php +++ b/src/Set/LaravelSetList.php @@ -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'; } diff --git a/stubs/Illuminate/Database/Eloquent/Builder.php b/stubs/Illuminate/Database/Eloquent/Builder.php new file mode 100644 index 00000000..7169443b --- /dev/null +++ b/stubs/Illuminate/Database/Eloquent/Builder.php @@ -0,0 +1,16 @@ +doTestFile($filePath); + } + + public static function provideData(): Iterator + { + return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/tests/Rector/StaticCall/EloquentMagicMethodToQueryBuilderRector/Fixture/fixture.php.inc b/tests/Rector/StaticCall/EloquentMagicMethodToQueryBuilderRector/Fixture/fixture.php.inc new file mode 100644 index 00000000..cd25aa79 --- /dev/null +++ b/tests/Rector/StaticCall/EloquentMagicMethodToQueryBuilderRector/Fixture/fixture.php.inc @@ -0,0 +1,47 @@ +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); + } +} +----- +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); + } +} diff --git a/tests/Rector/StaticCall/EloquentMagicMethodToQueryBuilderRector/config/configured_rule.php b/tests/Rector/StaticCall/EloquentMagicMethodToQueryBuilderRector/config/configured_rule.php new file mode 100644 index 00000000..6841bc57 --- /dev/null +++ b/tests/Rector/StaticCall/EloquentMagicMethodToQueryBuilderRector/config/configured_rule.php @@ -0,0 +1,14 @@ +import(__DIR__ . '/../../../../../config/config.php'); + $rectorConfig->importNames(importDocBlockNames: false); + $rectorConfig->importShortClasses(false); + $rectorConfig->rule(EloquentMagicMethodToQueryBuilderRector::class); +};