Passed
Pull Request — main (#339)
by Chema
05:43 queued 01:20
created

DocBlockResolver::searchClassOverAttributes()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3.0261

Importance

Changes 0
Metric Value
cc 3
eloc 6
nc 3
nop 2
dl 0
loc 13
ccs 6
cts 7
cp 0.8571
crap 3.0261
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Gacela\Framework\DocBlockResolver;
6
7
use Gacela\Framework\AbstractFactory;
8
use Gacela\Framework\ClassResolver\DocBlockService\DocBlockParser;
9
use Gacela\Framework\ClassResolver\DocBlockService\MissingClassDefinitionException;
10
use Gacela\Framework\ClassResolver\DocBlockService\UseBlockParser;
11
use ReflectionAttribute;
12
use ReflectionClass;
13
14
use function is_string;
15
16
final class DocBlockResolver
17
{
18
    private const SPECIAL_RESOLVABLE_TYPES = ['Facade', 'Factory', 'Config'];
19
20
    /** @var array<string,string> [fileName => fileContent] */
21
    private static array $fileContentCache = [];
22
23
    /** @var class-string */
24
    private readonly string $callerClass;
25
26
    /**
27
     * @param class-string $callerClass
28 34
     */
29
    private function __construct(string $callerClass)
30
    {
31 34
        /** @psalm-suppress PropertyTypeCoercion */
32
        $this->callerClass = '\\' . ltrim($callerClass, '\\'); // @phpstan-ignore-line
0 ignored issues
show
Bug introduced by
The property callerClass is declared read-only in Gacela\Framework\DocBlockResolver\DocBlockResolver.
Loading history...
33
    }
34 34
35
    public static function fromCaller(object $caller): self
36 34
    {
37
        return new self($caller::class);
38
    }
39 34
40
    public function getDocBlockResolvable(string $method): DocBlockResolvable
41 34
    {
42 31
        $className = $this->getClassName($method);
43
        $resolvableType = $this->normalizeResolvableType($className);
44 31
45
        return new DocBlockResolvable($className, $resolvableType);
46
    }
47
48
    /**
49
     * @return class-string
50 34
     */
51
    private function getClassName(string $method): string
52 34
    {
53 34
        $cacheKey = $this->generateCacheKey($method);
54
        $cache = DocBlockResolverCache::getCacheInstance();
55 34
56 25
        if (!$cache->has($cacheKey)) {
57 22
            $className = $this->getClassFromDoc($method);
58
            $cache->put($cacheKey, $className);
59
        }
60 31
61
        return $cache->get($cacheKey);
62
    }
63 34
64
    private function generateCacheKey(string $method): string
65 34
    {
66
        return $this->callerClass . '::' . $method;
67
    }
68
69
    /**
70
     * @return class-string
71 25
     */
72
    private function getClassFromDoc(string $method): string
73 25
    {
74 25
        $reflectionClass = new ReflectionClass($this->callerClass);
75 25
76
        $className = $this->searchClassOverAttributes($reflectionClass, $method);
77
        if ($className !== null) {
78
            return $className;
79 25
        }
80 25
81 22
        $className = $this->searchClassOverDocBlock($reflectionClass, $method);
82
        if (class_exists($className)) {
83
            return $className;
84 3
        }
85
86
        $className = $this->searchClassOverUseStatements($reflectionClass, $className);
87 25
        if (class_exists($className)) {
88
            return $className;
89 25
        }
90
91 25
        if ($method === 'getFactory') {
92
            return AbstractFactory::class;
93
        }
94
95
        throw MissingClassDefinitionException::missingDefinition($this->callerClass, $method, $className);
96
    }
97 25
98
    /**
99 25
     * @param ReflectionClass<object> $reflectionClass
100 25
     */
101 15
    private function searchClassOverDocBlock(ReflectionClass $reflectionClass, string $method): string
102
    {
103 25
        $docBlock = (string)$reflectionClass->getDocComment();
104
105 25
        return (new DocBlockParser())->getClassFromMethod($docBlock, $method);
106
    }
107
108 31
    /**
109
     * @param ReflectionClass<object> $reflectionClass
110
     *
111 31
     * @return class-string|null
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string|null at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string|null.
Loading history...
112 31
     */
113
    private function searchClassOverAttributes(ReflectionClass $reflectionClass, string $method): ?string
114 31
    {
115 31
        $attributes = $reflectionClass->getAttributes(Doc::class, ReflectionAttribute::IS_INSTANCEOF);
116
117
        foreach ($attributes as $attribute) {
118 31
            /** @var Doc $instance */
119 31
            $instance = $attribute->newInstance();
120 22
            if ($instance->method === $method) {
121
                return $instance->className;
122
            }
123
        }
124 9
125
        return null;
126
    }
127
128
    /**
129
     * Look the uses, to find the fully-qualified class name for the className.
130
     *
131
     * @param ReflectionClass<object> $reflectionClass
132
     */
133
    private function searchClassOverUseStatements(ReflectionClass $reflectionClass, string $className): string
134
    {
135
        $fileName = (string)$reflectionClass->getFileName();
136
        if (!isset(self::$fileContentCache[$fileName])) {
137
            self::$fileContentCache[$fileName] = (string)file_get_contents($fileName);
138
        }
139
140
        $phpFile = self::$fileContentCache[$fileName];
141
142
        return (new UseBlockParser())->getUseStatement($className, $phpFile);
143
    }
144
145
    private function normalizeResolvableType(string $resolvableType): string
146
    {
147
        /** @var list<string> $resolvableTypeParts */
148
        $resolvableTypeParts = explode('\\', $resolvableType);
149
        $normalizedResolvableType = end($resolvableTypeParts);
150
151
        $result = is_string($normalizedResolvableType)
152
            ? $normalizedResolvableType
153
            : $resolvableType;
154
155
        foreach (self::SPECIAL_RESOLVABLE_TYPES as $specialName) {
156
            if (str_contains($result, $specialName)) {
157
                return $specialName;
158
            }
159
        }
160
161
        return $result;
162
    }
163
}
164