Container   A
last analyzed

Complexity

Total Complexity 18

Size/Duplication

Total Lines 231
Duplicated Lines 0 %

Coupling/Cohesion

Dependencies 1

Importance

Changes 0
Metric Value
wmc 18
dl 0
loc 231
c 0
b 0
f 0
cbo 1
rs 10

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A offsetExists() 0 4 1
B offsetGet() 0 19 5
A offsetSet() 0 12 3
B offsetUnset() 0 18 5
A get() 0 11 2
A has() 0 4 1
A alias() 0 14 4
A find() 0 12 3
A keys() 0 4 1
A factory() 0 9 3
A lock() 0 6 2
A resolveIdentifier() 0 8 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Jarvis\Skill\DependencyInjection;
6
7
use Psr\Container\ContainerInterface;
8
use Psr\Container\NotFoundExceptionInterface;
9
10
/**
11
 * Minimalist dependency injection container
12
 *
13
 * @author Eric Chau <[email protected]>
14
 */
15
class Container implements \ArrayAccess, ContainerInterface
16
{
17
    protected $locked = [];
18
    private $aliasOf = [];
19
    private $factories;
20
    private $hasAliases = [];
21
    private $raw = [];
22
    private $values = [];
23
24
    public function __construct()
25
    {
26
        $this->factories = new \SplObjectStorage();
27
    }
28
29
    /**
30
     * Checks if a parameter or an object is defined.
31
     *
32
     * @param  string $id the parameter/object identifier to check
33
     * @return boolean
34
     */
35
    public function offsetExists($id): bool
36
    {
37
        return in_array($id, $this->keys());
38
    }
39
40
    /**
41
     * Gets a parameter or an object.
42
     *
43
     * @param  string $id the parameter/object identifier
44
     * @return mixed The requested value
45
     * @throws \InvalidArgumentException if provided identifier is not defined
46
     */
47
    public function offsetGet($id)
48
    {
49
        $id = $this->resolveIdentifier($id);
50
        if (
51
            isset($this->raw[$id])
52
            || !is_object($this->values[$id])
53
            || !method_exists($this->values[$id], '__invoke')
54
        ) {
55
            return $this->values[$id];
56
        }
57
58
        if (isset($this->factories[$this->values[$id]])) {
59
            return $this->values[$id]($this);
60
        }
61
62
        $this->raw[$id] = $this->values[$id];
63
64
        return $this->values[$id] = $this->raw[$id]($this);
65
    }
66
67
    /**
68
     * Sets a parameter or an object.
69
     *
70
     * Note that you cannot override locked value, you have to call ::offsetUnset first.
71
     *
72
     * @param  string $id the identifier for parameter or object
73
     * @param  mixed  $v  the value of the parameter or an object
74
     * @throws \RuntimeException prevents override of locked value
75
     * @throws \InvalidArgumentException prevents value's identifier to be equal to an existing alias
76
     */
77
    public function offsetSet($id, $v): void
78
    {
79
        if (isset($this->locked[$id])) {
80
            throw new \RuntimeException(sprintf('Cannot override locked value "%s".', $id));
81
        }
82
83
        if (isset($this->aliasOf[$id])) {
84
            throw new \InvalidArgumentException("Value's identifier cannot be equal to existing alias.");
85
        }
86
87
        $this->values[$id] = $v;
88
    }
89
90
    /**
91
     * Unsets a parameter or an object. It can also unset an alias.
92
     *
93
     * Note that if you unset a value it will also unset all its aliases.
94
     *
95
     * @param  string $id the identifier of the object/parameter to unset
96
     */
97
    public function offsetUnset($id): void
98
    {
99
        if (isset($this->values[$id])) {
100
            if (is_object($this->values[$id])) {
101
                unset($this->factories[$this->values[$id]]);
102
            }
103
104
            if (isset($this->hasAliases[$id])) {
105
                foreach ($this->hasAliases[$id] as $alias) {
106
                    unset($this->aliasOf[$alias]);
107
                }
108
            }
109
110
            unset($this->values[$id], $this->raw[$id], $this->hasAliases[$id], $this->locked[$id]);
111
        } else {
112
            unset($this->aliasOf[$id]);
113
        }
114
    }
115
116
    /**
117
     * {@inheritdoc}
118
     * {@see ContainerInterface::get}
119
     *
120
     * This method has been added to be compatible with PSR-11.
121
     */
122
    public function get($id)
123
    {
124
        try {
125
            return $this->offsetGet($id);
126
        } catch (\InvalidArgumentException $exception) {
127
            throw new class($exception->getMessage())
128
                extends \InvalidArgumentException
129
                implements NotFoundExceptionInterface {}
130
            ;
131
        }
132
    }
133
134
    /**
135
     * {@inheritdoc}
136
     * {@see ContainerInterface::has}
137
     *
138
     * This method has been added to be compatible with PSR-11.
139
     */
140
    public function has($id)
141
    {
142
        return $this->offsetExists($id);
143
    }
144
145
    /**
146
     * Adds alias to service's identifier.
147
     *
148
     * @param  string $alias the alias to identifier
149
     * @param  string $id    the identifier to alias
150
     * @return self
151
     * @throws InvalidArgumentException if provided identifier is undefined or if alias is
152
     *                                  equals to identifier
153
     */
154
    public function alias(string $alias, string $id): void
155
    {
156
        if (!$this->offsetExists($id)) {
157
            throw new \InvalidArgumentException(sprintf('Cannot create alias for undefined value "%s".', $id));
158
        }
159
160
        if ($alias === $id || array_key_exists($alias, $this->values)) {
161
            throw new \InvalidArgumentException('Alias cannot be equals to value identifier.');
162
        }
163
164
        $this->aliasOf[$alias] = $id;
165
        $this->hasAliases[$id] = $this->hasAliases[$id] ?? [];
166
        $this->hasAliases[$id][] = $alias;
167
    }
168
169
    /**
170
     * Retrieves parameter and/or object by identifier.
171
     * this method also support wildcard character (*) in identifier.
172
     *
173
     * @param  string $id this identifier can contain one or many wildcard character (*)
174
     * @return array an array of values that mached with provided identifier pattern
175
     */
176
    public function find(string $id): array
177
    {
178
        $values = [];
179
        $pattern = str_replace(['.', '*'], ['\.', '[\w\-\.]*'], $id);
180
        foreach ($this->keys() as $id) {
181
            if (1 === preg_match(sprintf('/^%s$/', $pattern), $id)) {
182
                $values[] = $this->offsetGet($id);
183
            }
184
        }
185
186
        return $values;
187
    }
188
189
    /**
190
     * Returns all defined identifiers and aliases.
191
     *
192
     * @return array
193
     */
194
    public function keys(): array
195
    {
196
        return array_merge(array_keys($this->values), array_keys($this->aliasOf));
197
    }
198
199
    /**
200
     * Add provided service as factory.
201
     *
202
     * @param  string $id      the factory identifier
203
     * @param  mixed  $factory the factory object
204
     * @return self
205
     * @throws InvalidArgumentException if provided factory is not a Closure or not an invokable object
206
     */
207
    public function factory(string $id, $factory): void
208
    {
209
        if (!is_object($factory) || !method_exists($factory, '__invoke')) {
210
            throw new \InvalidArgumentException('Service factory must be a closure or an invokable object.');
211
        }
212
213
        $this->offsetSet($id, $factory);
214
        $this->factories->attach($factory);
215
    }
216
217
    /**
218
     * Locks an object or a parameter so you can not override it until unset() is called.
219
     *
220
     * @param  string|array $ids the identifier(s) to lock
221
     * @return self
222
     */
223
    public function lock($ids): void
224
    {
225
        foreach ((array) $ids as $id) {
226
            $this->locked[$this->resolveIdentifier($id)] = true;
227
        }
228
    }
229
230
    /**
231
     * Returns associated identifier if provided argument is an alias.
232
     *
233
     * @param  string $id the identifier to convert if needed
234
     * @return string
235
     * @throws \InvalidArgumentException if provided identifier is not defined
236
     */
237
    private function resolveIdentifier(string $id): string
238
    {
239
        if (!$this->offsetExists($id)) {
240
            throw new \InvalidArgumentException(sprintf('Identifier "%s" is not defined.', $id));
241
        }
242
243
        return $this->aliasOf[$id] ?? $id;
244
    }
245
}
246