22
33namespace RectorLaravel \NodeAnalyzer ;
44
5- use Exception ;
65use Illuminate \Database \Eloquent \Model ;
6+ use InvalidArgumentException ;
7+ use PHPStan \Analyser \Scope ;
78use PHPStan \Reflection \ClassReflection ;
9+ use PHPStan \Reflection \ExtendedMethodReflection ;
810use PHPStan \Reflection \ReflectionProvider ;
11+ use PHPStan \Type \ObjectType ;
12+ use ReflectionException ;
13+ use Throwable ;
914
1015class ModelAnalyzer
1116{
1217 public function __construct (
13- private readonly ReflectionProvider $ reflectionProvider
18+ private readonly ReflectionProvider $ reflectionProvider,
1419 ) {}
1520
21+ protected static function relationType (): ObjectType
22+ {
23+ return new ObjectType ('Illuminate\Database\Eloquent\Relations\Relation ' );
24+ }
25+
1626 /**
1727 * Returns the table name of a model
1828 *
19- * @param class-string<Model> $class
29+ * @param class-string<Model>|ObjectType $model
2030 *
21- * @throws Exception
31+ * @throws InvalidArgumentException|ReflectionException
2232 */
23- public function getTable (string $ class ): ?string
33+ public function getTable (string | ObjectType $ model ): ?string
2434 {
25- $ classReflection = $ this ->getClass ($ class );
35+ $ model = $ this ->resolveModelClassToInstance ($ model );
36+
37+ if (! $ model instanceof Model) {
38+ return null ;
39+ }
40+
41+ $ table = $ model ->getTable ();
2642
27- /** @var Model $instance */
28- $ instance = $ classReflection ->getNativeReflection ()->newInstanceWithoutConstructor ();
29- $ table = $ instance ->getTable ();
3043 if (! is_string ($ table )) {
3144 return null ;
3245 }
@@ -35,15 +48,22 @@ public function getTable(string $class): ?string
3548 }
3649
3750 /**
38- * @param class-string<Model> $class
51+ * Returns the primary key for a model
52+ *
53+ * @param class-string<Model>|ObjectType $model
54+ *
55+ * @throws ReflectionException
3956 */
40- public function getPrimaryKey (string $ class ): ?string
57+ public function getPrimaryKey (string | ObjectType $ model ): ?string
4158 {
42- $ classReflection = $ this ->getClass ($ class );
59+ $ model = $ this ->resolveModelClassToInstance ($ model );
60+
61+ if (! $ model instanceof Model) {
62+ return null ;
63+ }
64+
65+ $ keyName = $ model ->getKeyName ();
4366
44- /** @var Model $instance */
45- $ instance = $ classReflection ->getNativeReflection ()->newInstanceWithoutConstructor ();
46- $ keyName = $ instance ->getKeyName ();
4767 if (! is_string ($ keyName )) {
4868 return null ;
4969 }
@@ -52,26 +72,125 @@ public function getPrimaryKey(string $class): ?string
5272 }
5373
5474 /**
75+ * @param class-string<Model>|ObjectType $model
76+ */
77+ public function isQueryScopeOnModel (string |ObjectType $ model , string $ scopeName , Scope $ scope ): bool
78+ {
79+ if (! is_string ($ model )) {
80+ /** @var class-string<Model> $model */
81+ $ model = $ model ->getClassName ();
82+ }
83+
84+ $ classReflection = $ this ->getClass ($ model );
85+
86+ if ($ classReflection ->hasMethod ('scope ' . ucfirst ($ scopeName ))) {
87+ return true ;
88+ }
89+
90+ if (! $ classReflection ->hasMethod ($ scopeName )) {
91+ return false ;
92+ }
93+
94+ $ extendedMethodReflection = $ classReflection ->getMethod ($ scopeName , $ scope );
95+
96+ return $ this ->usesScopeAttribute ($ extendedMethodReflection );
97+ }
98+
99+ /**
100+ * @param class-string<Model>|ObjectType $model
101+ */
102+ public function isRelationshipOnModel (string |ObjectType $ model , string $ relationName , Scope $ scope ): bool
103+ {
104+ if (! is_string ($ model )) {
105+ /** @var class-string<Model> $model */
106+ $ model = $ model ->getClassName ();
107+ }
108+
109+ $ classReflection = $ this ->getClass ($ model );
110+
111+ if (! $ classReflection ->hasMethod ($ relationName )) {
112+ return false ;
113+ }
114+
115+ $ extendedMethodReflection = $ classReflection ->getMethod ($ relationName , $ scope );
116+
117+ foreach ($ extendedMethodReflection ->getVariants () as $ extendedParametersAcceptor ) {
118+ $ returnType = $ extendedParametersAcceptor ->getReturnType ();
119+
120+ if ($ returnType ->isObject ()->maybe ()) {
121+ continue ;
122+ }
123+
124+ if (self ::relationType ()->isSuperTypeOf ($ returnType )->yes ()) {
125+ return true ;
126+ }
127+ }
128+
129+ return false ;
130+ }
131+
132+ private function usesScopeAttribute (ExtendedMethodReflection $ extendedMethodReflection ): bool
133+ {
134+ foreach ($ extendedMethodReflection ->getAttributes () as $ attributeReflection ) {
135+ if ($ attributeReflection ->getName () === 'Illuminate\Database\Eloquent\Attributes\Scope ' ) {
136+ return true ;
137+ }
138+ }
139+
140+ return false ;
141+ }
142+
143+ /**
144+ * Get the ClassReflectionFor the Model
145+ *
55146 * @param class-string<Model> $class
56147 *
57- * @throws Exception
148+ * @throws InvalidArgumentException
58149 */
59150 private function getClass (string $ class ): ClassReflection
60151 {
61152 if (! $ this ->reflectionProvider ->hasClass ($ class )) {
62- throw new Exception ('Class not found ' );
153+ throw new InvalidArgumentException ('Class not found ' );
63154 }
64155
65156 $ classReflection = $ this ->reflectionProvider ->getClass ($ class );
66157
67158 if (! $ classReflection ->isClass ()) {
68- throw new Exception ('Class is not class ' );
159+ throw new InvalidArgumentException ('Class string does not resolve to class ' );
69160 }
70161
71162 if (! $ classReflection ->isSubclassOfClass ($ this ->reflectionProvider ->getClass (Model::class))) {
72- throw new Exception ('Class is not subclass of Model ' );
163+ throw new InvalidArgumentException ('Class is not subclass of Model ' );
73164 }
74165
75166 return $ classReflection ;
76167 }
168+
169+ /**
170+ * Create an instance of the Model to interact with
171+ *
172+ * @param class-string<Model>|ObjectType $model
173+ *
174+ * @throws ReflectionException
175+ */
176+ private function resolveModelClassToInstance (string |ObjectType $ model ): ?Model
177+ {
178+ $ classReflection = is_string ($ model )
179+ ? $ this ->getClass ($ model )
180+ : $ model ->getObjectClassReflections ()[0 ];
181+
182+ if ($ classReflection ->isAbstract ()) {
183+ return null ;
184+ }
185+
186+ try {
187+ /** @var Model $instance */
188+ $ instance = $ classReflection ->getNativeReflection ()->newInstance ();
189+ } catch (Throwable ) {
190+ /** @var Model $instance */
191+ $ instance = $ classReflection ->getNativeReflection ()->newInstanceWithoutConstructor ();
192+ }
193+
194+ return $ instance ;
195+ }
77196}
0 commit comments