Passed
Push — master ( 2b04c5...3174fa )
by Jesús
01:25 queued 12s
created

AbstractClassResolver::getGlobalInstance()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 1
dl 0
loc 7
ccs 5
cts 5
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\ClassResolver;
6
7
use Gacela\Framework\ClassResolver\ClassNameFinder\ClassNameFinderInterface;
8
use Gacela\Framework\ClassResolver\DependencyResolver\DependencyResolver;
9
use Gacela\Framework\Config\ConfigFactory;
10
use RuntimeException;
11
use function get_class;
12
use function in_array;
13
use function is_string;
14
use function sprintf;
15
16
abstract class AbstractClassResolver
17
{
18
    private const ALLOWED_TYPES_FOR_ANONYMOUS_GLOBAL = ['Config', 'Factory', 'DependencyProvider'];
19
20
    /** @var array<string,null|object> */
21
    private static array $cachedInstances = [];
22
23
    /** @var array<string,object> */
24
    private static array $cachedGlobalInstances = [];
25
26
    private static ?ClassNameFinderInterface $classNameFinder = null;
27
28
    private ?ConfigFactory $configFactory = null;
29
30
    private ?DependencyResolver $dependencyResolver = null;
31
32
    abstract public function resolve(object $callerClass): ?object;
33
34
    abstract protected function getResolvableType(): string;
35
36
    /**
37
     * Add an anonymous class as 'Config', 'Factory' or 'DependencyProvider' as a global resource
38
     * bound to the context that it's pass as first argument. It can be the string-key
39
     * (from a non-class/file context) or the class/object itself.
40
     *
41
     * @param object|string $context
42
     */
43 5
    public static function addAnonymousGlobal($context, object $resolvedClass): void
44
    {
45 5
        $contextName = self::extractContextNameFromContext($context);
46 5
        $parentClass = get_parent_class($resolvedClass);
47
48 5
        $type = is_string($parentClass)
0 ignored issues
show
introduced by
The condition is_string($parentClass) is always true.
Loading history...
49 4
            ? ResolvableType::fromClassName($parentClass)->resolvableType()
50 5
            : $contextName;
51
52 5
        self::validateTypeForAnonymousGlobalRegistration($type);
53
54 4
        $key = sprintf('\%s\%s\%s', ClassInfo::MODULE_NAME_ANONYMOUS, $contextName, $type);
55 4
        self::addCachedGlobalInstance($key, $resolvedClass);
56 4
    }
57
58
    /**
59
     * @param object|string $context
60
     */
61 5
    private static function extractContextNameFromContext($context): string
62
    {
63 5
        if (is_string($context)) {
64 1
            return $context;
65
        }
66
67 5
        $callerClass = get_class($context);
68
        /** @var list<string> $callerClassParts */
69 5
        $callerClassParts = explode('\\', ltrim($callerClass, '\\'));
70
71 5
        return end($callerClassParts);
72
    }
73
74 4
    public static function overrideExistingResolvedClass(string $className, object $resolvedClass): void
75
    {
76 4
        $key = self::getGlobalKeyFromClassName($className);
77
78 4
        self::addCachedGlobalInstance($key, $resolvedClass);
79 4
    }
80
81
    /**
82
     * @internal so the Locator can access to the global instances before creating a new instance
83
     */
84 8
    public static function getCachedGlobalInstance(string $className): ?object
85
    {
86 8
        $key = self::getGlobalKeyFromClassName($className);
87
88 8
        return self::$cachedGlobalInstances[$key]
89 4
            ?? self::$cachedGlobalInstances['\\' . $key]
90 8
            ?? null;
91
    }
92
93 8
    private static function getGlobalKeyFromClassName(string $className): string
94
    {
95 8
        return GlobalKey::fromClassName($className);
96
    }
97
98 5
    private static function validateTypeForAnonymousGlobalRegistration(string $type): void
99
    {
100 5
        if (!in_array($type, self::ALLOWED_TYPES_FOR_ANONYMOUS_GLOBAL)) {
101 1
            throw new RuntimeException(
102 1
                "Type '$type' not allowed. Valid types: " . implode(', ', self::ALLOWED_TYPES_FOR_ANONYMOUS_GLOBAL)
103
            );
104
        }
105 4
    }
106
107 8
    private static function addCachedGlobalInstance(string $key, object $resolvedClass): void
108
    {
109 8
        self::$cachedGlobalInstances[$key] = $resolvedClass;
110 8
    }
111
112 16
    public function doResolve(object $callerClass): ?object
113
    {
114 16
        $classInfo = new ClassInfo($callerClass);
115 16
        $cacheKey = $this->getCacheKey($classInfo);
116 16
        if (isset(self::$cachedInstances[$cacheKey])) {
117
            return self::$cachedInstances[$cacheKey];
118
        }
119
120 16
        $resolvedClass = $this->resolveGlobal($cacheKey);
121 16
        if (null !== $resolvedClass) {
122 1
            return $resolvedClass;
123
        }
124
125 15
        $resolvedClassName = $this->findClassName($classInfo);
126 15
        if (null === $resolvedClassName) {
127 3
            return null;
128
        }
129
130 14
        self::$cachedInstances[$cacheKey] = $this->createInstance($resolvedClassName);
131
132 14
        return self::$cachedInstances[$cacheKey];
133
    }
134
135 16
    private function resolveGlobal(string $cacheKey): ?object
136
    {
137 16
        $resolvedClass = self::$cachedGlobalInstances[$cacheKey] ?? null;
138
139 16
        if (null === $resolvedClass) {
140 15
            return null;
141
        }
142
143 1
        self::$cachedInstances[$cacheKey] = $resolvedClass;
144
145 1
        return self::$cachedInstances[$cacheKey];
146
    }
147
148 16
    private function getCacheKey(ClassInfo $classInfo): string
149
    {
150 16
        return $classInfo->getCacheKey($this->getResolvableType());
151
    }
152
153 15
    private function findClassName(ClassInfo $classInfo): ?string
154
    {
155 15
        return $this->getClassNameFinder()->findClassName(
156 15
            $classInfo,
157 15
            $this->getResolvableType()
158
        );
159
    }
160
161 15
    private function getClassNameFinder(): ClassNameFinderInterface
162
    {
163 15
        if (null === self::$classNameFinder) {
164 1
            self::$classNameFinder = (new ClassResolverFactory())->createClassNameFinder();
165
        }
166
167 15
        return self::$classNameFinder;
168
    }
169
170 14
    private function createInstance(string $resolvedClassName): ?object
171
    {
172 14
        if (class_exists($resolvedClassName)) {
173 14
            $dependencies = $this->getDependencyResolver()
174 14
                ->resolveDependencies($resolvedClassName);
175
176
            /** @psalm-suppress MixedMethodCall */
177 14
            return new $resolvedClassName(...$dependencies);
178
        }
179
180
        return null;
181
    }
182
183 14
    private function getDependencyResolver(): DependencyResolver
184
    {
185 14
        if (null === $this->dependencyResolver) {
186 14
            $gacelaFileConfig = $this->getConfigFactory()
187 14
                ->createGacelaConfigFileFactory()
188 14
                ->createGacelaFileConfig();
189
190 14
            $this->dependencyResolver = new DependencyResolver($gacelaFileConfig);
191
        }
192
193 14
        return $this->dependencyResolver;
194
    }
195
196 14
    private function getConfigFactory(): ConfigFactory
197
    {
198 14
        if (null === $this->configFactory) {
199 14
            $this->configFactory = new ConfigFactory();
200
        }
201
202 14
        return $this->configFactory;
203
    }
204
}
205