Skip to content

Commit 40bbddd

Browse files
authored
Scope to use attribute rule (#320)
1 parent 03101b5 commit 40bbddd

10 files changed

+273
-5
lines changed

Diff for: config/sets/laravel120.php

+3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
declare(strict_types=1);
44

55
use Rector\Config\RectorConfig;
6+
use RectorLaravel\Rector\ClassMethod\ScopeNamedClassMethodToScopeAttributedClassMethodRector;
67
use RectorLaravel\Rector\MethodCall\ContainerBindConcreteWithClosureOnlyRector;
78

89
// see https://laravel.com/docs/12.x/upgrade
@@ -11,4 +12,6 @@
1112

1213
// https://github.com/laravel/framework/pull/54628
1314
$rectorConfig->rule(ContainerBindConcreteWithClosureOnlyRector::class);
15+
// https://github.com/laravel/framework/pull/54450
16+
$rectorConfig->rule(ScopeNamedClassMethodToScopeAttributedClassMethodRector::class);
1417
};

Diff for: docs/rector_rules_overview.md

+21-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# 77 Rules Overview
1+
# 78 Rules Overview
22

33
## AbortIfRector
44

@@ -1325,6 +1325,26 @@ Use PHP callable syntax instead of string syntax for controller route declaratio
13251325

13261326
<br>
13271327

1328+
## ScopeNamedClassMethodToScopeAttributedClassMethodRector
1329+
1330+
Changes model scope methods to use the scope attribute
1331+
1332+
- class: [`RectorLaravel\Rector\ClassMethod\ScopeNamedClassMethodToScopeAttributedClassMethodRector`](../src/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector.php)
1333+
1334+
```diff
1335+
class User extends Model
1336+
{
1337+
- public function scopeActive($query)
1338+
+ #[\Illuminate\Database\Eloquent\Attributes\Scope]
1339+
+ public function active($query)
1340+
{
1341+
return $query->where('active', 1);
1342+
}
1343+
}
1344+
```
1345+
1346+
<br>
1347+
13281348
## ServerVariableToRequestFacadeRector
13291349

13301350
Change server variable to Request facade's server method
@@ -1459,12 +1479,8 @@ Use the base collection methods instead of their aliases.
14591479
$collection = new Collection([0, 1, null, -1]);
14601480
-$collection->average();
14611481
-$collection->some(fn (?int $number): bool => is_null($number));
1462-
-$collection->unlessEmpty(fn(Collection $collection) => $collection->push('Foo'));
1463-
-$collection->unlessNotEmpty(fn(Collection $collection) => $collection->push('Foo'));
14641482
+$collection->avg();
14651483
+$collection->contains(fn (?int $number): bool => is_null($number));
1466-
+$collection->whenNotEmpty(fn(Collection $collection) => $collection->push('Foo'));
1467-
+$collection->whenEmpty(fn(Collection $collection) => $collection->push('Foo'));
14681484
```
14691485

14701486
<br>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace RectorLaravel\Rector\ClassMethod;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Attribute;
9+
use PhpParser\Node\AttributeGroup;
10+
use PhpParser\Node\Identifier;
11+
use PhpParser\Node\Name\FullyQualified;
12+
use PhpParser\Node\Stmt\Class_;
13+
use PHPStan\Reflection\ReflectionProvider;
14+
use PHPStan\Type\ObjectType;
15+
use Rector\Php80\NodeAnalyzer\PhpAttributeAnalyzer;
16+
use RectorLaravel\AbstractRector;
17+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
18+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
19+
20+
/**
21+
* @see \RectorLaravel\Tests\Rector\ClassMethod\ScopeNamedClassMethodToScopeAttributedClassMethodRector\ScopeNamedClassMethodToScopeAttributedClassMethodRectorTest
22+
*/
23+
final class ScopeNamedClassMethodToScopeAttributedClassMethodRector extends AbstractRector
24+
{
25+
private const string SCOPE_ATTRIBUTE = 'Illuminate\Database\Eloquent\Attributes\Scope';
26+
27+
public function __construct(
28+
private readonly PhpAttributeAnalyzer $phpAttributeAnalyzer,
29+
private readonly ReflectionProvider $reflectionProvider,
30+
) {}
31+
32+
public function getRuleDefinition(): RuleDefinition
33+
{
34+
return new RuleDefinition(
35+
'Changes model scope methods to use the scope attribute',
36+
[new CodeSample(
37+
<<<'CODE_SAMPLE'
38+
class User extends Model
39+
{
40+
public function scopeActive($query)
41+
{
42+
return $query->where('active', 1);
43+
}
44+
}
45+
CODE_SAMPLE,
46+
<<<'CODE_SAMPLE'
47+
class User extends Model
48+
{
49+
#[\Illuminate\Database\Eloquent\Attributes\Scope]
50+
public function active($query)
51+
{
52+
return $query->where('active', 1);
53+
}
54+
}
55+
CODE_SAMPLE
56+
)]
57+
);
58+
}
59+
60+
/**
61+
* @return array<class-string<Node>>
62+
*/
63+
public function getNodeTypes(): array
64+
{
65+
return [Class_::class];
66+
}
67+
68+
/**
69+
* @param Class_ $node
70+
*/
71+
public function refactor(Node $node): ?Node
72+
{
73+
if (! $this->isObjectType($node, new ObjectType('Illuminate\Database\Eloquent\Model'))) {
74+
return null;
75+
}
76+
77+
if (! is_string($className = $this->getName($node))) {
78+
return null;
79+
}
80+
81+
$classReflection = $this->reflectionProvider->getClass($className);
82+
83+
$changes = false;
84+
foreach ($node->getMethods() as $classMethod) {
85+
$name = $this->getName($classMethod);
86+
// make sure it starts with scope and the next character is upper case
87+
if (! str_starts_with($name, 'scope') || ! ctype_upper(substr($name, 5, 1))) {
88+
continue;
89+
}
90+
91+
$newName = lcfirst(str_replace('scope', '', $name));
92+
93+
if ($classReflection->hasMethod($newName)) {
94+
continue;
95+
}
96+
97+
if ($this->phpAttributeAnalyzer->hasPhpAttribute($classMethod, self::SCOPE_ATTRIBUTE)) {
98+
continue;
99+
}
100+
101+
$classMethod->name = new Identifier($newName);
102+
$classMethod->attrGroups[] = new AttributeGroup([new Attribute(new FullyQualified(self::SCOPE_ATTRIBUTE))]);
103+
$changes = true;
104+
}
105+
106+
if ($changes === false) {
107+
return null;
108+
}
109+
110+
return $node;
111+
}
112+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
namespace RectorLaravel\Tests\Rector\ScopeNamedClassMethodToScopeAttributedClassMethodRector\Fixture;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
7+
class SomeClass extends Model
8+
{
9+
public function scopeSomeMethod()
10+
{
11+
12+
}
13+
}
14+
15+
?>
16+
-----
17+
<?php
18+
19+
namespace RectorLaravel\Tests\Rector\ScopeNamedClassMethodToScopeAttributedClassMethodRector\Fixture;
20+
21+
use Illuminate\Database\Eloquent\Model;
22+
23+
class SomeClass extends Model
24+
{
25+
#[\Illuminate\Database\Eloquent\Attributes\Scope]
26+
public function someMethod()
27+
{
28+
29+
}
30+
}
31+
32+
?>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace RectorLaravel\Tests\Rector\ScopeNamedClassMethodToScopeAttributedClassMethodRector\Fixture;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
7+
class NonDuplicateAttributeNodes extends Model
8+
{
9+
#[\Illuminate\Database\Eloquent\Attributes\Scope]
10+
public function scopeSomeMethod()
11+
{
12+
13+
}
14+
}
15+
16+
?>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace RectorLaravel\Tests\Rector\ScopeNamedClassMethodToScopeAttributedClassMethodRector\Fixture;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
7+
class NonMatchingMethods extends Model
8+
{
9+
public function scopefoo()
10+
{
11+
12+
}
13+
}
14+
15+
?>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace RectorLaravel\Tests\Rector\ScopeNamedClassMethodToScopeAttributedClassMethodRector\Fixture;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
7+
class NonDuplicateMethod extends Model
8+
{
9+
public function scopeSomeMethod()
10+
{
11+
12+
}
13+
14+
public function someMethod()
15+
{
16+
17+
}
18+
}
19+
20+
?>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace RectorLaravel\Tests\Rector\ScopeNamedClassMethodToScopeAttributedClassMethodRector\Fixture;
4+
5+
class NonModelClass
6+
{
7+
public function scopeSomeMethod()
8+
{
9+
10+
}
11+
}
12+
13+
?>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace RectorLaravel\Tests\Rector\ClassMethod\ScopeNamedClassMethodToScopeAttributedClassMethodRector;
6+
7+
use Iterator;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
10+
11+
final class ScopeNamedClassMethodToScopeAttributedClassMethodRectorTest extends AbstractRectorTestCase
12+
{
13+
public static function provideData(): Iterator
14+
{
15+
return self::yieldFilesFromDirectory(__DIR__ . '/Fixture');
16+
}
17+
18+
/**
19+
* @test
20+
*/
21+
#[DataProvider('provideData')]
22+
public function test(string $filePath): void
23+
{
24+
$this->doTestFile($filePath);
25+
}
26+
27+
public function provideConfigFilePath(): string
28+
{
29+
return __DIR__ . '/config/configured_rule.php';
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Rector\Config\RectorConfig;
6+
use RectorLaravel\Rector\ClassMethod\ScopeNamedClassMethodToScopeAttributedClassMethodRector;
7+
8+
return static function (RectorConfig $rectorConfig): void {
9+
$rectorConfig->rule(ScopeNamedClassMethodToScopeAttributedClassMethodRector::class);
10+
};

0 commit comments

Comments
 (0)