Passed
Pull Request — master (#151)
by Alexander
03:59 queued 01:56
created

FactoryContainer::build()   A

Complexity

Conditions 2
Paths 3

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 11
c 0
b 0
f 0
nc 3
nop 1
dl 0
loc 17
ccs 9
cts 9
cp 1
crap 2
rs 9.9
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Factory;
6
7
use Psr\Container\ContainerInterface;
8
use Yiisoft\Definitions\ArrayDefinition;
9
use Yiisoft\Definitions\Contract\DefinitionInterface;
10
use Yiisoft\Definitions\Contract\ReferenceInterface;
11
use Yiisoft\Definitions\Exception\CircularReferenceException;
12
use Yiisoft\Definitions\Exception\InvalidConfigException;
13
use Yiisoft\Definitions\Exception\NotInstantiableClassException;
14
use Yiisoft\Definitions\Exception\NotInstantiableException;
15
use Yiisoft\Definitions\Helpers\Normalizer;
16
17
use function array_key_exists;
18
use function is_object;
19
20
/**
21
 * Factory's primary container.
22
 *
23
 * @internal
24
 */
25
final class FactoryContainer implements ContainerInterface
26
{
27
    /**
28
     * @var ContainerInterface|null Container to use for resolving dependencies. When null, only definitions
29
     * are used.
30
     */
31
    private ?ContainerInterface $container;
32
33
    /**
34
     * @var array Definitions to create objects with.
35
     * @psalm-var array<string, mixed>
36
     */
37
    private array $definitions = [];
38
39
    /**
40
     * @var DefinitionInterface[] Object created from 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
    /**
53
     * @param ContainerInterface|null $container Container to use for resolving dependencies. When null, only definitions
54
     * are used.
55
     */
56 107
    public function __construct(?ContainerInterface $container)
57
    {
58 107
        $this->container = $container;
59 107
    }
60
61
    /**
62
     * @inheritDoc
63
     *
64
     * @param string $id
65
     *
66
     * @return mixed|object
67
     * @psalm-suppress InvalidThrow
68
     */
69 54
    public function get($id)
70
    {
71 54
        if ($this->hasDefinition($id)) {
72 36
            return $this->build($id);
73
        }
74
75 21
        if ($this->hasInContainer($id)) {
76 14
            return $this->container->get($id);
77
        }
78
79 7
        throw new NotInstantiableClassException($id);
80
    }
81
82
    public function has($id): bool
83
    {
84
        return $this->hasDefinition($id) || $this->hasInContainer($id);
85
    }
86
87
    /**
88
     * @return mixed
89
     */
90 94
    public function create(DefinitionInterface $definition)
91
    {
92 94
        if ($definition instanceof ArrayDefinition) {
93 68
            $this->creatingIds[$definition->getClass()] = 1;
94
        }
95
96
        try {
97 94
            return $definition->resolve($this);
98
        } finally {
99 94
            if ($definition instanceof ArrayDefinition) {
100 94
                unset($this->creatingIds[$definition->getClass()]);
101
            }
102
        }
103
    }
104
105
    /**
106
     * Get definition by identifier provided.
107
     *
108
     * @throws InvalidConfigException
109
     */
110 71
    public function getDefinition(string $id): DefinitionInterface
111
    {
112 71
        if (!isset($this->definitionInstances[$id])) {
113 71
            if (isset($this->definitions[$id])) {
114 71
                if (is_object($this->definitions[$id]) && !($this->definitions[$id] instanceof ReferenceInterface)) {
115 24
                    return Normalizer::normalize(clone $this->definitions[$id], $id);
116
                }
117 64
                $this->definitionInstances[$id] = Normalizer::normalize($this->definitions[$id], $id);
118
            } else {
119
                /** @psalm-var class-string $id */
120
                $this->definitionInstances[$id] = ArrayDefinition::fromPreparedData($id);
121
            }
122
        }
123
124 63
        return $this->definitionInstances[$id];
125
    }
126
127
    /**
128
     * Check if there is a definition with a given identifier.
129
     *
130
     * @param string $id Identifier to look for.
131
     *
132
     * @return bool If there is a definition with a given identifier.
133
     */
134 98
    public function hasDefinition(string $id): bool
135
    {
136 98
        return array_key_exists($id, $this->definitions);
137
    }
138
139
    /**
140
     * Set definition for a given identifier.
141
     *
142
     * @param string $id Identifier to set definition for.
143
     * @param mixed $definition Definition to set.
144
     */
145 71
    public function setDefinition(string $id, $definition): void
146
    {
147 71
        $this->definitions[$id] = $definition;
148 71
    }
149
150
    /**
151
     * @param string $id
152
     *
153
     * @throws CircularReferenceException
154
     * @throws InvalidConfigException
155
     * @throws NotFoundException
156
     * @throws NotInstantiableException
157
     *
158
     * @return mixed|object
159
     */
160 36
    private function build(string $id)
161
    {
162 36
        if (isset($this->creatingIds[$id])) {
163 5
            throw new CircularReferenceException(sprintf(
164 5
                'Circular reference to "%s" detected while creating: %s.',
165
                $id,
166 5
                implode(',', array_keys($this->creatingIds))
167
            ));
168
        }
169
170 35
        $definition = $this->getDefinition($id);
171
172 35
        $this->creatingIds[$id] = 1;
173
        try {
174 35
            return $definition->resolve($this);
175
        } finally {
176 35
            unset($this->creatingIds[$id]);
177
        }
178
    }
179
180
    /**
181
     * @psalm-assert ContainerInterface $this->container
182
     */
183 21
    private function hasInContainer(string $id): bool
184
    {
185 21
        return $this->container !== null && $this->container->has($id);
186
    }
187
}
188