Completed
Push — master ( 268357...00b275 )
by Matthieu
14s
created

Container::getDefinition()   A

Complexity

Conditions 2
Paths 2

Duplication

Lines 0
Ratio 0 %

Size

Total Lines 8
Code Lines 4

Importance

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