5
5
namespace Rector \Symfony \CodeQuality \Rector \Class_ ;
6
6
7
7
use PhpParser \Node ;
8
- use PhpParser \Node \Name ;
8
+ use PhpParser \Node \Attribute ;
9
+ use PhpParser \Node \Scalar \String_ ;
9
10
use PhpParser \Node \Stmt \Class_ ;
10
11
use Rector \BetterPhpDocParser \PhpDoc \ArrayItemNode ;
11
12
use Rector \BetterPhpDocParser \PhpDoc \DoctrineAnnotationTagValueNode ;
12
13
use Rector \BetterPhpDocParser \PhpDoc \StringNode ;
13
- use Rector \BetterPhpDocParser \PhpDocInfo \PhpDocInfo ;
14
14
use Rector \BetterPhpDocParser \PhpDocInfo \PhpDocInfoFactory ;
15
15
use Rector \BetterPhpDocParser \PhpDocManipulator \PhpDocTagRemover ;
16
16
use Rector \Comments \NodeDocBlock \DocBlockUpdater ;
17
+ use Rector \Doctrine \NodeAnalyzer \AttrinationFinder ;
17
18
use Rector \Rector \AbstractRector ;
18
19
use Rector \Symfony \Enum \FosAnnotation ;
19
20
use Rector \Symfony \Enum \SymfonyAnnotation ;
21
+ use Rector \Symfony \Enum \SymfonyAttribute ;
20
22
use Rector \Symfony \TypeAnalyzer \ControllerAnalyzer ;
21
23
use Symplify \RuleDocGenerator \ValueObject \CodeSample \CodeSample ;
22
24
use Symplify \RuleDocGenerator \ValueObject \RuleDefinition ;
@@ -29,17 +31,23 @@ final class InlineClassRoutePrefixRector extends AbstractRector
29
31
/**
30
32
* @var string[]
31
33
*/
32
- private const SKIPPED_ANNOTATIONS = [
34
+ private const FOS_REST_ANNOTATIONS = [
33
35
FosAnnotation::REST_POST ,
34
36
FosAnnotation::REST_GET ,
35
37
FosAnnotation::REST_ROUTE ,
36
38
];
37
39
40
+ /**
41
+ * @var string
42
+ */
43
+ private const PATH = 'path ' ;
44
+
38
45
public function __construct (
39
46
private readonly PhpDocInfoFactory $ phpDocInfoFactory ,
40
47
private readonly PhpDocTagRemover $ phpDocTagRemover ,
41
48
private readonly DocBlockUpdater $ docBlockUpdater ,
42
49
private readonly ControllerAnalyzer $ controllerAnalyzer ,
50
+ private readonly AttrinationFinder $ attrinationFinder
43
51
) {
44
52
}
45
53
@@ -80,7 +88,6 @@ public function action()
80
88
}
81
89
CODE_SAMPLE
82
90
),
83
-
84
91
]
85
92
);
86
93
}
@@ -99,109 +106,150 @@ public function refactor(Node $node): ?Class_
99
106
return null ;
100
107
}
101
108
102
- // 1. detect and remove class-level Route annotation
103
- $ classPhpDocInfo = $ this ->phpDocInfoFactory ->createFromNode ($ node );
104
- if (! $ classPhpDocInfo instanceof PhpDocInfo) {
105
- return null ;
106
- }
109
+ $ classRoutePath = null ;
107
110
108
- $ classRouteTagValueNode = $ classPhpDocInfo ->getByAnnotationClass (SymfonyAnnotation::ROUTE );
109
- if (! $ classRouteTagValueNode instanceof DoctrineAnnotationTagValueNode) {
110
- return null ;
111
- }
111
+ // 1. detect attribute
112
+ $ routeAttributeOrAnnotation = $ this ->attrinationFinder ->getByMany (
113
+ $ node ,
114
+ [SymfonyAttribute::ROUTE , SymfonyAnnotation::ROUTE ]
115
+ );
112
116
113
- $ classRoutePathNode = $ classRouteTagValueNode ->getSilentValue () ?: $ classRouteTagValueNode ->getValue ('path ' );
114
- if (! $ classRoutePathNode instanceof ArrayItemNode) {
115
- return null ;
117
+ if ($ routeAttributeOrAnnotation instanceof DoctrineAnnotationTagValueNode) {
118
+ $ classRoutePath = $ this ->resolveRoutePath ($ routeAttributeOrAnnotation );
119
+ } elseif ($ routeAttributeOrAnnotation instanceof Attribute) {
120
+ $ classRoutePath = $ this ->resolveRoutePathFromAttribute ($ routeAttributeOrAnnotation );
116
121
}
117
122
118
- if (! $ classRoutePathNode -> value instanceof StringNode ) {
123
+ if ($ classRoutePath === null ) {
119
124
return null ;
120
125
}
121
126
122
- $ classRoutePath = $ classRoutePathNode ->value ->value ;
123
-
124
127
// 2. inline prefix to all method routes
125
128
$ hasChanged = false ;
126
129
127
130
foreach ($ node ->getMethods () as $ classMethod ) {
128
- if (! $ classMethod ->isPublic ()) {
129
- continue ;
130
- }
131
-
132
- if ($ classMethod ->isMagic ()) {
131
+ if (! $ classMethod ->isPublic () || $ classMethod ->isMagic ()) {
133
132
continue ;
134
133
}
135
134
136
135
// can be route method
137
- $ methodPhpDocInfo = $ this ->phpDocInfoFactory ->createFromNode ($ classMethod );
138
- if (! $ methodPhpDocInfo instanceof PhpDocInfo) {
139
- continue ;
140
- }
141
-
142
- $ methodRouteTagValueNodes = $ methodPhpDocInfo ->findByAnnotationClass (SymfonyAnnotation::ROUTE );
143
- foreach ($ methodRouteTagValueNodes as $ methodRouteTagValueNode ) {
144
- $ routePathArrayItemNode = $ methodRouteTagValueNode ->getSilentValue () ?? $ methodRouteTagValueNode ->getValue (
145
- 'path '
146
- );
147
- if (! $ routePathArrayItemNode instanceof ArrayItemNode) {
148
- continue ;
149
- }
150
-
151
- if (! $ routePathArrayItemNode ->value instanceof StringNode) {
152
- continue ;
136
+ $ methodRouteAnnotationOrAttributes = $ this ->attrinationFinder ->findManyByMany (
137
+ $ classMethod ,
138
+ [SymfonyAttribute::ROUTE , SymfonyAnnotation::ROUTE ]
139
+ );
140
+
141
+ foreach ($ methodRouteAnnotationOrAttributes as $ methodRouteAnnotationOrAttribute ) {
142
+ if ($ methodRouteAnnotationOrAttribute instanceof DoctrineAnnotationTagValueNode) {
143
+ $ routePathArrayItemNode = $ methodRouteAnnotationOrAttribute ->getSilentValue () ?? $ methodRouteAnnotationOrAttribute ->getValue (
144
+ self ::PATH
145
+ );
146
+ if (! $ routePathArrayItemNode instanceof ArrayItemNode) {
147
+ continue ;
148
+ }
149
+
150
+ if (! $ routePathArrayItemNode ->value instanceof StringNode) {
151
+ continue ;
152
+ }
153
+
154
+ $ methodPrefix = $ routePathArrayItemNode ->value ;
155
+ $ newMethodPath = $ classRoutePath . $ methodPrefix ->value ;
156
+
157
+ $ routePathArrayItemNode ->value = new StringNode ($ newMethodPath );
158
+ $ this ->docBlockUpdater ->updateRefactoredNodeWithPhpDocInfo ($ classMethod );
159
+
160
+ $ hasChanged = true ;
161
+ } elseif ($ methodRouteAnnotationOrAttribute instanceof Attribute) {
162
+ foreach ($ methodRouteAnnotationOrAttribute ->args as $ methodRouteArg ) {
163
+ if ($ methodRouteArg ->name === null || $ methodRouteArg ->name ->toString () === self ::PATH ) {
164
+ if (! $ methodRouteArg ->value instanceof String_) {
165
+ continue ;
166
+ }
167
+
168
+ $ methodRouteString = $ methodRouteArg ->value ;
169
+ $ methodRouteArg ->value = new String_ (sprintf (
170
+ '%s%s ' ,
171
+ $ classRoutePath ,
172
+ $ methodRouteString ->value
173
+ ));
174
+
175
+ $ hasChanged = true ;
176
+ }
177
+ }
153
178
}
154
-
155
- $ methodPrefix = $ routePathArrayItemNode ->value ;
156
- $ newMethodPath = $ classRoutePath . $ methodPrefix ->value ;
157
-
158
- $ routePathArrayItemNode ->value = new StringNode ($ newMethodPath );
159
- $ this ->docBlockUpdater ->updateRefactoredNodeWithPhpDocInfo ($ classMethod );
160
-
161
- $ hasChanged = true ;
162
179
}
163
180
}
164
181
165
182
if (! $ hasChanged ) {
166
183
return null ;
167
184
}
168
185
169
- $ this ->phpDocTagRemover ->removeTagValueFromNode ($ classPhpDocInfo , $ classRouteTagValueNode );
170
- $ this ->docBlockUpdater ->updateRefactoredNodeWithPhpDocInfo ($ node );
186
+ if ($ routeAttributeOrAnnotation instanceof DoctrineAnnotationTagValueNode) {
187
+ $ classPhpDocInfo = $ this ->phpDocInfoFactory ->createFromNodeOrEmpty ($ node );
188
+
189
+ $ this ->phpDocTagRemover ->removeTagValueFromNode ($ classPhpDocInfo , $ routeAttributeOrAnnotation );
190
+ $ this ->docBlockUpdater ->updateRefactoredNodeWithPhpDocInfo ($ node );
191
+ } else {
192
+ foreach ($ node ->attrGroups as $ attrGroupKey => $ attrGroup ) {
193
+ foreach ($ attrGroup ->attrs as $ attribute ) {
194
+ if ($ attribute === $ routeAttributeOrAnnotation ) {
195
+ unset($ node ->attrGroups [$ attrGroupKey ]);
196
+ }
197
+ }
198
+ }
199
+ }
171
200
172
201
return $ node ;
173
202
}
174
203
175
204
private function shouldSkipClass (Class_ $ class ): bool
176
205
{
177
- if (! $ class ->extends instanceof Name) {
178
- return true ;
179
- }
180
-
181
206
if (! $ this ->controllerAnalyzer ->isController ($ class )) {
182
207
return true ;
183
208
}
184
209
185
210
foreach ($ class ->getMethods () as $ classMethod ) {
186
- if (! $ classMethod ->isPublic ()) {
187
- continue ;
188
- }
189
-
190
- if ($ classMethod ->isMagic ()) {
191
- continue ;
192
- }
193
-
194
- $ classMethodPhpDocInfo = $ this ->phpDocInfoFactory ->createFromNode ($ classMethod );
195
- if (! $ classMethodPhpDocInfo instanceof PhpDocInfo) {
211
+ if (! $ classMethod ->isPublic () || $ classMethod ->isMagic ()) {
196
212
continue ;
197
213
}
198
214
199
215
// special cases for FOS rest that should be skipped
200
- if ($ classMethodPhpDocInfo -> hasByAnnotationClasses ( self ::SKIPPED_ANNOTATIONS )) {
216
+ if ($ this -> attrinationFinder -> hasByMany ( $ class , self ::FOS_REST_ANNOTATIONS )) {
201
217
return true ;
202
218
}
203
219
}
204
220
205
221
return false ;
206
222
}
223
+
224
+ private function resolveRoutePath (DoctrineAnnotationTagValueNode $ doctrineAnnotationTagValueNode ): ?string
225
+ {
226
+ $ classRoutePathNode = $ doctrineAnnotationTagValueNode ->getSilentValue () ?: $ doctrineAnnotationTagValueNode ->getValue (
227
+ self ::PATH
228
+ );
229
+
230
+ if (! $ classRoutePathNode instanceof ArrayItemNode) {
231
+ return null ;
232
+ }
233
+
234
+ if (! $ classRoutePathNode ->value instanceof StringNode) {
235
+ return null ;
236
+ }
237
+
238
+ return $ classRoutePathNode ->value ->value ;
239
+ }
240
+
241
+ private function resolveRoutePathFromAttribute (Attribute $ attribute ): ?string
242
+ {
243
+ foreach ($ attribute ->args as $ arg ) {
244
+ // silent or "path"
245
+ if ($ arg ->name === null || $ arg ->name ->toString () === self ::PATH ) {
246
+ $ routeExpr = $ arg ->value ;
247
+ if ($ routeExpr instanceof String_) {
248
+ return $ routeExpr ->value ;
249
+ }
250
+ }
251
+ }
252
+
253
+ return null ;
254
+ }
207
255
}
0 commit comments