Passed
Push — feature/add-config-files ( 1c4158...837ed6 )
by Jesús
05:48 queued 02:26
created

  A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
ccs 2
cts 2
cp 1
crap 1
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 9
        $className = $cache->get($cacheKey);
51 9
        $resolvableType = $this->normalizeResolvableType($className);
52
53 9
        $resolved = (new DocBlockServiceResolver($resolvableType))
54 9
            ->resolve($className);
55
56 9
        if ($resolved !== null) {
57 9
            return $resolved;
58
        }
59
60
        if ($this->hasParentClass()) {
61
            /** @psalm-suppress ParentNotFound */
62
            $parentReturn = parent::__call($method, $parameters);
63
            $this->customServices[$method] = $parentReturn;
64
65
            return $parentReturn;
66
        }
67
68
        return null;
69
    }
70
71
    private function hasParentClass(): bool
72
    {
73
        /** @psalm-suppress ParentNotFound,MixedArgument */
74
        return class_parents($this)
75
            && method_exists(parent::class, '__call');
76
    }
77
78
    /**
79
     * @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...
80
     */
81 9
    private function getClassFromDoc(string $method): string
82
    {
83 9
        $reflectionClass = new ReflectionClass(static::class);
84 9
        $className = $this->searchClassOverDocBlock($reflectionClass, $method);
85 9
        if (class_exists($className)) {
86
            return $className;
87
        }
88 9
        $className = $this->searchClassOverUseStatements($reflectionClass, $className);
89 9
        if (class_exists($className)) {
90 7
            return $className;
91
        }
92 2
        throw MissingClassDefinitionException::missingDefinition(static::class, $method, $className);
93
    }
94
95 9
    private function searchClassOverDocBlock(ReflectionClass $reflectionClass, string $method): string
96
    {
97 9
        $docBlock = (string)$reflectionClass->getDocComment();
98
99 9
        return (new DocBlockParser())->getClassFromMethod($docBlock, $method);
100
    }
101
102
    /**
103
     * Look the uses, to find the fully-qualified class name for the className.
104
     */
105 9
    private function searchClassOverUseStatements(ReflectionClass $reflectionClass, string $className): string
106
    {
107 9
        $fileName = $reflectionClass->getFileName();
108 9
        if (!isset(static::$fileContentCache[$fileName])) {
109 7
            static::$fileContentCache[$fileName] = file_get_contents($fileName);
110
        }
111 9
        $phpFile = static::$fileContentCache[$fileName];
112
113 9
        return (new UseBlockParser())->getUseStatement($className, $phpFile);
114
    }
115
116 9
    private function normalizeResolvableType(string $resolvableType): string
117
    {
118
        /** @var list<string> $resolvableTypeParts */
119 9
        $resolvableTypeParts = explode('\\', ltrim($resolvableType, '\\'));
120 9
        $normalizedResolvableType = end($resolvableTypeParts);
121
122 9
        return is_string($normalizedResolvableType)
123 9
            ? $normalizedResolvableType
124 9
            : $resolvableType;
125
    }
126
127 11
    private function generateCacheKey(string $method): string
128
    {
129 11
        return self::class . '::' . $method;
130
    }
131
132 11
    private function createCustomServicesCache(): ClassNameCacheInterface
133
    {
134 11
        if (!$this->isCacheEnabled()) {
135 3
            return new InMemoryCache(CustomServicesCache::class);
136
        }
137
138 8
        return new CustomServicesCache(
139 8
            Config::getInstance()->getCacheDir()
140
        );
141
    }
142
143 11
    private function isCacheEnabled(): bool
144
    {
145 11
        return (bool)Config::getInstance()
146 11
            ->get(GacelaCache::ENABLED, GacelaCache::DEFAULT_VALUE);
147
    }
148
}
149