Passed
Push — feature/remove-profiler ( 1b185f )
by Chema
04:28
created

DocBlockResolver::createClassNameCache()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

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