Completed
Push — master ( 0f0da2...360bf1 )
by Matthieu
01:46 queued 01:43
created

Container   D

Coupling/Cohesion

Components 1
Dependencies 21

Complexity

Total Complexity 45

Size/Duplication

Total Lines 362
Duplicated Lines 3.31 %

Importance

Changes 0
Metric Value
dl 12
loc 362
rs 4.7028
c 0
b 0
f 0
wmc 45
lcom 1
cbo 21

14 Methods

Rating   Name   Duplication   Size   Complexity  
A getInvoker() 0 16 2
A createDefaultDefinitionSource() 0 7 1
A __construct() 0 19 4
A get() 0 18 4
A getDefinition() 0 9 2
B make() 6 21 5
B has() 6 20 5
A injectOn() 0 17 3
A call() 0 4 1
A set() 0 14 4
A debugEntry() 0 13 3
B getEntryType() 0 20 7
A resolveDefinition() 0 19 2
A setDefinition() 0 10 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Container often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Container, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace DI;
6
7
use DI\Definition\Definition;
8
use DI\Definition\Exception\InvalidDefinition;
9
use DI\Definition\FactoryDefinition;
10
use DI\Definition\Helper\DefinitionHelper;
11
use DI\Definition\InstanceDefinition;
12
use DI\Definition\ObjectDefinition;
13
use DI\Definition\Resolver\DefinitionResolver;
14
use DI\Definition\Resolver\ResolverDispatcher;
15
use DI\Definition\Source\DefinitionArray;
16
use DI\Definition\Source\MutableDefinitionSource;
17
use DI\Definition\Source\ReflectionBasedAutowiring;
18
use DI\Definition\Source\SourceChain;
19
use DI\Invoker\DefinitionParameterResolver;
20
use DI\Proxy\ProxyFactory;
21
use InvalidArgumentException;
22
use Invoker\Invoker;
23
use Invoker\InvokerInterface;
24
use Invoker\ParameterResolver\AssociativeArrayResolver;
25
use Invoker\ParameterResolver\Container\TypeHintContainerResolver;
26
use Invoker\ParameterResolver\DefaultValueResolver;
27
use Invoker\ParameterResolver\NumericArrayResolver;
28
use Invoker\ParameterResolver\ResolverChain;
29
use Psr\Container\ContainerInterface;
30
31
/**
32
 * Dependency Injection Container.
33
 *
34
 * @api
35
 *
36
 * @author Matthieu Napoli <[email protected]>
37
 */
38
class Container implements ContainerInterface, FactoryInterface, InvokerInterface
39
{
40
    /**
41
     * Map of entries that are already resolved.
42
     * @var array
43
     */
44
    protected $resolvedEntries = [];
45
46
    /**
47
     * @var MutableDefinitionSource
48
     */
49
    private $definitionSource;
50
51
    /**
52
     * @var DefinitionResolver
53
     */
54
    private $definitionResolver;
55
56
    /**
57
     * Map of definitions that are already fetched (local cache).
58
     *
59
     * @var array
60
     */
61
    private $fetchedDefinitions = [];
62
63
    /**
64
     * Array of entries being resolved. Used to avoid circular dependencies and infinite loops.
65
     * @var array
66
     */
67
    protected $entriesBeingResolved = [];
68
69
    /**
70
     * @var InvokerInterface|null
71
     */
72
    private $invoker;
73
74
    /**
75
     * Container that wraps this container. If none, points to $this.
76
     *
77
     * @var ContainerInterface
78
     */
79
    protected $delegateContainer;
80
81
    /**
82
     * @var ProxyFactory
83
     */
84
    protected $proxyFactory;
85
86
    /**
87
     * Use `$container = new Container()` if you want a container with the default configuration.
88
     *
89
     * If you want to customize the container's behavior, you are discouraged to create and pass the
90
     * dependencies yourself, the ContainerBuilder class is here to help you instead.
91
     *
92
     * @see ContainerBuilder
93
     *
94
     * @param ContainerInterface $wrapperContainer If the container is wrapped by another container.
95
     */
96
    public function __construct(
97
        MutableDefinitionSource $definitionSource = null,
98
        ProxyFactory $proxyFactory = null,
99
        ContainerInterface $wrapperContainer = null
100
    ) {
101
        $this->delegateContainer = $wrapperContainer ?: $this;
102
103
        $this->definitionSource = $definitionSource ?: $this->createDefaultDefinitionSource();
104
        $this->proxyFactory = $proxyFactory ?: new ProxyFactory(false);
105
        $this->definitionResolver = new ResolverDispatcher($this->delegateContainer, $this->proxyFactory);
106
107
        // Auto-register the container
108
        $this->resolvedEntries = [
109
            self::class => $this,
110
            FactoryInterface::class => $this,
111
            InvokerInterface::class => $this,
112
            ContainerInterface::class => $this,
113
        ];
114
    }
115
116
    /**
117
     * Returns an entry of the container by its name.
118
     *
119
     * @param string $name Entry name or a class name.
120
     *
121
     * @throws InvalidArgumentException The name parameter must be of type string.
122
     * @throws DependencyException Error while resolving the entry.
123
     * @throws NotFoundException No entry found for the given name.
124
     * @return mixed
125
     */
126
    public function get($name)
127
    {
128
        // If the entry is already resolved we return it
129
        if (isset($this->resolvedEntries[$name]) || array_key_exists($name, $this->resolvedEntries)) {
130
            return $this->resolvedEntries[$name];
131
        }
132
133
        $definition = $this->getDefinition($name);
134
        if (! $definition) {
135
            throw new NotFoundException("No entry or class found for '$name'");
136
        }
137
138
        $value = $this->resolveDefinition($definition);
139
140
        $this->resolvedEntries[$name] = $value;
141
142
        return $value;
143
    }
144
145
    private function getDefinition($name)
146
    {
147
        // Local cache that avoids fetching the same definition twice
148
        if (!array_key_exists($name, $this->fetchedDefinitions)) {
149
            $this->fetchedDefinitions[$name] = $this->definitionSource->getDefinition($name);
150
        }
151
152
        return $this->fetchedDefinitions[$name];
153
    }
154
155
    /**
156
     * Build an entry of the container by its name.
157
     *
158
     * This method behave like get() except resolves the entry again every time.
159
     * For example if the entry is a class then a new instance will be created each time.
160
     *
161
     * This method makes the container behave like a factory.
162
     *
163
     * @param string $name       Entry name or a class name.
164
     * @param array  $parameters Optional parameters to use to build the entry. Use this to force specific parameters
165
     *                           to specific values. Parameters not defined in this array will be resolved using
166
     *                           the container.
167
     *
168
     * @throws InvalidArgumentException The name parameter must be of type string.
169
     * @throws DependencyException Error while resolving the entry.
170
     * @throws NotFoundException No entry found for the given name.
171
     * @return mixed
172
     */
173
    public function make($name, array $parameters = [])
174
    {
175 View Code Duplication
        if (! is_string($name)) {
176
            throw new InvalidArgumentException(sprintf(
177
                'The name parameter must be of type string, %s given',
178
                is_object($name) ? get_class($name) : gettype($name)
179
            ));
180
        }
181
182
        $definition = $this->getDefinition($name);
183
        if (! $definition) {
184
            // If the entry is already resolved we return it
185
            if (array_key_exists($name, $this->resolvedEntries)) {
186
                return $this->resolvedEntries[$name];
187
            }
188
189
            throw new NotFoundException("No entry or class found for '$name'");
190
        }
191
192
        return $this->resolveDefinition($definition, $parameters);
193
    }
194
195
    /**
196
     * Test if the container can provide something for the given name.
197
     *
198
     * @param string $name Entry name or a class name.
199
     *
200
     * @throws InvalidArgumentException The name parameter must be of type string.
201
     * @return bool
202
     */
203
    public function has($name)
204
    {
205 View Code Duplication
        if (! is_string($name)) {
206
            throw new InvalidArgumentException(sprintf(
207
                'The name parameter must be of type string, %s given',
208
                is_object($name) ? get_class($name) : gettype($name)
209
            ));
210
        }
211
212
        if (array_key_exists($name, $this->resolvedEntries)) {
213
            return true;
214
        }
215
216
        $definition = $this->getDefinition($name);
217
        if ($definition === null) {
218
            return false;
219
        }
220
221
        return $this->definitionResolver->isResolvable($definition);
222
    }
223
224
    /**
225
     * Inject all dependencies on an existing instance.
226
     *
227
     * @param object $instance Object to perform injection upon
228
     * @throws InvalidArgumentException
229
     * @throws DependencyException Error while injecting dependencies
230
     * @return object $instance Returns the same instance
231
     */
232
    public function injectOn($instance)
233
    {
234
        if (!$instance) {
235
            return $instance;
236
        }
237
238
        $objectDefinition = $this->definitionSource->getDefinition(get_class($instance));
239
        if (! $objectDefinition instanceof ObjectDefinition) {
240
            return $instance;
241
        }
242
243
        $definition = new InstanceDefinition($instance, $objectDefinition);
244
245
        $this->definitionResolver->resolve($definition);
246
247
        return $instance;
248
    }
249
250
    /**
251
     * Call the given function using the given parameters.
252
     *
253
     * Missing parameters will be resolved from the container.
254
     *
255
     * @param callable $callable   Function to call.
256
     * @param array    $parameters Parameters to use. Can be indexed by the parameter names
257
     *                             or not indexed (same order as the parameters).
258
     *                             The array can also contain DI definitions, e.g. DI\get().
259
     *
260
     * @return mixed Result of the function.
261
     */
262
    public function call($callable, array $parameters = [])
263
    {
264
        return $this->getInvoker()->call($callable, $parameters);
265
    }
266
267
    /**
268
     * Define an object or a value in the container.
269
     *
270
     * @param string $name Entry name
271
     * @param mixed|DefinitionHelper $value Value, use definition helpers to define objects
272
     */
273
    public function set(string $name, $value)
274
    {
275
        if ($value instanceof DefinitionHelper) {
276
            $value = $value->getDefinition($name);
277
        } elseif ($value instanceof \Closure) {
278
            $value = new FactoryDefinition($name, $value);
279
        }
280
281
        if ($value instanceof Definition) {
282
            $this->setDefinition($name, $value);
283
        } else {
284
            $this->resolvedEntries[$name] = $value;
285
        }
286
    }
287
288
    /**
289
     * Get entry debug information.
290
     *
291
     * @param string $name Entry name
292
     *
293
     * @throws InvalidDefinition
294
     * @throws NotFoundException
295
     */
296
    public function debugEntry(string $name) : string
297
    {
298
        $definition = $this->definitionSource->getDefinition($name);
299
        if ($definition instanceof Definition) {
300
            return (string) $definition;
301
        }
302
303
        if (array_key_exists($name, $this->resolvedEntries)) {
304
            return $this->getEntryType($this->resolvedEntries[$name]);
305
        }
306
307
        throw new NotFoundException("No entry or class found for '$name'");
308
    }
309
310
    /**
311
     * Get formatted entry type.
312
     *
313
     * @param mixed $entry
314
     */
315
    private function getEntryType($entry) : string
316
    {
317
        if (is_object($entry)) {
318
            return sprintf("Object (\n    class = %s\n)", get_class($entry));
319
        }
320
321
        if (is_array($entry)) {
322
            return preg_replace(['/^array \(/', '/\)$/'], ['[', ']'], var_export($entry, true));
323
        }
324
325
        if (is_string($entry)) {
326
            return sprintf('Value (\'%s\')', $entry);
327
        }
328
329
        if (is_bool($entry)) {
330
            return sprintf('Value (%s)', $entry === true ? 'true' : 'false');
331
        }
332
333
        return sprintf('Value (%s)', is_scalar($entry) ? $entry : ucfirst(gettype($entry)));
334
    }
335
336
    /**
337
     * Resolves a definition.
338
     *
339
     * Checks for circular dependencies while resolving the definition.
340
     *
341
     * @throws DependencyException Error while resolving the entry.
342
     * @return mixed
343
     */
344
    private function resolveDefinition(Definition $definition, array $parameters = [])
345
    {
346
        $entryName = $definition->getName();
347
348
        // Check if we are already getting this entry -> circular dependency
349
        if (isset($this->entriesBeingResolved[$entryName])) {
350
            throw new DependencyException("Circular dependency detected while trying to resolve entry '$entryName'");
351
        }
352
        $this->entriesBeingResolved[$entryName] = true;
353
354
        // Resolve the definition
355
        try {
356
            $value = $this->definitionResolver->resolve($definition, $parameters);
357
        } finally {
358
            unset($this->entriesBeingResolved[$entryName]);
359
        }
360
361
        return $value;
362
    }
363
364
    protected function setDefinition(string $name, Definition $definition)
365
    {
366
        // Clear existing entry if it exists
367
        if (array_key_exists($name, $this->resolvedEntries)) {
368
            unset($this->resolvedEntries[$name]);
369
        }
370
        $this->fetchedDefinitions = []; // Completely clear this local cache
371
372
        $this->definitionSource->addDefinition($definition);
373
    }
374
375
    private function getInvoker() : InvokerInterface
376
    {
377
        if (! $this->invoker) {
378
            $parameterResolver = new ResolverChain([
379
                new DefinitionParameterResolver($this->definitionResolver),
380
                new NumericArrayResolver,
381
                new AssociativeArrayResolver,
382
                new DefaultValueResolver,
383
                new TypeHintContainerResolver($this->delegateContainer),
384
            ]);
385
386
            $this->invoker = new Invoker($parameterResolver, $this);
387
        }
388
389
        return $this->invoker;
390
    }
391
392
    private function createDefaultDefinitionSource() : SourceChain
393
    {
394
        $source = new SourceChain([new ReflectionBasedAutowiring]);
395
        $source->setMutableDefinitionSource(new DefinitionArray([], new ReflectionBasedAutowiring));
396
397
        return $source;
398
    }
399
}
400