Passed
Push — master ( 6a389c...42f43f )
by Divine Niiquaye
01:03 queued 12s
created

Container::set()   A

Complexity

Conditions 6
Paths 8

Size

Total Lines 26
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 6

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 15
c 5
b 0
f 0
dl 0
loc 26
ccs 16
cts 16
cp 1
rs 9.2222
cc 6
nc 8
nop 3
crap 6
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of DivineNii opensource projects.
7
 *
8
 * PHP version 7.4 and above required
9
 *
10
 * @author    Divine Niiquaye Ibok <[email protected]>
11
 * @copyright 2021 DivineNii (https://divinenii.com/)
12
 * @license   https://opensource.org/licenses/BSD-3-Clause License
13
 *
14
 * For the full copyright and license information, please view the LICENSE
15
 * file that was distributed with this source code.
16
 */
17
18
namespace Rade\DI;
19
20
use Rade\DI\{
21
    Builder\Statement,
22
    Exceptions\CircularReferenceException,
23
    Exceptions\FrozenServiceException,
24
    Exceptions\NotFoundServiceException,
25
    Exceptions\ContainerResolutionException
26
};
27
use Rade\DI\Resolvers\Resolver;
28
use Symfony\Contracts\Service\ResetInterface;
29
30
/**
31
 * Dependency injection container.
32
 *
33
 * @author Divine Niiquaye Ibok <[email protected]>
34
 */
35
class Container extends AbstractContainer implements \ArrayAccess
36
{
37
    public const IGNORE_FROM_FREEZING = 2;
38
39
    /** @var array<string,string> internal cached services */
40
    protected array $methodsMap = [];
41
42
    /** @var array<string,mixed> service name => instance */
43
    private array $values = [];
44
45
    /** @var RawDefinition[] */
46
    private array $raw = [];
47
48
    /** @var array<string,bool> service name => bool */
49
    private array $frozen = [];
50
51
    /** @var array<string,bool> service name => bool */
52
    private array $keys = [];
53
54
    /**
55
     * Instantiates the container.
56
     */
57 104
    public function __construct()
58
    {
59 104
        parent::__construct();
60 104
        $this->types += [self::class => ['container']];
61 104
    }
62
63
    /**
64
     * Sets a new service to a unique identifier.
65
     *
66
     * @param string $offset The unique identifier for the parameter or object
67
     * @param mixed  $value  The value of the service assign to the $offset
68
     *
69
     * @throws FrozenServiceException Prevent override of a frozen service
70
     */
71 62
    public function offsetSet($offset, $value): void
72
    {
73 62
        $this->set($offset, $value, true);
74 62
    }
75
76
    /**
77
     * Gets a registered service definition.
78
     *
79
     * @param string $offset The unique identifier for the service
80
     *
81
     * @throws NotFoundServiceException If the identifier is not defined
82
     *
83
     * @return mixed The value of the service
84
     */
85 55
    public function offsetGet($offset)
86
    {
87 55
        return $this->get($offset);
88
    }
89
90
    /**
91
     * Checks if a service is set.
92
     *
93
     * @param string $offset The unique identifier for the service
94
     */
95 7
    public function offsetExists($offset): bool
96
    {
97 7
        return $this->has($offset);
98
    }
99
100
    /**
101
     * Unset a service by given offset.
102
     *
103
     * @param string $offset The unique identifier for service definition
104
     */
105 6
    public function offsetUnset($offset): void
106
    {
107 6
        $this->remove($offset);
108 6
    }
109
110
    /**
111
     * A helper method of Statement class for calling a service lazily.
112
     *
113
     * @param callable|string         $definition A class string or a callable
114
     * @param array<int|string,mixed> $args
115
     */
116 17
    public function lazy($definition, array $args = []): Statement
117
    {
118 17
        return new Statement($definition, $args);
119
    }
120
121
    /**
122
     * Create a definition service.
123
     *
124
     * @param Definition|object|callable|string $service
125
     * @param int|null                          $type    of Definition::FACTORY | Definition::LAZY
126
     */
127 11
    public function definition($service, int $type = null): Definition
128
    {
129 11
        $definition = new Definition($service);
130
131 11
        return null === $type ? $definition : $definition->should($type);
132
    }
133
134
    /**
135
     * Extends an object definition.
136
     *
137
     * Useful when you want to extend an existing object definition,
138
     * without necessarily loading that object.
139
     *
140
     * @param string   $id    The unique identifier for the object
141
     * @param callable $scope A service definition to extend the original
142
     *
143
     * @throws NotFoundServiceException   If the identifier is not defined
144
     * @throws FrozenServiceException     If the service is frozen
145
     * @throws CircularReferenceException If infinite loop among service is detected
146
     *
147
     * @return mixed The wrapped scope or Definition instance
148
     */
149 10
    public function extend(string $id, callable $scope)
150
    {
151 10
        if ($this->frozen[$id] ?? isset($this->methodsMap[$id])) {
152 2
            throw new FrozenServiceException($id);
153
        }
154
155 8
        $extended = $this->raw[$id] ?? $this->values[$id] ?? $this->createNotFound($id, true);
156
157 7
        if ($extended instanceof RawDefinition) {
158 1
            return $this->raw[$id] = new RawDefinition($scope($extended(), $this));
159
        }
160
161 6
        if (\is_callable($extended)) {
162 6
            $extended = $extended instanceof Definition ? $extended : $this->doCreate($id, $extended);
163
        }
164
165 5
        return $this->values[$id] = $scope($extended, $this);
166
    }
167
168
    /**
169
     * {@inheritdoc}
170
     */
171 24
    public function keys(): array
172
    {
173 24
        return \array_keys($this->keys + $this->methodsMap);
174
    }
175
176
    /**
177
     * {@inheritdoc}
178
     */
179 4
    public function reset(): void
180
    {
181 4
        parent::reset();
182
183 4
        foreach ($this->values as $id => $service) {
184 2
            if (isset(self::$services[$id])) {
185 2
                $service = self::$services[$id];
186 2
                unset(self::$services[$id]);
187
            }
188
189 2
            if ($service instanceof ResetInterface) {
190 1
                $service->reset();
191
            }
192
193 2
            $this->remove($id);
194
        }
195
196 4
        self::$services['container'] = $this;
197 4
    }
198
199
    /**
200
     * {@inheritdoc}
201
     */
202 9
    public function remove(string $id): void
203
    {
204 9
        if (isset($this->keys[$id])) {
205 8
            unset($this->values[$id], $this->raw[$id], $this->keys[$id], $this->frozen[$id]);
206
        }
207
208 9
        parent::remove($id);
209 9
    }
210
211
    /**
212
     * {@inheritdoc}
213
     */
214 12
    public function service(string $id)
215
    {
216 12
        return $this->values[$this->aliases[$id] ?? $id] ?? $this->createNotFound($id, true);
217
    }
218
219
    /**
220
     * {@inheritdoc}
221
     */
222 82
    public function get(string $id, int $invalidBehavior = /* self::EXCEPTION_ON_MULTIPLE_SERVICE */ 1)
223
    {
224 82
        if (isset($this->aliases[$id])) {
225 3
            $id = $this->aliases[$id];
226
        }
227
228 82
        return self::$services[$id] ?? $this->{$this->methodsMap[$id] ?? 'doGet'}($id, $invalidBehavior);
229
    }
230
231
    /**
232
     * {@inheritdoc}
233
     */
234 75
    public function has(string $id): bool
235
    {
236 75
        return ($this->keys[$id] ?? isset($this->methodsMap[$id])) || isset($this->aliases[$id]);
237
    }
238
239
    /**
240
     * Set a service definition.
241
     *
242
     * @param Definition|RawDefinition|Statement|\Closure|object $definition
243
     *
244
     * @throws FrozenServiceException Prevent override of a frozen service
245
     *
246
     * @return Definition|RawDefinition|object|\Closure of Definition, RawService, class object or closure
247
     */
248 105
    public function set(string $id, object $definition, bool $autowire = false)
249
    {
250 105
        if ($this->frozen[$id] ?? isset($this->methodsMap[$id])) {
251 1
            throw new FrozenServiceException($id);
252
        }
253
254 105
        unset($this->aliases[$id]); // Incase new service definition exists in aliases.
255 105
        $this->keys[$id] = true;
256
257 105
        if ($definition instanceof RawDefinition) {
258 17
            return $this->raw[$id] = $definition;
259
        }
260
261 96
        if ($definition instanceof Definition) {
262 42
            $typed = $definition->get('entity');
263 42
            $definition->withContainer($id, $this);
264 61
        } elseif ($definition instanceof Statement) {
265 17
            $typed = $definition->value;
266 17
            $definition = fn () => $this->resolver->resolve($typed, $definition->args);
267
        }
268
269 96
        if ($autowire) {
270 57
            $this->type($id, Resolver::autowireService($typed ?? $definition));
271
        }
272
273 96
        return $this->values[$id] = $definition;
274
    }
275
276
    /**
277
     * Build an entry of the container by its name.
278
     *
279
     * @throws CircularReferenceException|NotFoundServiceException
280
     *
281
     * @return mixed
282
     */
283 79
    protected function doGet(string $id, int $invalidBehavior)
284
    {
285 79
        if (isset($this->raw[$id])) {
286 11
            $rawService = $this->raw[$id];
287 11
            unset($this->raw[$id]);
288
289 11
            return self::$services[$id] = $rawService();
290
        }
291
292 72
        if (isset($this->values[$id])) {
293 58
            if (\is_callable($definition = $this->values[$id])) {
294 46
                if ($definition instanceof Definition) {
295 13
                    if (!$definition->isPublic()) {
296 1
                        throw new ContainerResolutionException(\sprintf('Using service definition for "%s" as private is not supported.', $id));
297
                    }
298
299 12
                    if ($definition->isFactory()) {
300 5
                        return $this->doCreate($id, $definition);
301
                    }
302
                }
303
304 41
                $definition = $this->doCreate($id, $definition, self::IGNORE_FROM_FREEZING !== $invalidBehavior);
305 33
            } elseif (self::IGNORE_FROM_FREEZING !== $invalidBehavior) {
306 32
                $this->frozen[$id] = true;
307
308 32
                return self::$services[$id] = $definition; // If definition is frozen, cache it ...
309
            }
310
311 34
            return $this->values[$id] = $definition;
312
        }
313
314 26
        if (isset($this->types[$id])) {
315 14
            return $this->autowired($id, self::EXCEPTION_ON_MULTIPLE_SERVICE === $invalidBehavior);
316
        }
317
318 18
        if (\class_exists($id) && !$this instanceof FallbackContainer) {
319
            try {
320 9
                return $this->resolver->resolveClass($id);
321 1
            } catch (ContainerResolutionException $e) {
322
                // Only resolves class string and not throw it's error.
323
            }
324
        }
325
326 13
        throw $this->createNotFound($id);
327
    }
328
329
    /**
330
     * {@inheritdoc}
331
     */
332 46
    protected function doCreate(string $id, $service, bool $freeze = false)
333
    {
334
        // Checking if circular reference exists ...
335 46
        if (isset($this->loading[$id])) {
336 2
            throw new CircularReferenceException($id, [...\array_keys($this->loading), $id]);
337
        }
338
339 46
        $this->loading[$id] = true;
340
341
        try {
342 46
            return $this->resolver->resolve($service);
343
        } finally {
344 46
            unset($this->loading[$id]);
345
346 46
            if ($freeze) {
347 46
                $this->frozen[$id] = true; // Freeze resolved service ...
348
            }
349
        }
350
    }
351
}
352