1212use PHPStan \Analyser \Scope ;
1313use PHPStan \PhpDocParser \Ast \PhpDoc \ReturnTagValueNode ;
1414use PHPStan \PhpDocParser \Ast \Type \GenericTypeNode ;
15+ use PHPStan \PhpDocParser \Ast \Type \IdentifierTypeNode ;
1516use PHPStan \Reflection \ClassReflection ;
1617use PHPStan \Type \Constant \ConstantStringType ;
1718use PHPStan \Type \Generic \GenericClassStringType ;
1819use PHPStan \Type \Generic \GenericObjectType ;
1920use PHPStan \Type \ObjectType ;
21+ use PHPStan \Type \ThisType ;
2022use Rector \BetterPhpDocParser \PhpDocInfo \PhpDocInfoFactory ;
2123use Rector \BetterPhpDocParser \ValueObject \Type \FullyQualifiedIdentifierTypeNode ;
2224use Rector \Comments \NodeDocBlock \DocBlockUpdater ;
25+ use Rector \Contract \Rector \ConfigurableRectorInterface ;
2326use Rector \NodeTypeResolver \TypeComparator \TypeComparator ;
2427use Rector \PhpParser \Node \BetterNodeFinder ;
2528use Rector \Rector \AbstractScopeAwareRector ;
2629use Rector \StaticTypeMapper \StaticTypeMapper ;
27- use Symplify \RuleDocGenerator \ValueObject \CodeSample \CodeSample ;
30+ use Symplify \RuleDocGenerator \ValueObject \CodeSample \ConfiguredCodeSample ;
2831use Symplify \RuleDocGenerator \ValueObject \RuleDefinition ;
32+ use Webmozart \Assert \Assert ;
2933
30- /** @see \RectorLaravel\Tests\Rector\ClassMethod\AddGenericReturnTypeToRelationsRector\AddGenericReturnTypeToRelationsRectorTest */
31- class AddGenericReturnTypeToRelationsRector extends AbstractScopeAwareRector
34+ /**
35+ * @see \RectorLaravel\Tests\Rector\ClassMethod\AddGenericReturnTypeToRelationsRector\AddGenericReturnTypeToRelationsRectorNewGenericsTest
36+ * @see \RectorLaravel\Tests\Rector\ClassMethod\AddGenericReturnTypeToRelationsRector\AddGenericReturnTypeToRelationsRectorOldGenericsTest
37+ */
38+ class AddGenericReturnTypeToRelationsRector extends AbstractScopeAwareRector implements ConfigurableRectorInterface
3239{
3340 // Relation methods which are supported by this Rector.
3441 private const RELATION_METHODS = [
@@ -41,6 +48,11 @@ class AddGenericReturnTypeToRelationsRector extends AbstractScopeAwareRector
4148 // Relation methods which need the class as TChildModel.
4249 private const RELATION_WITH_CHILD_METHODS = ['belongsTo ' , 'morphTo ' ];
4350
51+ // Relation methods which need the class as TIntermediateModel.
52+ private const RELATION_WITH_INTERMEDIATE_METHODS = ['hasManyThrough ' , 'hasOneThrough ' ];
53+
54+ private bool $ shouldUseNewGenerics = false ;
55+
4456 public function __construct (
4557 private readonly TypeComparator $ typeComparator ,
4658 private readonly DocBlockUpdater $ docBlockUpdater ,
@@ -55,7 +67,7 @@ public function getRuleDefinition(): RuleDefinition
5567 return new RuleDefinition (
5668 'Add generic return type to relations in child of Illuminate\Database\Eloquent\Model ' ,
5769 [
58- new CodeSample (
70+ new ConfiguredCodeSample (
5971 <<<'CODE_SAMPLE'
6072use App\Account;
6173use Illuminate\Database\Eloquent\Model;
@@ -84,8 +96,39 @@ public function accounts(): HasMany
8496 return $this->hasMany(Account::class);
8597 }
8698}
99+ CODE_SAMPLE,
100+ ['shouldUseNewGenerics ' => false ]),
101+ new ConfiguredCodeSample (
102+ <<<'CODE_SAMPLE'
103+ use App\Account;
104+ use Illuminate\Database\Eloquent\Model;
105+ use Illuminate\Database\Eloquent\Relations\HasMany;
106+
107+ class User extends Model
108+ {
109+ public function accounts(): HasMany
110+ {
111+ return $this->hasMany(Account::class);
112+ }
113+ }
87114CODE_SAMPLE
88- ),
115+
116+ ,
117+ <<<'CODE_SAMPLE'
118+ use App\Account;
119+ use Illuminate\Database\Eloquent\Model;
120+ use Illuminate\Database\Eloquent\Relations\HasMany;
121+
122+ class User extends Model
123+ {
124+ /** @return HasMany<Account, $this> */
125+ public function accounts(): HasMany
126+ {
127+ return $this->hasMany(Account::class);
128+ }
129+ }
130+ CODE_SAMPLE,
131+ ['shouldUseNewGenerics ' => true ]),
89132 ]
90133 );
91134 }
@@ -154,6 +197,7 @@ public function refactorWithScope(Node $node, Scope $scope): ?Node
154197 }
155198
156199 $ classForChildGeneric = $ this ->getClassForChildGeneric ($ scope , $ relationMethodCall );
200+ $ classForIntermediateGeneric = $ this ->getClassForIntermediateGeneric ($ relationMethodCall );
157201
158202 // Don't update the docblock if return type already contains the correct generics. This avoids overwriting
159203 // non-FQCN with our fully qualified ones.
@@ -163,15 +207,16 @@ public function refactorWithScope(Node $node, Scope $scope): ?Node
163207 $ node ,
164208 $ phpDocInfo ->getReturnTagValue (),
165209 $ relatedClass ,
166- $ classForChildGeneric
210+ $ classForChildGeneric ,
211+ $ classForIntermediateGeneric
167212 )
168213 ) {
169214 return null ;
170215 }
171216
172217 $ genericTypeNode = new GenericTypeNode (
173218 new FullyQualifiedIdentifierTypeNode ($ methodReturnTypeName ),
174- $ this ->getGenericTypes ($ relatedClass , $ classForChildGeneric ),
219+ $ this ->getGenericTypes ($ relatedClass , $ classForChildGeneric, $ classForIntermediateGeneric ),
175220 );
176221
177222 // Update or add return tag
@@ -187,6 +232,18 @@ public function refactorWithScope(Node $node, Scope $scope): ?Node
187232 return $ node ;
188233 }
189234
235+ /**
236+ * {@inheritDoc}
237+ */
238+ public function configure (array $ configuration ): void
239+ {
240+ Assert::count ($ configuration , 1 );
241+ Assert::keyExists ($ configuration , 'shouldUseNewGenerics ' );
242+ Assert::boolean ($ configuration ['shouldUseNewGenerics ' ]);
243+
244+ $ this ->shouldUseNewGenerics = $ configuration ['shouldUseNewGenerics ' ];
245+ }
246+
190247 private function getRelatedModelClassFromMethodCall (MethodCall $ methodCall ): ?string
191248 {
192249 $ argType = $ this ->getType ($ methodCall ->getArgs ()[0 ]->value );
@@ -243,6 +300,10 @@ private function getRelationMethodCall(ClassMethod $classMethod): ?MethodCall
243300 */
244301 private function getClassForChildGeneric (Scope $ scope , MethodCall $ methodCall ): ?string
245302 {
303+ if ($ this ->shouldUseNewGenerics ) {
304+ return null ;
305+ }
306+
246307 if (! $ this ->doesMethodHasName ($ methodCall , self ::RELATION_WITH_CHILD_METHODS )) {
247308 return null ;
248309 }
@@ -252,6 +313,45 @@ private function getClassForChildGeneric(Scope $scope, MethodCall $methodCall):
252313 return $ classReflection ?->getName();
253314 }
254315
316+ /**
317+ * We need the intermediate class for generics which need a TIntermediateModel.
318+ * This is the case for *through relations
319+ */
320+ private function getClassForIntermediateGeneric (MethodCall $ methodCall ): ?string
321+ {
322+ if (! $ this ->shouldUseNewGenerics ) {
323+ return null ;
324+ }
325+
326+ if (! $ this ->doesMethodHasName ($ methodCall , self ::RELATION_WITH_INTERMEDIATE_METHODS )) {
327+ return null ;
328+ }
329+
330+ $ args = $ methodCall ->getArgs ();
331+
332+ if (count ($ args ) < 2 ) {
333+ return null ;
334+ }
335+
336+ $ argType = $ this ->getType ($ args [1 ]->value );
337+
338+ if ($ argType instanceof ConstantStringType && $ argType ->isClassStringType ()->yes ()) {
339+ return $ argType ->getValue ();
340+ }
341+
342+ if (! $ argType instanceof GenericClassStringType) {
343+ return null ;
344+ }
345+
346+ $ modelType = $ argType ->getGenericType ();
347+
348+ if (! $ modelType instanceof ObjectType) {
349+ return null ;
350+ }
351+
352+ return $ modelType ->getClassName ();
353+ }
354+
255355 private function areNativeTypeAndPhpDocReturnTypeEqual (
256356 ClassMethod $ classMethod ,
257357 Node $ node ,
@@ -279,7 +379,8 @@ private function areGenericTypesEqual(
279379 Node $ node ,
280380 ReturnTagValueNode $ returnTagValueNode ,
281381 string $ relatedClass ,
282- ?string $ classForChildGeneric
382+ ?string $ classForChildGeneric ,
383+ ?string $ classForIntermediateGeneric
283384 ): bool {
284385 $ phpDocPHPStanType = $ this ->staticTypeMapper ->mapPHPStanPhpDocTypeNodeToPHPStanType (
285386 $ returnTagValueNode ->type ,
@@ -299,16 +400,37 @@ private function areGenericTypesEqual(
299400 return false ;
300401 }
301402
302- $ phpDocHasChildGeneric = count ($ phpDocTypes ) === 2 ;
303- if ($ classForChildGeneric === null && ! $ phpDocHasChildGeneric ) {
304- return true ;
403+ if (! $ this ->shouldUseNewGenerics ) {
404+ $ phpDocHasChildGeneric = count ($ phpDocTypes ) === 2 ;
405+
406+ if ($ classForChildGeneric === null && ! $ phpDocHasChildGeneric ) {
407+ return true ;
408+ }
409+
410+ if ($ classForChildGeneric === null || ! $ phpDocHasChildGeneric ) {
411+ return false ;
412+ }
413+
414+ return $ this ->typeComparator ->areTypesEqual ($ phpDocTypes [1 ], new ObjectType ($ classForChildGeneric ));
305415 }
306416
307- if ($ classForChildGeneric === null || ! $ phpDocHasChildGeneric ) {
417+ $ phpDocHasIntermediateGeneric = count ($ phpDocTypes ) === 3 ;
418+
419+ if ($ classForIntermediateGeneric === null && ! $ phpDocHasIntermediateGeneric ) {
420+ // If there is only one generic, it means method is using the old format. We should update it.
421+ if (count ($ phpDocTypes ) === 1 ) {
422+ return false ;
423+ }
424+
425+ // We want to convert the existing relationship definition to use `$this` as the second generic
426+ return $ phpDocTypes [1 ] instanceof ThisType;
427+ }
428+
429+ if ($ classForIntermediateGeneric === null || ! $ phpDocHasIntermediateGeneric ) {
308430 return false ;
309431 }
310432
311- return $ this ->typeComparator ->areTypesEqual ($ phpDocTypes [1 ], new ObjectType ($ classForChildGeneric ));
433+ return $ this ->typeComparator ->areTypesEqual ($ phpDocTypes [1 ], new ObjectType ($ classForIntermediateGeneric ));
312434 }
313435
314436 private function shouldSkipNode (ClassMethod $ classMethod , Scope $ scope ): bool
@@ -341,16 +463,24 @@ private function doesMethodHasName(MethodCall $methodCall, array $methodNames):
341463 }
342464
343465 /**
344- * @return FullyQualifiedIdentifierTypeNode []
466+ * @return IdentifierTypeNode []
345467 */
346- private function getGenericTypes (string $ relatedClass , ?string $ childClass ): array
468+ private function getGenericTypes (string $ relatedClass , ?string $ childClass, ? string $ intermediateClass ): array
347469 {
348470 $ generics = [new FullyQualifiedIdentifierTypeNode ($ relatedClass )];
349471
350- if ($ childClass !== null ) {
472+ if (! $ this -> shouldUseNewGenerics && $ childClass !== null ) {
351473 $ generics [] = new FullyQualifiedIdentifierTypeNode ($ childClass );
352474 }
353475
476+ if ($ this ->shouldUseNewGenerics ) {
477+ if ($ intermediateClass !== null ) {
478+ $ generics [] = new FullyQualifiedIdentifierTypeNode ($ intermediateClass );
479+ }
480+
481+ $ generics [] = new IdentifierTypeNode ('$this ' );
482+ }
483+
354484 return $ generics ;
355485 }
356486}
0 commit comments