Completed
Push — develop ( f0edc8...54f05a )
by Filipe
22s queued 20s
created

Container::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 4
nc 2
nop 0
dl 0
loc 7
ccs 3
cts 3
cp 1
crap 2
rs 10
c 1
b 0
f 0
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 74
            ? self::$instances['container']
55
            : null;
56 74
57 74
        self::$instances['container'] = $this;
58 74
    }
59 72
60 72
    /**
61
     * Finds an entry of the container by its identifier and returns it.
62 74
     *
63 74
     * @param string $id Identifier of the entry to look for.
64 74
     *
65 74
     * @return mixed Entry.
66 74
     *@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 52
            throw new NotFoundException(
79
                "Dependency container has not found any definition for '$id'"
80 52
            );
81 2
        }
82 2
        return $this->resolve($id);
83
    }
84 2
85
    /**
86
     * Returns true if the container can return an entry for the given
87 50
     * identifier. Returns false otherwise.
88 50
     *
89 50
     * @param string $id Identifier of the entry to look for.
90
     *
91 50
     * @return boolean
92 46
     */
93 46
    public function has(string $id): bool
94
    {
95 50
        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 56
     * @param mixed|null $definition
107
     * @param string|Scope $scope      Resolving scope
108 56
     * @param array        $parameters Used if $value is a callable
109 28
     *
110
     * @return Container
111
     */
112 56
    public function register(
113 18
        string       $name,
114
        mixed        $definition = null,
115 52
        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 74
     *
134
     * @param string $key
135
     *
136
     * @return bool
137 74
     */
138 72
    protected function parentHas(string $key): bool
139 32
    {
140 32
        if (!$this->parent) {
141 32
            return false;
142 32
        }
143 32
        return $this->parent->has($key);
144 32
    }
145 32
146
    /**
147 72
     * Resolves the definition that was saved under the provided name
148 72
     *
149 72
     * @param string $name
150 72
     *
151
     * @return mixed
152 72
     *
153 72
     * @throws NotFoundException
154
     */
155 72
    protected function resolve(string $name): mixed
156
    {
157 72
        if (array_key_exists($name, self::$instances)) {
158 72
            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 10
     * @param string $name
176
     * @param DefinitionInterface $definition
177
     * @return mixed
178 10
     */
179 2
    protected function registerEntry(string $name, DefinitionInterface $definition): mixed
180
    {
181 2
        $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 8
        if ((string) $definition->getScope() !== Scope::PROTOTYPE) {
185 4
            self::$instances[$name] = $value;
186 4
        }
187
        return $value;
188 8
    }
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 46
     * therefore set any entry that will override the latest containers build.
199
     *
200 46
     * @param string $name
201 46
     * @param DefinitionInterface $definition
202 46
     *
203 46
     * @return Container
204 46
     */
205 46
    protected function add(string $name, DefinitionInterface $definition): static
206 46
    {
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 70
    /**
217
     * Creates an instance of provided class injecting its dependencies
218 70
     *
219 70
     * @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 72
            return call_user_func_array([$className, 'create'], [$this]);
231
        }
232 72
233
        $definition = (new ObjectDefinition($className))
234 72
            ->setContainer($this->container())
235
        ;
236 72
237 72
        $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 32
     * @return null|ContainerInterface
251
     */
252
    public function parent(): ?ContainerInterface
253 32
    {
254
        return $this->parent;
255 32
    }
256 32
257
    /**
258
     * Get the top container
259
     *
260
     * @return container
261
     */
262
    private function container(): Container
263
    {
264
        return self::$instances['container'];
265 50
    }
266
}
267