Passed
Push — feature/change-cache-to-profil... ( 1b58b5...c0f98d )
by Chema
03:41
created

DocBlockResolver   A

Complexity

Total Complexity 21

Size/Duplication

Total Lines 141
Duplicated Lines 0 %

Test Coverage

Coverage 93.22%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 21
eloc 50
c 1
b 0
f 0
dl 0
loc 141
rs 10
ccs 55
cts 59
cp 0.9322

13 Methods

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