Completed
Push — master ( 4a8896...30bfd7 )
by Filipe
02:12
created

Container::make()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 4
cts 4
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 9
nc 2
nop 2
crap 2
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 Interop\Container\Exception\ContainerException;
13
use Slick\Di\Definition\Alias;
14
use Slick\Di\Definition\Factory;
15
use Slick\Di\Definition\ObjectDefinition;
16
use Slick\Di\Definition\Scope;
17
use Slick\Di\Definition\Value;
18
use Slick\Di\Exception\NotFoundException;
19
20
/**
21
 * Container
22
 *
23
 * @package Slick\Di
24
 * @author  Filipe Silva <[email protected]>
25
 */
26
class Container implements ContainerInterface, ObjectHydratorAwareInterface
27
{
28
    /**
29
     * @var array
30
     */
31
    protected $definitions = [];
32
33
    /**
34
     * @var array
35
     */
36
    protected static $instances = [];
37
38
    /**
39
     * @var ObjectHydratorInterface
40
     */
41
    protected $hydrator;
42
43
    /**
44
     * @var null|ContainerInterface
45
     */
46
    protected $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
     * @throws NotFoundException  No entry was found for this identifier.
66 74
     * @throws ContainerException Error while retrieving the entry.
67
     *
68
     * @return mixed Entry.
69
     */
70
    public function get($id)
71
    {
72
        if (!$this->has($id) && $id !== 'container') {
73
            throw new NotFoundException(
74
                "Dependency container has not found any definition for '{$id}'"
75
            );
76
        }
77
        return $this->resolve($id);
78 52
    }
79
80 52
    /**
81 2
     * Returns true if the container can return an entry for the given
82 2
     * identifier. Returns false otherwise.
83
     *
84 2
     * @param string $id Identifier of the entry to look for.
85
     *
86
     * @return boolean
87 50
     */
88 50
    public function has($id)
89 50
    {
90
        if (!array_key_exists($id, $this->definitions)) {
91 50
            return $this->parentHas($id);
92 46
        }
93 46
94
        return true;
95 50
    }
96
97
    /**
98
     * Adds a definition or a value to the container
99
     *
100
     * @param string       $name
101
     * @param mixed        $definition
102
     * @param Scope|string $scope      Resolving scope
103
     * @param array        $parameters Used if $value is a callable
104
     *
105
     * @return Container
106 56
     */
107
    public function register(
108 56
        $name,
109 28
        $definition = null,
110
        $scope = Scope::SINGLETON,
111
        array $parameters = []
112 56
    ) {
113 18
        if (!$definition instanceof DefinitionInterface) {
114
            $definition = $this->createDefinition(
115 52
                $definition,
116
                $parameters
117
            );
118
            $definition->setScope($scope);
119
        }
120
        return $this->add($name, $definition);
121
    }
122
123
    /**
124
     * Checks if parent has a provided key
125
     *
126
     * @param string $key
127
     *
128
     * @return bool
129
     */
130
    protected function parentHas($key)
131
    {
132
        if (!$this->parent) {
133 74
            return false;
134
        }
135
        return $this->parent->has($key);
136
    }
137 74
138 72
    /**
139 32
     * Resolves the definition that was saved under the provided name
140 32
     *
141 32
     * @param string $name
142 32
     *
143 32
     * @return mixed
144 32
     */
145 32
    protected function resolve($name)
146
    {
147 72
        if (array_key_exists($name, self::$instances)) {
148 72
            return self::$instances[$name];
149 72
        }
150 72
151
        if (array_key_exists($name, $this->definitions)) {
152 72
            $entry = $this->definitions[$name];
153 72
            return $this->registerEntry($name, $entry);
154
        }
155 72
156
        return $this->parent->get($name);
157 72
    }
158 72
159
    /**
160
     * Checks the definition scope to register resolution result
161
     *
162
     * If scope is set to prototype the the resolution result is not
163
     * stores in the container instances.
164
     *
165
     * @param string              $name
166
     * @param DefinitionInterface $definition
167
     * @return mixed
168
     */
169
    protected function registerEntry($name, DefinitionInterface $definition)
170
    {
171
        $value = $definition->resolve();
172
        if ((string) $definition->getScope() !== Scope::PROTOTYPE) {
173
            self::$instances[$name] = $value;
174
        }
175 10
        return $value;
176
    }
177
178 10
    /**
179 2
     * Adds a definition to the definitions list
180
     *
181 2
     * This method does not override an existing entry if the same name exists
182
     * in the definitions or in any definitions of its parents.
183
     * This way it is possible to change entries defined by other packages
184 8
     * as those are build after the main application container is build.
185 4
     * The main application container should be the first to be created and
186 4
     * therefore set any entry that will override the latest containers build.
187
     *
188 8
     * @param string              $name
189
     * @param DefinitionInterface $definition
190
     *
191
     * @return Container
192
     */
193
    protected function add($name, DefinitionInterface $definition)
194
    {
195
        if ($this->has($name)) {
196
            return $this;
197
        }
198 46
199
        $this->definitions[$name] = $definition;
200 46
        $definition->setContainer($this);
201 46
        return $this;
202 46
    }
203 46
204 46
    /**
205 46
     * Creates the definition for registered data
206 46
     *
207
     * If value is a callable then the definition is Factory, otherwise
208
     * it will create a Value definition.
209
     *
210
     * @see Factory, Value
211
     *
212
     * @param callable|mixed $value
213
     * @param array          $parameters
214
     *
215
     * @return Factory|Value
216 70
     */
217
    protected function createDefinition(
218 70
        $value,
219 70
        array $parameters = []
220
    ) {
221
        if (is_callable($value)) {
222
            return new Factory($value, $parameters);
223
        }
224
        return $this->createValueDefinition($value);
225
    }
226
227
    /**
228
     * Creates a definition for provided name and value pair
229
     *
230 72
     * If $value is a string prefixed with '@' it will create an Alias
231
     * definition. Otherwise a Value definition will be created.
232 72
     *
233
     * @param mixed  $value
234 72
     *
235
     * @return Value|Alias
236 72
     */
237 72
    protected function createValueDefinition($value)
238
    {
239
        if (is_string($value) && strpos($value, '@') !== false) {
240
            return new Alias($value);
241
        }
242
243
        return new Value($value);
244
    }
245
246
    /**
247
     * Creates an instance of provided class injecting its dependencies
248
     *
249
     * @param string $className
250 32
     * @param array ...$arguments
251
     *
252
     * @return mixed
253 32
     */
254
    public function make($className, ...$arguments)
255 32
    {
256 32
        if (is_a($className, ContainerInjectionInterface::class, true)) {
257
            return call_user_func_array([$className, 'create'], [$this]);
258
        }
259
260
        $definition = (new ObjectDefinition($className))
261
            ->setContainer($this)
262
        ;
263
        call_user_func_array([$definition, 'with'], $arguments);
264
        $object = $definition->resolve();
0 ignored issues
show
Bug introduced by
It seems like resolve() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
265 50
        $this->getHydrator()->hydrate($object);
266
        return $object;
267 50
    }
268 8
269
    /**
270 46
     * Set the object hydrator
271
     *
272
     * @param ObjectHydratorInterface $hydrator
273
     *
274
     * @return Container|ObjectHydratorAwareInterface
275
     */
276
    public function setHydrator(ObjectHydratorInterface $hydrator)
277
    {
278
        $this->hydrator = $hydrator;
279 46
        return $this;
280
    }
281 46
282 46
    /**
283
     * Get the object hydrator
284 46
     *
285 14
     * @return ObjectHydratorInterface
286 14
     */
287 46
    public function getHydrator()
288
    {
289
        if (!$this->hydrator) {
290
            $this->setHydrator(new ObjectHydrator($this));
291
        }
292
        return $this->hydrator;
293
    }
294
295
    /**
296
     * Gets the parent container if it exists
297
     *
298
     * @return null|ContainerInterface
299 4
     */
300
    public function parent()
301 4
    {
302 4
        return $this->parent;
303 4
    }
304
}
305