Skip to content

Commit 883ccb2

Browse files
Added custom class loader support
1 parent 992f441 commit 883ccb2

File tree

3 files changed

+157
-11
lines changed

3 files changed

+157
-11
lines changed

phpstan.neon.dist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,5 @@ parameters:
1111
message: "#^Unable to resolve the template type T in call to method Spiral\\\\Attributes\\\\ReaderInterface\\:\\:(getParameterMetadata\\(\\)|getFunctionMetadata\\(\\)|getClassMetadata\\(\\)|getPropertyMetadata\\(\\)|getConstantMetadata\\(\\))$#"
1212
path: src/AnnotationLoader.php
1313
-
14-
message: "#^Parameter \\#1 \\$array of class ArrayIterator constructor expects array, array\\|null given.$#"
14+
message: "#^Argument of an invalid type array\\|null passed to yield from, only iterables are supported.$#"
1515
path: src/AnnotationLoader.php

src/AnnotationLoader.php

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,17 @@ class AnnotationLoader implements LoaderInterface
3333
/** @var string[] */
3434
private $resources = [];
3535

36+
/** @var null|callable(string[]) */
37+
private $classLoader;
38+
3639
/**
3740
* @param ReaderInterface $reader
41+
* @param callable $classLoader
3842
*/
39-
public function __construct(ReaderInterface $reader)
43+
public function __construct(ReaderInterface $reader, callable $classLoader = null)
4044
{
4145
$this->reader = $reader;
46+
$this->classLoader = $classLoader;
4247
}
4348

4449
/**
@@ -66,7 +71,7 @@ public function attach(string ...$resources): void
6671
*/
6772
public function build(): void
6873
{
69-
$this->annotations = $annotations = $files = [];
74+
$this->annotations = $annotations = $classes = $files = [];
7075

7176
foreach ($this->resources as $resource) {
7277
if (\is_dir($resource)) {
@@ -79,11 +84,10 @@ public function build(): void
7984
continue;
8085
}
8186

82-
$annotations[] = $resource;
87+
$classes[] = $resource;
8388
}
8489

85-
$classes = \array_merge($annotations, $this->findClasses($files));
86-
$annotations = [];
90+
$classes += $this->findClasses($files);
8791

8892
foreach ($classes as $class) {
8993
$annotations += $this->findAnnotations($class);
@@ -109,7 +113,7 @@ public function load(): iterable
109113
$this->build();
110114
}
111115

112-
yield from new \ArrayIterator($this->annotations);
116+
return yield from $this->annotations;
113117
}
114118

115119
/**
@@ -263,6 +267,10 @@ private function fetchAnnotations(string $className, array $reflections, array $
263267
*/
264268
private function findClasses(array $files): array
265269
{
270+
if (null !== $this->classLoader) {
271+
return ($this->classLoader)($files);
272+
}
273+
266274
$declared = \get_declared_classes();
267275

268276
foreach ($files as $file) {

tests/AnnotationLoaderTest.php

Lines changed: 142 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@
1919

2020
use Biurad\Annotations\AnnotationLoader;
2121
use 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;
2228
use PHPUnit\Framework\TestCase;
2329
use Spiral\Attributes\AnnotationReader;
2430
use 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

Comments
 (0)