DocBlockResolver::searchClassOverDocBlock()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 2
c 0
b 0
f 0
dl 0
loc 5
ccs 1
cts 1
cp 1
rs 10
cc 1
nc 1
nop 2
crap 1
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 ReflectionClass;
12
13
use function is_string;
14
15
final class DocBlockResolver
16
{
17
    private const SPECIAL_RESOLVABLE_TYPES = ['Facade', 'Factory', 'Config'];
18
19
    /** @var array<string,string> [fileName => fileContent] */
20
    private static array $fileContentCache = [];
21
22
    /** @var class-string */
23
    private readonly string $callerClass;
24
25
    /**
26
     * @param class-string $callerClass
27
     */
28 34
    private function __construct(string $callerClass)
29
    {
30
        /** @psalm-suppress PropertyTypeCoercion */
31 34
        $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...
32
    }
33
34 34
    public static function fromCaller(object $caller): self
35
    {
36 34
        return new self($caller::class);
37
    }
38
39 34
    public function getDocBlockResolvable(string $method): DocBlockResolvable
40
    {
41 34
        $className = $this->getClassName($method);
42 31
        $resolvableType = $this->normalizeResolvableType($className);
43
44 31
        return new DocBlockResolvable($className, $resolvableType);
45
    }
46
47
    /**
48
     * @return class-string
49
     */
50 34
    private function getClassName(string $method): string
51
    {
52 34
        $cacheKey = $this->generateCacheKey($method);
53 34
        $cache = DocBlockResolverCache::getCacheInstance();
54
55 34
        if (!$cache->has($cacheKey)) {
56 25
            $className = $this->getClassFromDoc($method);
57 22
            $cache->put($cacheKey, $className);
58
        }
59
60 31
        return $cache->get($cacheKey);
61
    }
62
63 34
    private function generateCacheKey(string $method): string
64
    {
65 34
        return $this->callerClass . '::' . $method;
66
    }
67
68
    /**
69
     * @return class-string
70
     */
71 25
    private function getClassFromDoc(string $method): string
72
    {
73 25
        $reflectionClass = new ReflectionClass($this->callerClass);
74 25
        $className = $this->searchClassOverDocBlock($reflectionClass, $method);
75 25
        if (class_exists($className)) {
76
            return $className;
77
        }
78
79 25
        $className = $this->searchClassOverUseStatements($reflectionClass, $className);
80 25
        if (class_exists($className)) {
81 22
            return $className;
82
        }
83
84 3
        if ($method === 'getFactory') {
85
            return AbstractFactory::class;
86
        }
87 25
88
        throw MissingClassDefinitionException::missingDefinition($this->callerClass, $method, $className);
89 25
    }
90
91 25
    /**
92
     * @param ReflectionClass<object> $reflectionClass
93
     */
94
    private function searchClassOverDocBlock(ReflectionClass $reflectionClass, string $method): string
95
    {
96
        $docBlock = (string)$reflectionClass->getDocComment();
97 25
98
        return (new DocBlockParser())->getClassFromMethod($docBlock, $method);
99 25
    }
100 25
101 15
    /**
102
     * Look the uses, to find the fully-qualified class name for the className.
103 25
     *
104
     * @param ReflectionClass<object> $reflectionClass
105 25
     */
106
    private function searchClassOverUseStatements(ReflectionClass $reflectionClass, string $className): string
107
    {
108 31
        $fileName = (string)$reflectionClass->getFileName();
109
        if (!isset(self::$fileContentCache[$fileName])) {
110
            self::$fileContentCache[$fileName] = (string)file_get_contents($fileName);
111 31
        }
112 31
113
        $phpFile = self::$fileContentCache[$fileName];
114 31
115 31
        return (new UseBlockParser())->getUseStatement($className, $phpFile);
116
    }
117
118 31
    private function normalizeResolvableType(string $resolvableType): string
119 31
    {
120 22
        /** @var list<string> $resolvableTypeParts */
121
        $resolvableTypeParts = explode('\\', $resolvableType);
122
        $normalizedResolvableType = end($resolvableTypeParts);
123
124 9
        $result = is_string($normalizedResolvableType)
125
            ? $normalizedResolvableType
126
            : $resolvableType;
127
128
        foreach (self::SPECIAL_RESOLVABLE_TYPES as $specialName) {
129
            if (str_contains($result, $specialName)) {
130
                return $specialName;
131
            }
132
        }
133
134
        return $result;
135
    }
136
}
137