Skip to content

Commit f9e741c

Browse files
authored
fix: skip privatizing Laravel Model attributes and scopes (#7218)
* fix: skip privatizing Laravel Model attributes and scopes * fix: use StringUtils::isMatch * fix: reuse existing class reflection * chore: move laravel model logic to a separate service
1 parent 5cb87a1 commit f9e741c

6 files changed

Lines changed: 152 additions & 0 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace Rector\Tests\Privatization\Rector\ClassMethod\PrivatizeFinalClassMethodRector\Fixture;
4+
5+
use Illuminate\Database\Eloquent\Attributes\Scope;
6+
use Illuminate\Database\Eloquent\Casts\Attribute;
7+
use Illuminate\Database\Eloquent\Model;
8+
9+
final class User extends Model
10+
{
11+
protected function getSomeAttribute(): mixed
12+
{
13+
}
14+
15+
protected function setSomeAttribute(): mixed
16+
{
17+
}
18+
19+
protected function foo(): Attribute
20+
{
21+
}
22+
23+
protected function scopeFoo(): mixed
24+
{
25+
}
26+
27+
#[Scope]
28+
protected function bar(): mixed
29+
{
30+
}
31+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Privatization\Guard;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Stmt\ClassMethod;
9+
use PHPStan\Reflection\ClassReflection;
10+
use PHPStan\Type\ObjectType;
11+
use Rector\NodeNameResolver\NodeNameResolver;
12+
use Rector\NodeTypeResolver\NodeTypeResolver;
13+
use Rector\Php80\NodeAnalyzer\PhpAttributeAnalyzer;
14+
use Rector\Util\StringUtils;
15+
16+
/**
17+
* Guards against privatizing Laravel model attributes and scopes
18+
*/
19+
final readonly class LaravelModelGuard
20+
{
21+
/**
22+
* @var string
23+
* @see https://regex101.com/r/Dx0WN5/2
24+
*/
25+
private const LARAVEL_MODEL_ATTRIBUTE_REGEX = '#^[gs]et.+Attribute$#';
26+
27+
/**
28+
* @var string
29+
* @see https://regex101.com/r/hxOGeN/2
30+
*/
31+
private const LARAVEL_MODEL_SCOPE_REGEX = '#^scope.+$#';
32+
33+
public function __construct(
34+
private PhpAttributeAnalyzer $phpAttributeAnalyzer,
35+
private NodeNameResolver $nodeNameResolver,
36+
private NodeTypeResolver $nodeTypeResolver,
37+
) {
38+
}
39+
40+
public function isProtectedMethod(ClassReflection $classReflection, ClassMethod $classMethod): bool
41+
{
42+
if (! $classReflection->is('Illuminate\Database\Eloquent\Model')) {
43+
return false;
44+
}
45+
46+
$name = (string) $this->nodeNameResolver->getName($classMethod->name);
47+
48+
return $this->isAttributeMethod($name, $classMethod)
49+
|| $this->isScopeMethod($name, $classMethod);
50+
}
51+
52+
private function isAttributeMethod(string $name, ClassMethod $classMethod): bool
53+
{
54+
if (StringUtils::isMatch($name, self::LARAVEL_MODEL_ATTRIBUTE_REGEX)) {
55+
return true;
56+
}
57+
58+
if (! $classMethod->returnType instanceof Node) {
59+
return false;
60+
}
61+
62+
return $this->nodeTypeResolver->isObjectType(
63+
$classMethod->returnType,
64+
new ObjectType('Illuminate\Database\Eloquent\Casts\Attribute')
65+
);
66+
}
67+
68+
private function isScopeMethod(string $name, ClassMethod $classMethod): bool
69+
{
70+
if (StringUtils::isMatch($name, self::LARAVEL_MODEL_SCOPE_REGEX)) {
71+
return true;
72+
}
73+
74+
return $this->phpAttributeAnalyzer->hasPhpAttribute(
75+
$classMethod,
76+
'Illuminate\Database\Eloquent\Attributes\Scope'
77+
);
78+
}
79+
}

rules/Privatization/Rector/ClassMethod/PrivatizeFinalClassMethodRector.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PHPStan\Reflection\ClassReflection;
1212
use Rector\PhpParser\Node\BetterNodeFinder;
1313
use Rector\PHPStan\ScopeFetcher;
14+
use Rector\Privatization\Guard\LaravelModelGuard;
1415
use Rector\Privatization\Guard\OverrideByParentClassGuard;
1516
use Rector\Privatization\NodeManipulator\VisibilityManipulator;
1617
use Rector\Privatization\VisibilityGuard\ClassMethodVisibilityGuard;
@@ -28,6 +29,7 @@ public function __construct(
2829
private readonly VisibilityManipulator $visibilityManipulator,
2930
private readonly OverrideByParentClassGuard $overrideByParentClassGuard,
3031
private readonly BetterNodeFinder $betterNodeFinder,
32+
private readonly LaravelModelGuard $laravelModelGuard,
3133
) {
3234
}
3335

@@ -93,6 +95,10 @@ public function refactor(Node $node): ?Node
9395
continue;
9496
}
9597

98+
if ($this->laravelModelGuard->isProtectedMethod($classReflection, $classMethod)) {
99+
continue;
100+
}
101+
96102
if ($this->classMethodVisibilityGuard->isClassMethodVisibilityGuardedByParent(
97103
$classMethod,
98104
$classReflection
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace Illuminate\Database\Eloquent\Attributes;
4+
5+
use Attribute;
6+
7+
if (class_exists('Illuminate\Database\Eloquent\Attributes\Scope')) {
8+
return;
9+
}
10+
11+
#[Attribute(Attribute::TARGET_METHOD)]
12+
class Scope
13+
{
14+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace Illuminate\Database\Eloquent\Casts;
4+
5+
if (class_exists('Illuminate\Database\Eloquent\Casts\Attribute')) {
6+
return;
7+
}
8+
9+
class Attribute
10+
{
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace Illuminate\Database\Eloquent;
4+
5+
if (class_exists('Illuminate\Database\Eloquent\Model')) {
6+
return;
7+
}
8+
9+
abstract class Model
10+
{
11+
}

0 commit comments

Comments
 (0)