diff --git a/config/sets/laravel120.php b/config/sets/laravel120.php index e8ea63d5..e5c8d287 100644 --- a/config/sets/laravel120.php +++ b/config/sets/laravel120.php @@ -3,6 +3,7 @@ declare(strict_types=1); use Rector\Config\RectorConfig; +use RectorLaravel\Rector\ClassMethod\ScopeNamedClassMethodToScopeAttributedClassMethodRector; use RectorLaravel\Rector\MethodCall\ContainerBindConcreteWithClosureOnlyRector; // see https://laravel.com/docs/12.x/upgrade @@ -11,4 +12,6 @@ // https://github.com/laravel/framework/pull/54628 $rectorConfig->rule(ContainerBindConcreteWithClosureOnlyRector::class); + // https://github.com/laravel/framework/pull/54450 + $rectorConfig->rule(ScopeNamedClassMethodToScopeAttributedClassMethodRector::class); }; diff --git a/docs/rector_rules_overview.md b/docs/rector_rules_overview.md index 59361250..61a0cd72 100644 --- a/docs/rector_rules_overview.md +++ b/docs/rector_rules_overview.md @@ -1,4 +1,4 @@ -# 77 Rules Overview +# 78 Rules Overview ## AbortIfRector @@ -1325,6 +1325,26 @@ Use PHP callable syntax instead of string syntax for controller route declaratio
+## ScopeNamedClassMethodToScopeAttributedClassMethodRector + +Changes model scope methods to use the scope attribute + +- class: [`RectorLaravel\Rector\ClassMethod\ScopeNamedClassMethodToScopeAttributedClassMethodRector`](../src/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector.php) + +```diff + class User extends Model + { +- public function scopeActive($query) ++ #[\Illuminate\Database\Eloquent\Attributes\Scope] ++ public function active($query) + { + return $query->where('active', 1); + } + } +``` + +
+ ## ServerVariableToRequestFacadeRector Change server variable to Request facade's server method @@ -1459,12 +1479,8 @@ Use the base collection methods instead of their aliases. $collection = new Collection([0, 1, null, -1]); -$collection->average(); -$collection->some(fn (?int $number): bool => is_null($number)); --$collection->unlessEmpty(fn(Collection $collection) => $collection->push('Foo')); --$collection->unlessNotEmpty(fn(Collection $collection) => $collection->push('Foo')); +$collection->avg(); +$collection->contains(fn (?int $number): bool => is_null($number)); -+$collection->whenNotEmpty(fn(Collection $collection) => $collection->push('Foo')); -+$collection->whenEmpty(fn(Collection $collection) => $collection->push('Foo')); ```
diff --git a/src/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector.php b/src/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector.php new file mode 100644 index 00000000..cdb0dd1b --- /dev/null +++ b/src/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector.php @@ -0,0 +1,112 @@ +where('active', 1); + } +} +CODE_SAMPLE, + <<<'CODE_SAMPLE' +class User extends Model +{ + #[\Illuminate\Database\Eloquent\Attributes\Scope] + public function active($query) + { + return $query->where('active', 1); + } +} +CODE_SAMPLE + )] + ); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [Class_::class]; + } + + /** + * @param Class_ $node + */ + public function refactor(Node $node): ?Node + { + if (! $this->isObjectType($node, new ObjectType('Illuminate\Database\Eloquent\Model'))) { + return null; + } + + if (! is_string($className = $this->getName($node))) { + return null; + } + + $classReflection = $this->reflectionProvider->getClass($className); + + $changes = false; + foreach ($node->getMethods() as $classMethod) { + $name = $this->getName($classMethod); + // make sure it starts with scope and the next character is upper case + if (! str_starts_with($name, 'scope') || ! ctype_upper(substr($name, 5, 1))) { + continue; + } + + $newName = lcfirst(str_replace('scope', '', $name)); + + if ($classReflection->hasMethod($newName)) { + continue; + } + + if ($this->phpAttributeAnalyzer->hasPhpAttribute($classMethod, self::SCOPE_ATTRIBUTE)) { + continue; + } + + $classMethod->name = new Identifier($newName); + $classMethod->attrGroups[] = new AttributeGroup([new Attribute(new FullyQualified(self::SCOPE_ATTRIBUTE))]); + $changes = true; + } + + if ($changes === false) { + return null; + } + + return $node; + } +} diff --git a/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/fixture.php.inc b/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/fixture.php.inc new file mode 100644 index 00000000..6e4f77a2 --- /dev/null +++ b/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/fixture.php.inc @@ -0,0 +1,32 @@ + +----- + diff --git a/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/non_duplicate_nodes.php.inc b/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/non_duplicate_nodes.php.inc new file mode 100644 index 00000000..79659480 --- /dev/null +++ b/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/non_duplicate_nodes.php.inc @@ -0,0 +1,16 @@ + diff --git a/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/non_matching_methods.php.inc b/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/non_matching_methods.php.inc new file mode 100644 index 00000000..6060d6fe --- /dev/null +++ b/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/non_matching_methods.php.inc @@ -0,0 +1,15 @@ + diff --git a/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/non_method_duplicate.php.inc b/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/non_method_duplicate.php.inc new file mode 100644 index 00000000..4bfd156d --- /dev/null +++ b/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/non_method_duplicate.php.inc @@ -0,0 +1,20 @@ + diff --git a/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/non_model_class.php.inc b/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/non_model_class.php.inc new file mode 100644 index 00000000..3863b1a3 --- /dev/null +++ b/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/non_model_class.php.inc @@ -0,0 +1,13 @@ + diff --git a/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/ScopeNamedClassMethodToScopeAttributedClassMethodRectorTest.php b/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/ScopeNamedClassMethodToScopeAttributedClassMethodRectorTest.php new file mode 100644 index 00000000..9eefe0f0 --- /dev/null +++ b/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/ScopeNamedClassMethodToScopeAttributedClassMethodRectorTest.php @@ -0,0 +1,31 @@ +doTestFile($filePath); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/config/configured_rule.php b/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/config/configured_rule.php new file mode 100644 index 00000000..07585720 --- /dev/null +++ b/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/config/configured_rule.php @@ -0,0 +1,10 @@ +rule(ScopeNamedClassMethodToScopeAttributedClassMethodRector::class); +};