Passed
Push — master ( 8d4d73...ee2cd0 )
by Alexander
13:53
created

DependencyResolver::getFromFactory()   A

Complexity

Conditions 2
Paths 3

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 2
eloc 10
c 1
b 0
f 1
nc 3
nop 1
dl 0
loc 15
ccs 8
cts 8
cp 1
crap 2
rs 9.9332
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Factory;
6
7
use Psr\Container\ContainerExceptionInterface;
8
use Psr\Container\ContainerInterface;
9
use Psr\Container\NotFoundExceptionInterface;
10
use Yiisoft\Definitions\ArrayDefinition;
11
use Yiisoft\Definitions\Contract\DefinitionInterface;
12
use Yiisoft\Definitions\Contract\DependencyResolverInterface;
13
use Yiisoft\Definitions\Contract\ReferenceInterface;
14
use Yiisoft\Definitions\Exception\CircularReferenceException;
15
use Yiisoft\Definitions\Exception\InvalidConfigException;
16
use Yiisoft\Definitions\Exception\NotFoundException;
17
use Yiisoft\Definitions\Exception\NotInstantiableClassException;
18
use Yiisoft\Definitions\Exception\NotInstantiableException;
19
use Yiisoft\Definitions\Infrastructure\Normalizer;
20
use Yiisoft\Injector\Injector;
21
22
use function is_object;
23
use function is_string;
24
25
/**
26
 * @internal
27
 */
28
final class DependencyResolver implements DependencyResolverInterface
29
{
30
    private ?ContainerInterface $container;
31
    private ?Injector $injector = null;
32
33
    /**
34
     * @var mixed[] Definitions
35
     * @psalm-var array<string, mixed>
36
     */
37
    private array $definitions = [];
38
39
    /**
40
     * @var DefinitionInterface[] object definitions indexed by their types
41
     * @psalm-var array<string, DefinitionInterface>
42
     */
43
    private array $definitionInstances = [];
44
45
    /**
46
     * @var array used to collect IDs instantiated during build to detect circular references
47
     *
48
     * @psalm-var array<string,1>
49
     */
50
    private array $creatingIds = [];
51
52 84
    public function __construct(?ContainerInterface $container)
53
    {
54 84
        $this->container = $container;
55 84
    }
56
57
    /**
58
     * @param string $id
59
     *
60
     * @throws NotFoundExceptionInterface
61
     * @throws ContainerExceptionInterface
62
     *
63
     * @return mixed|object
64
     *
65
     * @psalm-suppress InvalidThrow
66
     */
67 18
    public function get($id)
68
    {
69 18
        if (isset($this->definitions[$id])) {
70 10
            return $this->getFromFactory($id);
71
        }
72
73 9
        if ($this->container !== null && $this->container->has($id)) {
74 2
            return $this->container->get($id);
75
        }
76
77 7
        if (class_exists($id)) {
78 5
            return $this->getFromFactory($id);
79
        }
80
81 2
        throw new NotInstantiableClassException($id);
82
    }
83
84
    /**
85
     * @param string $id
86
     */
87
    public function has($id): bool
88
    {
89
        if ($this->container !== null) {
90
            return $this->container->has($id);
91
        }
92
        return $this->canBeCreatedByFactory($id);
93
    }
94
95 23
    public function resolveReference(string $id)
96
    {
97 23
        return $this->getFromFactory($id);
98
    }
99
100 9
    public function invoke(callable $callable)
101
    {
102 9
        return $this->getInjector()->invoke($callable);
103
    }
104
105
    /**
106
     * @param mixed $definition
107
     */
108 49
    public function setFactoryDefinition(string $id, $definition): void
109
    {
110 49
        $this->definitions[$id] = $definition;
111 49
    }
112
113
    /**
114
     * @param mixed $config
115
     *
116
     * @throws CircularReferenceException
117
     * @throws NotFoundException
118
     * @throws NotInstantiableException
119
     * @throws InvalidConfigException
120
     *
121
     * @return mixed
122
     */
123 77
    public function create($config)
124
    {
125 77
        if (is_string($config)) {
126 60
            if ($this->canBeCreatedByFactory($config)) {
127 57
                return $this->getFromFactory($config);
128
            }
129 3
            throw new NotFoundException($config);
130
        }
131
132 19
        $definition = $this->createDefinition($config);
133
134 18
        if ($definition instanceof ArrayDefinition) {
135 16
            $this->creatingIds[$definition->getClass()] = 1;
136
        }
137
        try {
138 18
            return $definition->resolve($this);
139
        } finally {
140 18
            if ($definition instanceof ArrayDefinition) {
141 18
                unset($this->creatingIds[$definition->getClass()]);
142
            }
143
        }
144
    }
145
146
    /**
147
     * @param mixed $config
148
     *
149
     * @throws InvalidConfigException
150
     */
151 19
    private function createDefinition($config): DefinitionInterface
152
    {
153 19
        $definition = Normalizer::normalize($config);
154
155
        if (
156 18
            ($definition instanceof ArrayDefinition) &&
157 18
            isset($this->definitions[$definition->getClass()])
158
        ) {
159 4
            $definition = $this->mergeDefinitions(
160 4
                $this->getDefinition($definition->getClass()),
161
                $definition
162
            );
163
        }
164
165 18
        return $definition;
166
    }
167
168
    /**
169
     * @param string $id
170
     *
171
     * @throws CircularReferenceException
172
     * @throws InvalidConfigException
173
     * @throws NotFoundException
174
     * @throws NotInstantiableException
175
     *
176
     * @return mixed|object
177
     */
178 58
    private function getFromFactory(string $id)
179
    {
180 58
        if (isset($this->creatingIds[$id])) {
181 5
            throw new CircularReferenceException(sprintf(
182 5
                'Circular reference to "%s" detected while creating: %s.',
183
                $id,
184 5
                implode(',', array_keys($this->creatingIds))
185
            ));
186
        }
187
188 58
        $this->creatingIds[$id] = 1;
189
        try {
190 58
            return $this->getDefinition($id)->resolve($this);
191
        } finally {
192 58
            unset($this->creatingIds[$id]);
193
        }
194
    }
195
196
    /**
197
     * @throws InvalidConfigException
198
     */
199 62
    private function getDefinition(string $id): DefinitionInterface
200
    {
201 62
        if (!isset($this->definitionInstances[$id])) {
202 62
            if (isset($this->definitions[$id])) {
203 49
                if (is_object($this->definitions[$id]) && !($this->definitions[$id] instanceof ReferenceInterface)) {
204 7
                    return Normalizer::normalize(clone $this->definitions[$id], $id);
205
                }
206 45
                $this->definitionInstances[$id] = Normalizer::normalize($this->definitions[$id], $id);
207
            } else {
208
                /** @psalm-var class-string $id */
209 34
                $this->definitionInstances[$id] = ArrayDefinition::fromPreparedData($id);
210
            }
211
        }
212
213 57
        return $this->definitionInstances[$id];
214
    }
215
216 9
    private function getInjector(): Injector
217
    {
218 9
        return $this->injector ??= new Injector($this);
219
    }
220
221 60
    private function canBeCreatedByFactory(string $id): bool
222
    {
223 60
        return isset($this->definitions[$id]) || class_exists($id);
224
    }
225
226 4
    private function mergeDefinitions(DefinitionInterface $one, ArrayDefinition $two): DefinitionInterface
227
    {
228 4
        return $one instanceof ArrayDefinition ? $one->merge($two) : $two;
229
    }
230
}
231