Passed
Push — feat/attribute-doc-block-resol... ( 9fc40a )
by Chema
05:16
created

DocBlockResolver::searchClassOverAttributes()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 6
c 0
b 0
f 0
nc 3
nop 2
dl 0
loc 13
rs 10
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
     */
29
    private function __construct(string $callerClass)
30
    {
31
        /** @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
35
    public static function fromCaller(object $caller): self
36
    {
37
        return new self($caller::class);
38
    }
39
40
    public function getDocBlockResolvable(string $method): DocBlockResolvable
41
    {
42
        $className = $this->getClassName($method);
43
        $resolvableType = $this->normalizeResolvableType($className);
44
45
        return new DocBlockResolvable($className, $resolvableType);
46
    }
47
48
    /**
49
     * @return class-string
50
     */
51
    private function getClassName(string $method): string
52
    {
53
        $cacheKey = $this->generateCacheKey($method);
54
        $cache = DocBlockResolverCache::getCacheInstance();
55
56
        if (!$cache->has($cacheKey)) {
57
            $className = $this->getClassFromDoc($method);
58
            $cache->put($cacheKey, $className);
59
        }
60
61
        return $cache->get($cacheKey);
62
    }
63
64
    private function generateCacheKey(string $method): string
65
    {
66
        return $this->callerClass . '::' . $method;
67
    }
68
69
    /**
70
     * @return class-string
71
     */
72
    private function getClassFromDoc(string $method): string
73
    {
74
        $reflectionClass = new ReflectionClass($this->callerClass);
75
76
        $className = $this->searchClassOverAttributes($reflectionClass, $method);
77
        if ($className !== null) {
78
            return $className;
79
        }
80
81
        $className = $this->searchClassOverDocBlock($reflectionClass, $method);
82
        if (class_exists($className)) {
83
            return $className;
84
        }
85
86
        $className = $this->searchClassOverUseStatements($reflectionClass, $className);
87
        if (class_exists($className)) {
88
            return $className;
89
        }
90
91
        if ($method === 'getFactory') {
92
            return AbstractFactory::class;
93
        }
94
95
        throw MissingClassDefinitionException::missingDefinition($this->callerClass, $method, $className);
96
    }
97
98
    /**
99
     * @param ReflectionClass<object> $reflectionClass
100
     */
101
    private function searchClassOverDocBlock(ReflectionClass $reflectionClass, string $method): string
102
    {
103
        $docBlock = (string)$reflectionClass->getDocComment();
104
105
        return (new DocBlockParser())->getClassFromMethod($docBlock, $method);
106
    }
107
108
    /**
109
     * @param ReflectionClass<object> $reflectionClass
110
     *
111
     * @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
     */
113
    private function searchClassOverAttributes(ReflectionClass $reflectionClass, string $method): ?string
114
    {
115
        $attributes = $reflectionClass->getAttributes(Doc::class, ReflectionAttribute::IS_INSTANCEOF);
116
117
        foreach ($attributes as $attribute) {
118
            /** @var Doc $instance */
119
            $instance = $attribute->newInstance();
120
            if ($instance->method === $method) {
121
                return $instance->className;
122
            }
123
        }
124
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