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);
+};