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