Passed
Push — feature/change-cache-to-profil... ( 160b60 )
by Chema
04:07
created

DocBlockResolver   A

Complexity

Total Complexity 20

Size/Duplication

Total Lines 137
Duplicated Lines 0 %

Test Coverage

Coverage 92.98%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 20
eloc 49
c 1
b 0
f 0
dl 0
loc 137
rs 10
ccs 53
cts 57
cp 0.9298

12 Methods

Rating   Name   Duplication   Size   Complexity  
A getDocBlockResolvable() 0 6 1
A getClassName() 0 15 2
A normalizeResolvableType() 0 9 2
A isProjectProfilerEnabled() 0 3 1
A createClassNameCache() 0 14 2
A searchClassOverUseStatements() 0 9 2
A hasParentCallMethod() 0 5 2
A generateCacheKey() 0 3 1
A getClassFromDoc() 0 12 3
A fromCaller() 0 5 2
A __construct() 0 4 1
A searchClassOverDocBlock() 0 5 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Gacela\Framework\DocBlockResolver;
6
7
use Gacela\Framework\ClassResolver\ClassNameCacheInterface;
8
use Gacela\Framework\ClassResolver\DocBlockService\CustomServicesProfiler;
9
use Gacela\Framework\ClassResolver\DocBlockService\DocBlockParser;
10
use Gacela\Framework\ClassResolver\DocBlockService\MissingClassDefinitionException;
11
use Gacela\Framework\ClassResolver\DocBlockService\UseBlockParser;
12
use Gacela\Framework\ClassResolver\InMemoryClassNameCache;
13
use Gacela\Framework\ClassResolver\ProfiledInMemoryCache;
14
use Gacela\Framework\ClassResolver\Profiler\GacelaProfiler;
15
use Gacela\Framework\Config\Config;
16
use ReflectionClass;
17
18
use function get_class;
19
use function is_string;
20
21
final class DocBlockResolver
22
{
23
    /** @var array<string,string> [fileName => fileContent] */
24
    private static array $fileContentCache = [];
25
26
    /** @var class-string */
27
    private string $callerClass;
28
29
    /** @var class-string|string */
30
    private string $callerParentClass;
31
32
    /**
33
     * @param class-string $callerClass
34
     * @param class-string|string $callerParentClass
35
     */
36 13
    private function __construct(string $callerClass, string $callerParentClass)
37
    {
38 13
        $this->callerClass = $callerClass;
39 13
        $this->callerParentClass = $callerParentClass;
40
    }
41
42 13
    public static function fromCaller(object $caller): self
43
    {
44 13
        return new self(
45 13
            get_class($caller),
46 13
            get_parent_class($caller) ?: ''
47
        );
48
    }
49
50
    public function hasParentCallMethod(): bool
51
    {
52
        /** @psalm-suppress ArgumentTypeCoercion */
53
        return $this->callerParentClass !== ''
54
            && method_exists($this->callerParentClass, '__call');
55
    }
56
57 13
    public function getDocBlockResolvable(string $method): DocBlockResolvable
58
    {
59 13
        $className = $this->getClassName($method);
60 11
        $resolvableType = $this->normalizeResolvableType($className);
61
62 11
        return new DocBlockResolvable($className, $resolvableType);
63
    }
64
65
    /**
66
     * @return class-string
67
     */
68 13
    private function getClassName(string $method): string
69
    {
70 13
        $cacheKey = $this->generateCacheKey($method);
71 13
        $cache = $this->createClassNameCache();
72
73 13
        if (!$cache->has($cacheKey)) {
74 11
            $className = $this->getClassFromDoc($method);
75 9
            $cache->put($cacheKey, $className);
76
        }
77
78
        /** @psalm-suppress ArgumentTypeCoercion */
79
        /** @var class-string $className */
80 11
        $className = $cache->get($cacheKey);
81
82 11
        return $className;
83
    }
84
85 11
    private function normalizeResolvableType(string $resolvableType): string
86
    {
87
        /** @var list<string> $resolvableTypeParts */
88 11
        $resolvableTypeParts = explode('\\', ltrim($resolvableType, '\\'));
89 11
        $normalizedResolvableType = end($resolvableTypeParts);
90
91 11
        return is_string($normalizedResolvableType)
92 11
            ? $normalizedResolvableType
93 11
            : $resolvableType;
94
    }
95
96 13
    private function generateCacheKey(string $method): string
97
    {
98 13
        return $this->callerClass . '::' . $method;
99
    }
100
101 13
    private function createClassNameCache(): ClassNameCacheInterface
102
    {
103 13
        $inMemoryCache = new InMemoryClassNameCache(CustomServicesProfiler::class);
104
105 13
        if ($this->isProjectProfilerEnabled()) {
106 4
            return new ProfiledInMemoryCache(
107
                $inMemoryCache,
108 4
                new CustomServicesProfiler(
109 4
                    Config::getInstance()->getProfilerDir(),
110
                )
111
            );
112
        }
113
114 9
        return $inMemoryCache;
115
    }
116
117 13
    private function isProjectProfilerEnabled(): bool
118
    {
119 13
        return (new GacelaProfiler(Config::getInstance()))->isEnabled();
120
    }
121
122
    /**
123
     * @return class-string
124
     */
125 11
    private function getClassFromDoc(string $method): string
126
    {
127 11
        $reflectionClass = new ReflectionClass($this->callerClass);
128 11
        $className = $this->searchClassOverDocBlock($reflectionClass, $method);
129 11
        if (class_exists($className)) {
130
            return $className;
131
        }
132 11
        $className = $this->searchClassOverUseStatements($reflectionClass, $className);
133 11
        if (class_exists($className)) {
134 9
            return $className;
135
        }
136 2
        throw MissingClassDefinitionException::missingDefinition($this->callerClass, $method, $className);
137
    }
138
139 11
    private function searchClassOverDocBlock(ReflectionClass $reflectionClass, string $method): string
140
    {
141 11
        $docBlock = (string)$reflectionClass->getDocComment();
142
143 11
        return (new DocBlockParser())->getClassFromMethod($docBlock, $method);
144
    }
145
146
    /**
147
     * Look the uses, to find the fully-qualified class name for the className.
148
     */
149 11
    private function searchClassOverUseStatements(ReflectionClass $reflectionClass, string $className): string
150
    {
151 11
        $fileName = (string)$reflectionClass->getFileName();
152 11
        if (!isset(self::$fileContentCache[$fileName])) {
153 9
            self::$fileContentCache[$fileName] = (string)file_get_contents($fileName);
154
        }
155 11
        $phpFile = self::$fileContentCache[$fileName];
156
157 11
        return (new UseBlockParser())->getUseStatement($className, $phpFile);
158
    }
159
}
160