Completed
Push — master ( 5540f7...ce140e )
by Matthieu
05:03 queued 03:17
created

Container   C

Coupling/Cohesion

Components 1
Dependencies 20

Complexity

Total Complexity 45

Size/Duplication

Total Lines 361
Duplicated Lines 3.32 %

Importance

Changes 0
Metric Value
wmc 45
lcom 1
cbo 20
dl 12
loc 361
rs 5.0166
c 0
b 0
f 0

14 Methods

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