Passed
Branch feature/code-quality (2bf2a5)
by Filipe
13:16
created

Container   A

Complexity

Total Complexity 25

Size/Duplication

Total Lines 236
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 60
dl 0
loc 236
rs 10
c 4
b 0
f 0
wmc 25

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 2
A register() 0 17 3
A container() 0 3 1
A has() 0 7 2
A get() 0 14 5
A parentHas() 0 6 2
A add() 0 9 2
A parent() 0 3 1
A registerEntry() 0 9 2
A make() 0 20 2
A resolve() 0 12 3
1
<?php
2
3
/**
4
 * This file is part of slick/di package
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
namespace Slick\Di;
11
12
use ReflectionClass;
13
use ReflectionException;
14
use Slick\Di\Definition\Alias;
15
use Slick\Di\Definition\CreateDefinitionsMethods;
16
use Slick\Di\Definition\Factory;
17
use Slick\Di\Definition\ObjectDefinition;
18
use Slick\Di\Definition\Scope;
19
use Slick\Di\Definition\Value;
20
use Slick\Di\Exception\NotFoundException;
21
use Slick\Di\Inspector\ConstructorArgumentInspector;
22
23
/**
24
 * Container
25
 *
26
 * @package Slick\Di
27
 * @author  Filipe Silva <[email protected]>
28
 */
29
class Container implements ContainerInterface
30
{
31
    use CreateDefinitionsMethods;
32
33
    /**
34
     * @var array
35
     */
36
    protected array $definitions = [];
37
38
    /**
39
     * @var array
40
     */
41
    protected static array $instances = [];
42
43
    /**
44
     * @var null|ContainerInterface
45
     */
46
    protected mixed $parent;
47
48
    /**
49
     * Creates a dependency container
50
     */
51
    public function __construct()
52
    {
53
        $this->parent = array_key_exists('container', self::$instances)
54
            ? self::$instances['container']
55
            : null;
56
57
        self::$instances['container'] = $this;
58
    }
59
60
    /**
61
     * Finds an entry of the container by its identifier and returns it.
62
     *
63
     * @param string $id Identifier of the entry to look for.
64
     *
65
     * @return mixed Entry.
66
     *@throws NotFoundException  No entry was found for this identifier.
67
     *
68
     */
69
    public function get(string $id): mixed
70
    {
71
        if (!$this->has($id) && $id !== 'container') {
72
            if (class_exists($id)) {
73
                $reflectionClass = new ReflectionClass($id);
74
                if ($reflectionClass->isInstantiable()) {
75
                    return $this->make($id);
76
                }
77
            }
78
            throw new NotFoundException(
79
                "Dependency container has not found any definition for '$id'"
80
            );
81
        }
82
        return $this->resolve($id);
83
    }
84
85
    /**
86
     * Returns true if the container can return an entry for the given
87
     * identifier. Returns false otherwise.
88
     *
89
     * @param string $id Identifier of the entry to look for.
90
     *
91
     * @return boolean
92
     */
93
    public function has(string $id): bool
94
    {
95
        if (!array_key_exists($id, $this->definitions)) {
96
            return $this->parentHas($id);
97
        }
98
99
        return true;
100
    }
101
102
    /**
103
     * Adds a definition or a value to the container
104
     *
105
     * @param string $name
106
     * @param mixed|null $definition
107
     * @param string|Scope $scope      Resolving scope
108
     * @param array        $parameters Used if $value is a callable
109
     *
110
     * @return Container
111
     */
112
    public function register(
113
        string       $name,
114
        mixed        $definition = null,
115
        string|Scope $scope = Scope::SINGLETON,
116
        array        $parameters = []
117
    ): Container {
118
        if ($definition instanceof DefinitionInterface) {
119
            return $this->add($name, $definition);
120
        }
121
122
        $definition = $this->createDefinition(
123
            $definition,
124
            $parameters
125
        );
126
        $scope = is_string($scope) ? new Scope($scope) : $scope;
127
        $definition->setScope($scope);
128
        return $this->add($name, $definition);
129
    }
130
131
    /**
132
     * Checks if parent has a provided key
133
     *
134
     * @param string $key
135
     *
136
     * @return bool
137
     */
138
    protected function parentHas(string $key): bool
139
    {
140
        if (!$this->parent) {
141
            return false;
142
        }
143
        return $this->parent->has($key);
144
    }
145
146
    /**
147
     * Resolves the definition that was saved under the provided name
148
     *
149
     * @param string $name
150
     *
151
     * @return mixed
152
     *
153
     * @throws NotFoundException
154
     */
155
    protected function resolve(string $name): mixed
156
    {
157
        if (array_key_exists($name, self::$instances)) {
158
            return self::$instances[$name];
159
        }
160
161
        if (array_key_exists($name, $this->definitions)) {
162
            $entry = $this->definitions[$name];
163
            return $this->registerEntry($name, $entry);
164
        }
165
166
        return $this->parent->get($name);
167
    }
168
169
    /**
170
     * Checks the definition scope to register resolution result
171
     *
172
     * If scope is set to prototype the resolution result is not
173
     * stores in the container instances.
174
     *
175
     * @param string $name
176
     * @param DefinitionInterface $definition
177
     * @return mixed
178
     */
179
    protected function registerEntry(string $name, DefinitionInterface $definition): mixed
180
    {
181
        $value = $definition
182
            ->setContainer($this->container())
183
            ->resolve();
0 ignored issues
show
Bug introduced by
The method resolve() does not exist on Slick\Di\ContainerAwareInterface. It seems like you code against a sub-type of Slick\Di\ContainerAwareInterface such as Slick\Di\Definition\Object\ResolverInterface or Slick\Di\DefinitionInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

183
            ->/** @scrutinizer ignore-call */ resolve();
Loading history...
184
        if ((string) $definition->getScope() !== Scope::PROTOTYPE) {
185
            self::$instances[$name] = $value;
186
        }
187
        return $value;
188
    }
189
190
    /**
191
     * Adds a definition to the definitions list
192
     *
193
     * This method does not override an existing entry if the same name exists
194
     * in the definitions or in any definitions of its parents.
195
     * This way it is possible to change entries defined by other packages
196
     * as those are build after the main application container is build.
197
     * The main application container should be the first to be created and
198
     * therefore set any entry that will override the latest containers build.
199
     *
200
     * @param string $name
201
     * @param DefinitionInterface $definition
202
     *
203
     * @return Container
204
     */
205
    protected function add(string $name, DefinitionInterface $definition): static
206
    {
207
        if ($this->has($name)) {
208
            return $this;
209
        }
210
211
        $this->definitions[$name] = $definition;
212
        $definition->setContainer($this->container());
213
        return $this;
214
    }
215
216
    /**
217
     * Creates an instance of provided class injecting its dependencies
218
     *
219
     * @param string $className
220
     * @param array ...$arguments
221
     *
222
     * @return mixed
223
     * @throws ReflectionException
224
     */
225
    public function make(string $className, ...$arguments): mixed
226
    {
227
228
229
        if (is_a($className, ContainerInjectionInterface::class, true)) {
230
            return call_user_func_array([$className, 'create'], [$this]);
231
        }
232
233
        $definition = (new ObjectDefinition($className))
234
            ->setContainer($this->container())
235
        ;
236
237
        $arguments = (new ConstructorArgumentInspector(
238
            new ReflectionClass($className),
239
            $arguments
240
        ))
241
            ->arguments();
242
243
        call_user_func_array([$definition, 'with'], $arguments);
244
        return $definition->resolve();
245
    }
246
247
    /**
248
     * Gets the parent container if it exists
249
     *
250
     * @return null|ContainerInterface
251
     */
252
    public function parent(): ?ContainerInterface
253
    {
254
        return $this->parent;
255
    }
256
257
    /**
258
     * Get the top container
259
     *
260
     * @return container
261
     */
262
    private function container(): Container
263
    {
264
        return self::$instances['container'];
265
    }
266
}
267