Completed
Push — master ( 8d1b9d...b8cbf3 )
by Filipe
02:35
created

Container::make()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 21
ccs 4
cts 4
cp 1
rs 9.3142
c 0
b 0
f 0
cc 2
eloc 13
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
use Slick\Di\Inspector\ConstructorArgumentInspector;
20
21
/**
22
 * Container
23
 *
24
 * @package Slick\Di
25
 * @author  Filipe Silva <[email protected]>
26
 */
27
class Container implements ContainerInterface, ObjectHydratorAwareInterface
28
{
29
    /**
30
     * @var array
31
     */
32
    protected $definitions = [];
33
34
    /**
35
     * @var array
36
     */
37
    protected static $instances = [];
38
39
    /**
40
     * @var ObjectHydratorInterface
41
     */
42
    protected $hydrator;
43
44
    /**
45
     * @var null|ContainerInterface
46
     */
47
    protected $parent;
48
49
    /**
50
     * Creates a dependency container
51
     */
52
    public function __construct()
53
    {
54 74
        $this->parent = array_key_exists('container', self::$instances)
55
            ? self::$instances['container']
56 74
            : null;
57 74
58 74
        self::$instances['container'] = $this;
59 72
    }
60 72
61
    /**
62 74
     * Finds an entry of the container by its identifier and returns it.
63 74
     *
64 74
     * @param string $id Identifier of the entry to look for.
65 74
     *
66 74
     * @throws NotFoundException  No entry was found for this identifier.
67
     * @throws ContainerException Error while retrieving the entry.
68
     *
69
     * @return mixed Entry.
70
     */
71
    public function get($id)
72
    {
73
        if (!$this->has($id) && $id !== 'container') {
74
            throw new NotFoundException(
75
                "Dependency container has not found any definition for '{$id}'"
76
            );
77
        }
78 52
        return $this->resolve($id);
79
    }
80 52
81 2
    /**
82 2
     * Returns true if the container can return an entry for the given
83
     * identifier. Returns false otherwise.
84 2
     *
85
     * @param string $id Identifier of the entry to look for.
86
     *
87 50
     * @return boolean
88 50
     */
89 50
    public function has($id)
90
    {
91 50
        if (!array_key_exists($id, $this->definitions)) {
92 46
            return $this->parentHas($id);
93 46
        }
94
95 50
        return true;
96
    }
97
98
    /**
99
     * Adds a definition or a value to the container
100
     *
101
     * @param string       $name
102
     * @param mixed        $definition
103
     * @param Scope|string $scope      Resolving scope
104
     * @param array        $parameters Used if $value is a callable
105
     *
106 56
     * @return Container
107
     */
108 56
    public function register(
109 28
        $name,
110
        $definition = null,
111
        $scope = Scope::SINGLETON,
112 56
        array $parameters = []
113 18
    ) {
114
        if (!$definition instanceof DefinitionInterface) {
115 52
            $definition = $this->createDefinition(
116
                $definition,
117
                $parameters
118
            );
119
            $definition->setScope($scope);
120
        }
121
        return $this->add($name, $definition);
122
    }
123
124
    /**
125
     * Checks if parent has a provided key
126
     *
127
     * @param string $key
128
     *
129
     * @return bool
130
     */
131
    protected function parentHas($key)
132
    {
133 74
        if (!$this->parent) {
134
            return false;
135
        }
136
        return $this->parent->has($key);
137 74
    }
138 72
139 32
    /**
140 32
     * Resolves the definition that was saved under the provided name
141 32
     *
142 32
     * @param string $name
143 32
     *
144 32
     * @return mixed
145 32
     */
146
    protected function resolve($name)
147 72
    {
148 72
        if (array_key_exists($name, self::$instances)) {
149 72
            return self::$instances[$name];
150 72
        }
151
152 72
        if (array_key_exists($name, $this->definitions)) {
153 72
            $entry = $this->definitions[$name];
154
            return $this->registerEntry($name, $entry);
155 72
        }
156
157 72
        return $this->parent->get($name);
158 72
    }
159
160
    /**
161
     * Checks the definition scope to register resolution result
162
     *
163
     * If scope is set to prototype the the resolution result is not
164
     * stores in the container instances.
165
     *
166
     * @param string              $name
167
     * @param DefinitionInterface $definition
168
     * @return mixed
169
     */
170
    protected function registerEntry($name, DefinitionInterface $definition)
171
    {
172
        $value = $definition
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Slick\Di\ContainerAwareInterface as the method resolve() does only exist in the following implementations of said interface: Slick\Di\Definition\AbstractDefinition, Slick\Di\Definition\Alias, Slick\Di\Definition\Factory, Slick\Di\Definition\ObjectDefinition, Slick\Di\Definition\Object\Resolver, Slick\Di\Definition\Value.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
173
            ->setContainer($this->container())
174
            ->resolve();
175 10
        if ((string) $definition->getScope() !== Scope::PROTOTYPE) {
176
            self::$instances[$name] = $value;
177
        }
178 10
        return $value;
179 2
    }
180
181 2
    /**
182
     * Adds a definition to the definitions list
183
     *
184 8
     * This method does not override an existing entry if the same name exists
185 4
     * in the definitions or in any definitions of its parents.
186 4
     * This way it is possible to change entries defined by other packages
187
     * as those are build after the main application container is build.
188 8
     * The main application container should be the first to be created and
189
     * therefore set any entry that will override the latest containers build.
190
     *
191
     * @param string              $name
192
     * @param DefinitionInterface $definition
193
     *
194
     * @return Container
195
     */
196
    protected function add($name, DefinitionInterface $definition)
197
    {
198 46
        if ($this->has($name)) {
199
            return $this;
200 46
        }
201 46
202 46
        $this->definitions[$name] = $definition;
203 46
        $definition->setContainer($this->container());
204 46
        return $this;
205 46
    }
206 46
207
    /**
208
     * Creates the definition for registered data
209
     *
210
     * If value is a callable then the definition is Factory, otherwise
211
     * it will create a Value definition.
212
     *
213
     * @see Factory, Value
214
     *
215
     * @param callable|mixed $value
216 70
     * @param array          $parameters
217
     *
218 70
     * @return Factory|Value
219 70
     */
220
    protected function createDefinition(
221
        $value,
222
        array $parameters = []
223
    ) {
224
        if (is_callable($value)) {
225
            return new Factory($value, $parameters);
226
        }
227
        return $this->createValueDefinition($value);
228
    }
229
230 72
    /**
231
     * Creates a definition for provided name and value pair
232 72
     *
233
     * If $value is a string prefixed with '@' it will create an Alias
234 72
     * definition. Otherwise a Value definition will be created.
235
     *
236 72
     * @param mixed  $value
237 72
     *
238
     * @return Value|Alias
239
     */
240
    protected function createValueDefinition($value)
241
    {
242
        if (is_string($value) && strpos($value, '@') !== false) {
243
            return new Alias($value);
244
        }
245
246
        return new Value($value);
247
    }
248
249
    /**
250 32
     * Creates an instance of provided class injecting its dependencies
251
     *
252
     * @param string $className
253 32
     * @param array ...$arguments
254
     *
255 32
     * @return mixed
256 32
     */
257
    public function make($className, ...$arguments)
258
    {
259
        if (is_a($className, ContainerInjectionInterface::class, true)) {
260
            return call_user_func_array([$className, 'create'], [$this]);
261
        }
262
263
        $definition = (new ObjectDefinition($className))
264
            ->setContainer($this->container())
265 50
        ;
266
267 50
        $arguments = (new ConstructorArgumentInspector(
268 8
            new \ReflectionClass($className),
269
            $arguments
270 46
        ))
271
            ->arguments();
272
273
        call_user_func_array([$definition, 'with'], $arguments);
274
        $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...
275
        $this->getHydrator()->hydrate($object);
276
        return $object;
277
    }
278
279 46
    /**
280
     * Set the object hydrator
281 46
     *
282 46
     * @param ObjectHydratorInterface $hydrator
283
     *
284 46
     * @return Container|ObjectHydratorAwareInterface
285 14
     */
286 14
    public function setHydrator(ObjectHydratorInterface $hydrator)
287 46
    {
288
        $this->hydrator = $hydrator;
289
        return $this;
290
    }
291
292
    /**
293
     * Get the object hydrator
294
     *
295
     * @return ObjectHydratorInterface
296
     */
297
    public function getHydrator()
298
    {
299 4
        if (!$this->hydrator) {
300
            $this->setHydrator(new ObjectHydrator($this));
301 4
        }
302 4
        return $this->hydrator;
303 4
    }
304 2
305 2
    /**
306 4
     * Gets the parent container if it exists
307 4
     *
308 4
     * @return null|ContainerInterface
309
     */
310
    public function parent()
311
    {
312
        return $this->parent;
313
    }
314
315
    /**
316
     * Get the top container
317 18
     *
318
     * @return container
319 18
     */
320 10
    private function container()
321
    {
322 16
        return self::$instances['container'];
323
    }
324
}
325