Passed
Push — master ( 4e29d7...d889eb )
by Chema
03:33
created

isProjectCacheEnabled()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
ccs 3
cts 3
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Gacela\Framework;
6
7
use Gacela\Framework\ClassResolver\Cache\GacelaCache;
8
use Gacela\Framework\ClassResolver\ClassNameCacheInterface;
9
use Gacela\Framework\ClassResolver\DocBlockService\CustomServicesCache;
10
use Gacela\Framework\ClassResolver\DocBlockService\DocBlockParser;
11
use Gacela\Framework\ClassResolver\DocBlockService\DocBlockServiceResolver;
12
use Gacela\Framework\ClassResolver\DocBlockService\MissingClassDefinitionException;
13
use Gacela\Framework\ClassResolver\DocBlockService\UseBlockParser;
14
use Gacela\Framework\ClassResolver\InMemoryCache;
15
use Gacela\Framework\Config\Config;
16
use ReflectionClass;
17
18
use function is_string;
19
20
trait DocBlockResolverAwareTrait
21
{
22
    /** @var array<string,string> */
23
    protected static array $fileContentCache = [];
24
25
    /** @var array<string,?mixed> */
26
    private array $customServices = [];
27
28
    /**
29
     * @param string $method
30
     * @param array $parameters
31
     *
32
     * @return mixed
33
     */
34 11
    public function __call($method, $parameters = [])
35
    {
36 11
        if (isset($this->customServices[$method])) {
37
            return $this->customServices[$method];
38
        }
39
40 11
        $cacheKey = $this->generateCacheKey($method);
41 11
        $cache = $this->createCustomServicesCache();
42
43
44 11
        if (!$cache->has($cacheKey)) {
45 9
            $className = $this->getClassFromDoc($method);
46 7
            $cache->put($cacheKey, $className);
47
        }
48
49
        /** @psalm-suppress ArgumentTypeCoercion */
50
        /** @var class-string $className */
51 9
        $className = $cache->get($cacheKey);
52 9
        $resolvableType = $this->normalizeResolvableType($className);
53
54 9
        $resolved = (new DocBlockServiceResolver($resolvableType))
55 9
            ->resolve($className);
56
57 9
        if ($resolved !== null) {
58 9
            return $resolved;
59
        }
60
61
        if ($this->hasParentClass()) {
62
            /** @psalm-suppress ParentNotFound, MixedAssignment, UndefinedMethod */
63
            $parentReturn = parent::__call($method, $parameters); // @phpstan-ignore-line
64
            $this->customServices[$method] = $parentReturn;
65
66
            return $parentReturn;
67
        }
68
69
        return null;
70
    }
71
72
    private function hasParentClass(): bool
73
    {
74
        /** @psalm-suppress ParentNotFound,MixedArgument */
75
        return class_parents($this)
76
            && method_exists(parent::class, '__call');
77
    }
78
79
    /**
80
     * @return class-string
1 ignored issue
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
81
     */
82 9
    private function getClassFromDoc(string $method): string
83
    {
84 9
        $reflectionClass = new ReflectionClass(static::class);
85 9
        $className = $this->searchClassOverDocBlock($reflectionClass, $method);
86 9
        if (class_exists($className)) {
87
            return $className;
88
        }
89 9
        $className = $this->searchClassOverUseStatements($reflectionClass, $className);
90 9
        if (class_exists($className)) {
91 7
            return $className;
92
        }
93 2
        throw MissingClassDefinitionException::missingDefinition(static::class, $method, $className);
94
    }
95
96 9
    private function searchClassOverDocBlock(ReflectionClass $reflectionClass, string $method): string
97
    {
98 9
        $docBlock = (string)$reflectionClass->getDocComment();
99
100 9
        return (new DocBlockParser())->getClassFromMethod($docBlock, $method);
101
    }
102
103
    /**
104
     * Look the uses, to find the fully-qualified class name for the className.
105
     */
106 9
    private function searchClassOverUseStatements(ReflectionClass $reflectionClass, string $className): string
107
    {
108 9
        $fileName = (string) $reflectionClass->getFileName();
109 9
        if (!isset(static::$fileContentCache[$fileName])) {
110 7
            static::$fileContentCache[$fileName] = (string) file_get_contents($fileName);
111
        }
112 9
        $phpFile = static::$fileContentCache[$fileName];
113
114 9
        return (new UseBlockParser())->getUseStatement($className, $phpFile);
115
    }
116
117 9
    private function normalizeResolvableType(string $resolvableType): string
118
    {
119
        /** @var list<string> $resolvableTypeParts */
120 9
        $resolvableTypeParts = explode('\\', ltrim($resolvableType, '\\'));
121 9
        $normalizedResolvableType = end($resolvableTypeParts);
122
123 9
        return is_string($normalizedResolvableType)
124 9
            ? $normalizedResolvableType
125 9
            : $resolvableType;
126
    }
127
128 11
    private function generateCacheKey(string $method): string
129
    {
130 11
        return self::class . '::' . $method;
131
    }
132
133 11
    private function createCustomServicesCache(): ClassNameCacheInterface
134
    {
135 11
        if (!$this->isProjectCacheEnabled()) {
136 3
            return new InMemoryCache(CustomServicesCache::class);
137
        }
138
139 8
        return new CustomServicesCache(
140 8
            Config::getInstance()->getCacheDir()
141
        );
142
    }
143
144 11
    private function isProjectCacheEnabled(): bool
145
    {
146 11
        return (bool)Config::getInstance()
147 11
            ->get(GacelaCache::ENABLED, GacelaCache::DEFAULT_VALUE);
148
    }
149
}
150