Completed
Push — master ( 1621ce...fbf89a )
by Matthieu
22s
created

Container   D

Coupling/Cohesion

Components 1
Dependencies 22

Complexity

Total Complexity 46

Size/Duplication

Total Lines 366
Duplicated Lines 4.1 %

Importance

Changes 0
Metric Value
dl 15
loc 366
rs 4.2646
c 0
b 0
f 0
wmc 46
lcom 1
cbo 22

14 Methods

Rating   Name   Duplication   Size   Complexity  
A getKnownEntryNames() 0 10 1
A __construct() 0 19 4
A get() 3 18 4
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 22 3
A setDefinition() 0 14 3
A getInvoker() 0 16 2
A createDefaultDefinitionSource() 0 7 1

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