1919
2020use Biurad \Annotations \AnnotationLoader ;
2121use Doctrine \Common \Annotations \AnnotationRegistry ;
22+ use PhpParser \Node ;
23+ use PhpParser \Node \Stmt \Class_ ;
24+ use PhpParser \Node \Stmt \Namespace_ ;
25+ use PhpParser \NodeTraverser ;
26+ use PhpParser \NodeVisitorAbstract ;
27+ use PhpParser \ParserFactory ;
2228use PHPUnit \Framework \TestCase ;
2329use Spiral \Attributes \AnnotationReader ;
2430use Spiral \Attributes \AttributeReader ;
@@ -37,11 +43,12 @@ protected function setUp(): void
3743 }
3844
3945 /**
46+ * @dataProvider provideAnnotationLoader
4047 * @runInSeparateProcess
4148 */
42- public function testAttach (): void
49+ public function testAttach ($ loader ): void
4350 {
44- $ annotation = new AnnotationLoader (new AnnotationReader ());
51+ $ annotation = new AnnotationLoader (new AnnotationReader (), $ loader );
4552 $ result = $ names = [];
4653
4754 $ annotation ->attachListener (new Fixtures \SampleListener ());
@@ -113,11 +120,12 @@ public function testAttach(): void
113120 }
114121
115122 /**
123+ * @dataProvider provideAnnotationLoader
116124 * @runInSeparateProcess
117125 */
118- public function testAttachAttribute (): void
126+ public function testAttachAttribute ($ loader ): void
119127 {
120- $ annotation = new AnnotationLoader (new AttributeReader ());
128+ $ annotation = new AnnotationLoader (new AttributeReader (), $ loader );
121129 $ result = [];
122130
123131 $ annotation ->attachListener (new Fixtures \SampleListener ());
@@ -150,4 +158,134 @@ public function testAttachAttribute(): void
150158 'attribute_added_method_property ' => ['handler ' => \ReflectionParameter::class, 'priority ' => 4 ],
151159 ], $ result );
152160 }
161+
162+ public function provideAnnotationLoader (): array
163+ {
164+ return [
165+ 'Default Class Loader ' => [null ],
166+ 'Token Class Loader ' => [[$ this , 'tokenClassLoader ' ]],
167+ 'Node Class Loader ' => [[$ this , 'nodeClassLoader ' ]],
168+ ];
169+ }
170+
171+ public function tokenClassLoader (array $ files ): array
172+ {
173+ if (!\function_exists ('token_get_all ' )) {
174+ $ this ->markTestSkipped ('The Tokenizer extension is required for the annotation loader. ' );
175+ }
176+ $ classes = [];
177+
178+ foreach ($ files as $ file ) {
179+ $ classes [] = $ this ->findClassByToken ($ file );
180+ }
181+
182+ return $ classes ;
183+ }
184+
185+ public function nodeClassLoader (array $ files ): array
186+ {
187+ if (!\interface_exists (Node::class)) {
188+ $ this ->markTestSkipped ('The PhpParser is required for the annotation loader. ' );
189+ }
190+ $ classes = [];
191+
192+ foreach ($ files as $ file ) {
193+ $ classes [] = $ this ->findClassByNode ($ file );
194+ }
195+
196+ return $ classes ;
197+ }
198+
199+ protected function findClassByNode (string $ file )
200+ {
201+ $ parser = (new ParserFactory ())->create (ParserFactory::PREFER_PHP7 );
202+ $ ast = $ parser ->parse (file_get_contents ($ file ));
203+ $ traverser = new NodeTraverser ();
204+
205+ $ traverser ->addVisitor ($ class = new class () extends NodeVisitorAbstract {
206+ private $ className = null ;
207+
208+ public function enterNode (Node $ node )
209+ {
210+ if ($ node instanceof Namespace_) {
211+ // Clean out the function body
212+ $ this ->className = join ('\\' , $ node ->name ->parts ) . '\\' ;
213+ } elseif ($ node instanceof Class_) {
214+ $ this ->className .= $ node ->name ->name ;
215+ }
216+ }
217+
218+ public function getClassName ()
219+ {
220+ return $ this ->className ;
221+ }
222+ });
223+ $ traverser ->traverse ($ ast );
224+
225+ return $ class ->getClassName ();
226+ }
227+
228+
229+ protected function findClassByToken (string $ file )
230+ {
231+ $ class = false ;
232+ $ namespace = false ;
233+ $ tokens = token_get_all (file_get_contents ($ file ));
234+
235+ if (1 === \count ($ tokens ) && \T_INLINE_HTML === $ tokens [0 ][0 ]) {
236+ throw new \InvalidArgumentException (sprintf ('The file "%s" does not contain PHP code. Did you forgot to add the "<?php" start tag at the beginning of the file? ' , $ file ));
237+ }
238+
239+ $ nsTokens = [\T_NS_SEPARATOR => true , \T_STRING => true ];
240+ if (\defined ('T_NAME_QUALIFIED ' )) {
241+ $ nsTokens [\T_NAME_QUALIFIED ] = true ;
242+ }
243+
244+ for ($ i = 0 ; isset ($ tokens [$ i ]); ++$ i ) {
245+ $ token = $ tokens [$ i ];
246+
247+ if (!isset ($ token [1 ])) {
248+ continue ;
249+ }
250+
251+ if (true === $ class && \T_STRING === $ token [0 ]) {
252+ return $ namespace . '\\' . $ token [1 ];
253+ }
254+
255+ if (true === $ namespace && isset ($ nsTokens [$ token [0 ]])) {
256+ $ namespace = $ token [1 ];
257+ while (isset ($ tokens [++$ i ][1 ], $ nsTokens [$ tokens [$ i ][0 ]])) {
258+ $ namespace .= $ tokens [$ i ][1 ];
259+ }
260+ $ token = $ tokens [$ i ];
261+ }
262+
263+ if (\T_CLASS === $ token [0 ]) {
264+ // Skip usage of ::class constant and anonymous classes
265+ $ skipClassToken = false ;
266+ for ($ j = $ i - 1 ; $ j > 0 ; --$ j ) {
267+ if (!isset ($ tokens [$ j ][1 ])) {
268+ break ;
269+ }
270+
271+ if (\T_DOUBLE_COLON === $ tokens [$ j ][0 ] || \T_NEW === $ tokens [$ j ][0 ]) {
272+ $ skipClassToken = true ;
273+ break ;
274+ } elseif (!\in_array ($ tokens [$ j ][0 ], [\T_WHITESPACE , \T_DOC_COMMENT , \T_COMMENT ])) {
275+ break ;
276+ }
277+ }
278+
279+ if (!$ skipClassToken ) {
280+ $ class = true ;
281+ }
282+ }
283+
284+ if (\T_NAMESPACE === $ token [0 ]) {
285+ $ namespace = true ;
286+ }
287+ }
288+
289+ return false ;
290+ }
153291}
0 commit comments