@@ -35,37 +35,46 @@ public function narrow(
3535 }
3636
3737 if ($ tagValueNode ->type instanceof NullableTypeNode) {
38- if ($ hasNativeCollectionType ) {
39- $ tagValueNode ->type = $ tagValueNode ->type ->type ;
40- $ tagValueNode ->setAttribute (PhpDocAttributeKey::ORIG_NODE , null );
41-
42- $ collectionType = $ tagValueNode ->type ;
43- $ this ->addIntKeyIfMissing ($ collectionType );
44-
45- if ($ collectionType ->type instanceof IdentifierTypeNode && ! str_ends_with (
46- $ collectionType ->type ->name ,
47- 'Collection '
48- )) {
49- $ collectionType ->type = new FullyQualifiedIdentifierTypeNode (DoctrineClass::COLLECTION );
50- }
38+ return $ this ->processNullableTypeNode ($ hasNativeCollectionType , $ tagValueNode );
39+ }
5140
52- return true ;
53- }
41+ return $ this -> processIterableAndUnionTypeNode ( $ tagValueNode , $ hasNativeCollectionType ) ;
42+ }
5443
55- return false ;
44+ private function addIntKeyIfMissing (TypeNode |IdentifierTypeNode $ collectionType ): void
45+ {
46+ if (! $ collectionType instanceof GenericTypeNode) {
47+ return ;
48+ }
49+
50+ if (count ($ collectionType ->genericTypes ) !== 1 ) {
51+ return ;
5652 }
5753
54+ // add default key type
55+ $ collectionType ->genericTypes = array_merge ([new IdentifierTypeNode ('int ' )], $ collectionType ->genericTypes );
56+ }
57+
58+ private function processIterableAndUnionTypeNode (
59+ ParamTagValueNode |VarTagValueNode |ReturnTagValueNode $ tagValueNode ,
60+ bool $ hasNativeCollectionType
61+ ): bool {
5862 if (! $ tagValueNode ->type instanceof UnionTypeNode && ! $ tagValueNode ->type instanceof IntersectionTypeNode) {
5963 return false ;
6064 }
6165
6266 $ hasChanged = false ;
63- $ hasCollectionType = false ;
6467 $ hasArrayType = false ;
6568 $ arrayTypeNode = null ;
6669 $ arrayKeyTypeNode = null ;
6770
68- foreach ($ tagValueNode ->type ->types as $ key => $ unionedTypeNode ) {
71+ // has collection docblock type?
72+ $ hasCollectionType = $ this ->hasCollectionDocblockType ($ tagValueNode ->type );
73+ $ hasGenericIterableType = false ;
74+
75+ $ complexTypeNode = $ tagValueNode ->type ;
76+
77+ foreach ($ complexTypeNode ->types as $ key => $ unionedTypeNode ) {
6978 // possibly array<key, value>
7079 if ($ unionedTypeNode instanceof GenericTypeNode && $ unionedTypeNode ->type ->name === 'array ' ) {
7180 $ hasArrayType = true ;
@@ -88,36 +97,46 @@ public function narrow(
8897 }
8998
9099 // remove |null, if property type is present as Collection
91- if ($ unionedTypeNode instanceof IdentifierTypeNode && $ unionedTypeNode -> name === ' null ' && $ hasNativeCollectionType ) {
92-
93- $ hasChanged = true ;
94- unset($ tagValueNode ->type ->types [$ key ]);
95- continue ;
96- }
100+ if ($ unionedTypeNode instanceof IdentifierTypeNode) {
101+ if ( $ unionedTypeNode -> name === ' null ' && $ hasNativeCollectionType ) {
102+ $ hasChanged = true ;
103+ unset($ tagValueNode ->type ->types [$ key ]);
104+ continue ;
105+ }
97106
98- if ($ unionedTypeNode instanceof IdentifierTypeNode && in_array (
99- $ unionedTypeNode ->name ,
100- ['Collection ' , 'ArrayCollection ' ]
101- )) {
102107 if ($ unionedTypeNode ->name === 'ArrayCollection ' ) {
103108 $ tagValueNode ->type ->types [$ key ] = new IdentifierTypeNode ('\\' . DoctrineClass::COLLECTION );
104109 $ hasChanged = true ;
105110 }
106-
107- $ hasCollectionType = true ;
108111 }
109112
110113 // narrow array collection to more generic collection
111- if ($ unionedTypeNode instanceof GenericTypeNode && $ unionedTypeNode ->type ->name === 'ArrayCollection ' ) {
114+ if ($ unionedTypeNode instanceof GenericTypeNode && in_array (
115+ $ unionedTypeNode ->type ->name ,
116+ ['ArrayCollection ' , 'iterable ' ],
117+ true
118+ )) {
112119 $ unionedTypeNode ->type = new IdentifierTypeNode ('\\' . DoctrineClass::COLLECTION );
113120 $ hasChanged = true ;
121+
122+ $ hasGenericIterableType = true ;
114123 }
115124 }
116125
117126 if (($ hasArrayType === false || $ hasCollectionType === false ) && $ hasChanged === false ) {
118127 return false ;
119128 }
120129
130+ // remove duplicated Collection and Collection generics type
131+ if ($ hasCollectionType && $ hasGenericIterableType ) {
132+ foreach ($ complexTypeNode ->types as $ key => $ singleType ) {
133+ if ($ this ->isCollectionIdentifierTypeNode ($ singleType )) {
134+ // remove as has generic iterable type already
135+ unset($ complexTypeNode ->types [$ key ]);
136+ }
137+ }
138+ }
139+
121140 if ($ arrayTypeNode instanceof TypeNode) {
122141 $ tagValueNode ->type = new GenericTypeNode (new IdentifierTypeNode ('\\' . DoctrineClass::COLLECTION ), [
123142 $ arrayKeyTypeNode ?? new IdentifierTypeNode ('int ' ),
@@ -130,6 +149,7 @@ public function narrow(
130149 }
131150
132151 if ($ hasNativeCollectionType && $ type ->name === 'null ' ) {
152+ // remove null type
133153 unset($ tagValueNode ->type ->types [$ key ]);
134154 continue ;
135155 }
@@ -154,11 +174,53 @@ public function narrow(
154174 return true ;
155175 }
156176
157- private function addIntKeyIfMissing ( TypeNode | IdentifierTypeNode $ collectionType ): void
177+ private function hasCollectionDocblockType ( UnionTypeNode | IntersectionTypeNode $ complexTypeNode ): bool
158178 {
159- if ($ collectionType instanceof GenericTypeNode && count ($ collectionType ->genericTypes ) === 1 ) {
160- // add default key type
161- $ collectionType ->genericTypes = array_merge ([new IdentifierTypeNode ('int ' )], $ collectionType ->genericTypes );
179+ foreach ($ complexTypeNode ->types as $ singleType ) {
180+ if ($ this ->isCollectionIdentifierTypeNode ($ singleType )) {
181+ return true ;
182+ }
183+ }
184+
185+ return false ;
186+ }
187+
188+ private function processNullableTypeNode (
189+ bool $ hasNativeCollectionType ,
190+ ParamTagValueNode |VarTagValueNode |ReturnTagValueNode $ tagValueNode
191+ ): bool {
192+ if ($ hasNativeCollectionType === false ) {
193+ return false ;
162194 }
195+
196+ // unwrap nullable type
197+ $ tagValueNode ->type = $ tagValueNode ->type ->type ;
198+
199+ // invoke reprint
200+ $ tagValueNode ->setAttribute (PhpDocAttributeKey::ORIG_NODE , null );
201+
202+ $ collectionType = $ tagValueNode ->type ;
203+ $ this ->addIntKeyIfMissing ($ collectionType );
204+
205+ if ($ collectionType ->type instanceof IdentifierTypeNode && ! str_ends_with (
206+ $ collectionType ->type ->name ,
207+ 'Collection '
208+ )) {
209+ $ collectionType ->type = new FullyQualifiedIdentifierTypeNode (DoctrineClass::COLLECTION );
210+ }
211+
212+ return true ;
213+ }
214+
215+ private function isCollectionIdentifierTypeNode (TypeNode $ typeNode ): bool
216+ {
217+ if (! $ typeNode instanceof IdentifierTypeNode) {
218+ return false ;
219+ }
220+
221+ return in_array (
222+ $ typeNode ->name ,
223+ [DoctrineClass::COLLECTION , DoctrineClass::ARRAY_COLLECTION , 'Collection ' , 'ArrayCollection ' ]
224+ );
163225 }
164226}
0 commit comments