Completed
Push — master ( b8cbf3...b0f990 )
by Filipe
05:29
created

Container::setHydrator()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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