Passed
Pull Request — master (#208)
by Jesús
07:17 queued 03:55
created

DocBlockResolver::getDocBlockResolvable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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