44
55namespace RectorLaravel \Rector \StaticCall ;
66
7- use Illuminate \Database \Eloquent \Builder as EloquentQueryBuilder ;
7+ use Illuminate \Database \Eloquent \Builder as EloquentBuilder ;
88use Illuminate \Database \Eloquent \Model ;
99use Illuminate \Database \Query \Builder as QueryBuilder ;
1010use PhpParser \Node ;
11+ use PhpParser \Node \Expr \MethodCall ;
1112use PhpParser \Node \Expr \StaticCall ;
1213use PhpParser \Node \Identifier ;
13- use PhpParser \Node \Name ;
14+ use PHPStan \Analyser \OutOfClassScope ;
15+ use PHPStan \Reflection \ClassReflection ;
16+ use PHPStan \Reflection \ReflectionProvider ;
1417use Rector \Contract \Rector \ConfigurableRectorInterface ;
1518use RectorLaravel \AbstractRector ;
16- use ReflectionException ;
17- use ReflectionMethod ;
1819use Symplify \RuleDocGenerator \ValueObject \CodeSample \ConfiguredCodeSample ;
1920use Symplify \RuleDocGenerator \ValueObject \RuleDefinition ;
2021use Webmozart \Assert \Assert ;
@@ -31,6 +32,10 @@ final class EloquentMagicMethodToQueryBuilderRector extends AbstractRector imple
3132 */
3233 private array $ excludeMethods = [];
3334
35+ public function __construct (
36+ private readonly ReflectionProvider $ reflectionProvider
37+ ) {}
38+
3439 public function getRuleDefinition (): RuleDefinition
3540 {
3641 return new RuleDefinition (
@@ -40,13 +45,15 @@ public function getRuleDefinition(): RuleDefinition
4045 <<<'CODE_SAMPLE'
4146use App\Models\User;
4247
48+ $user = User::first();
4349$user = User::find(1);
4450CODE_SAMPLE
4551 ,
4652 <<<'CODE_SAMPLE'
4753use App\Models\User;
4854
49- $user = User::query()->find(1);
55+ $user = User::query()->first();
56+ $user = User::find(1);
5057CODE_SAMPLE
5158 , [
5259 self ::EXCLUDE_METHODS => ['find ' ],
@@ -68,60 +75,50 @@ public function getNodeTypes(): array
6875 */
6976 public function refactor (Node $ node ): ?Node
7077 {
71- $ resolvedType = $ this ->nodeTypeResolver ->getType ($ node ->class );
72-
73- $ classNames = $ resolvedType ->getObjectClassNames ();
74-
75- if ($ classNames === []) {
78+ if (! $ node ->name instanceof Identifier) {
7679 return null ;
7780 }
7881
79- $ className = $ classNames [0 ];
80-
81- $ originalClassName = $ this ->getName ($ node ->class ); // like "self" or "App\Models\User"
82+ $ methodName = $ node ->name ->toString ();
8283
83- if ($ originalClassName === null ) {
84+ if (
85+ $ methodName === 'query ' // short circuit
86+ || in_array ($ methodName , $ this ->excludeMethods , true )
87+ ) {
8488 return null ;
8589 }
8690
87- // does not extend Eloquent Model
88- if (! is_subclass_of ($ className , Model::class)) {
89- return null ;
90- }
91+ $ resolvedType = $ this ->nodeTypeResolver ->getType ($ node ->class );
9192
92- if (! $ node -> name instanceof Identifier) {
93- return null ;
94- }
93+ $ classNames = $ resolvedType -> isClassString ()-> yes ()
94+ ? $ resolvedType -> getClassStringObjectType ()-> getObjectClassNames ()
95+ : $ resolvedType -> getObjectClassNames ();
9596
96- $ methodName = $ node -> name -> toString () ;
97+ $ classReflection = null ;
9798
98- // if not a magic method
99- if (! $ this ->isMagicMethod ($ className, $ methodName )) {
100- return null ;
101- }
99+ foreach ( $ classNames as $ className ) {
100+ if (! $ this ->reflectionProvider -> hasClass ($ className )) {
101+ continue ;
102+ }
102103
103- // if method belongs to Eloquent Query Builder or Query Builder
104- if (! $ this ->isPublicMethod (EloquentQueryBuilder::class, $ methodName ) && ! $ this ->isPublicMethod (
105- QueryBuilder::class,
106- $ methodName
107- )) {
108- return null ;
109- }
104+ $ classReflection = $ this ->reflectionProvider ->getClass ($ className );
110105
111- if ($ node ->class instanceof Name) {
112- $ staticCall = $ this ->nodeFactory ->createStaticCall ($ originalClassName , 'query ' );
106+ if (! $ classReflection ->is (Model::class)) {
107+ continue ;
108+ }
109+
110+ break ;
113111 }
114112
115- if (! $ node -> class instanceof Name ) {
116- $ staticCall = new StaticCall ( $ node -> class , ' query ' ) ;
113+ if (! $ classReflection instanceof ClassReflection ) {
114+ return null ;
117115 }
118116
119- $ methodCall = $ this ->nodeFactory ->createMethodCall ($ staticCall , $ methodName );
120- foreach ($ node ->args as $ arg ) {
121- $ methodCall ->args [] = $ arg ;
117+ if (! $ this ->isMagicMethod ($ classReflection , $ methodName )) {
118+ return null ;
122119 }
123120
124- return $ methodCall ;
121+ return new MethodCall ( new StaticCall ( $ node -> class , ' query ' ), $ node -> name , $ node -> args ) ;
125122 }
126123
127124 /**
@@ -136,39 +133,45 @@ public function configure(array $configuration): void
136133 $ this ->excludeMethods = $ excludeMethods ;
137134 }
138135
139- public function isMagicMethod (string $ className , string $ methodName ): bool
136+ private function isMagicMethod (ClassReflection $ classReflection , string $ methodName ): bool
140137 {
141- if (in_array ($ methodName , $ this ->excludeMethods , true )) {
142- return false ;
138+ if (! $ classReflection ->hasMethod ($ methodName )) {
139+ // if the class doesn't have the method then check if the method is a scope
140+ if ($ classReflection ->hasMethod ('scope ' . ucfirst ($ methodName ))) {
141+ return true ;
142+ }
143+
144+ // otherwise, need to check if the method is directly on the EloquentBuilder or QueryBuilder
145+ return $ this ->isPublicMethod (EloquentBuilder::class, $ methodName )
146+ || $ this ->isPublicMethod (QueryBuilder::class, $ methodName );
143147 }
144148
145- try {
146- $ reflectionMethod = new ReflectionMethod ( $ className , $ methodName );
147- } catch ( ReflectionException ) {
148- return true ; // method does not exist => is magic method
149+ $ extendedMethodReflection = $ classReflection -> getMethod ( $ methodName , new OutOfClassScope );
150+
151+ if (! $ extendedMethodReflection -> isPublic () || $ extendedMethodReflection -> isStatic () ) {
152+ return false ;
149153 }
150154
151- return false ; // not a magic method
155+ $ declaringClass = $ extendedMethodReflection ->getDeclaringClass ();
156+
157+ // finally, make sure the method is on the builders or a subclass
158+ return $ declaringClass ->is (EloquentBuilder::class) || $ declaringClass ->is (QueryBuilder::class);
152159 }
153160
154- public function isPublicMethod (string $ className , string $ methodName ): bool
161+ private function isPublicMethod (string $ className , string $ methodName ): bool
155162 {
156- try {
157- $ reflectionMethod = new ReflectionMethod ($ className , $ methodName );
163+ if (! $ this ->reflectionProvider ->hasClass ($ className )) {
164+ return false ;
165+ }
158166
159- // if not public
160- if (! $ reflectionMethod ->isPublic ()) {
161- return false ;
162- }
167+ $ classReflection = $ this ->reflectionProvider ->getClass ($ className );
163168
164- // if static
165- if ($ reflectionMethod ->isStatic ()) {
166- return false ;
167- }
168- } catch (ReflectionException ) {
169- return false ; // method does not exist => is magic method
169+ if (! $ classReflection ->hasMethod ($ methodName )) {
170+ return false ;
170171 }
171172
172- return true ; // method exist
173+ $ extendedMethodReflection = $ classReflection ->getMethod ($ methodName , new OutOfClassScope );
174+
175+ return $ extendedMethodReflection ->isPublic () && ! $ extendedMethodReflection ->isStatic ();
173176 }
174177}
0 commit comments