Completed
Push — master ( 6b5549...473d89 )
by Matthieu
02:27
created

Container::debugEntry()   A

Complexity

Conditions 4
Paths 4

Duplication

Lines 0
Ratio 0 %

Size

Total Lines 15
Code Lines 8

Importance

Changes 0
Metric Value
cc 4
eloc 8
nc 4
nop 1
dl 0
loc 15
rs 9.2
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\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[self::class] = $this;
103
        $this->resolvedEntries[FactoryInterface::class] = $this;
104
        $this->resolvedEntries[InvokerInterface::class] = $this;
105
        $this->resolvedEntries[ContainerInterface::class] = $this;
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 View Code Duplication
        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
            $entry = $this->resolvedEntries[$name];
301
302
            return is_object($entry) ? get_class($entry) : gettype($entry);
303
        }
304
305
        throw new NotFoundException("No entry or class found for '$name'");
306
    }
307
308
    /**
309
     * Resolves a definition.
310
     *
311
     * Checks for circular dependencies while resolving the definition.
312
     *
313
     * @throws DependencyException Error while resolving the entry.
314
     * @return mixed
315
     */
316
    private function resolveDefinition(Definition $definition, array $parameters = [])
317
    {
318
        $entryName = $definition->getName();
319
320
        // Check if we are already getting this entry -> circular dependency
321
        if (isset($this->entriesBeingResolved[$entryName])) {
322
            throw new DependencyException("Circular dependency detected while trying to resolve entry '$entryName'");
323
        }
324
        $this->entriesBeingResolved[$entryName] = true;
325
326
        // Resolve the definition
327
        try {
328
            $value = $this->definitionResolver->resolve($definition, $parameters);
329
        } catch (Exception $exception) {
330
            unset($this->entriesBeingResolved[$entryName]);
331
            throw $exception;
332
        }
333
334
        unset($this->entriesBeingResolved[$entryName]);
335
336
        return $value;
337
    }
338
339
    protected function setDefinition(string $name, Definition $definition)
340
    {
341
        if (! $this->definitionSource instanceof MutableDefinitionSource) {
342
            // This can happen if you instantiate the container yourself
343
            throw new \LogicException('The container has not been initialized correctly');
344
        }
345
346
        // Clear existing entry if it exists
347
        if (array_key_exists($name, $this->resolvedEntries)) {
348
            unset($this->resolvedEntries[$name]);
349
        }
350
351
        $this->definitionSource->addDefinition($definition);
352
    }
353
354
    private function getInvoker() : \Invoker\InvokerInterface
355
    {
356
        if (! $this->invoker) {
357
            $parameterResolver = new ResolverChain([
358
                new DefinitionParameterResolver($this->definitionResolver),
359
                new NumericArrayResolver,
360
                new AssociativeArrayResolver,
361
                new DefaultValueResolver,
362
                new TypeHintContainerResolver($this->delegateContainer),
363
            ]);
364
365
            $this->invoker = new Invoker($parameterResolver, $this);
366
        }
367
368
        return $this->invoker;
369
    }
370
371
    private function createDefaultDefinitionSource() : DefinitionSource
372
    {
373
        $source = new SourceChain([new ReflectionBasedAutowiring]);
374
        $source->setMutableDefinitionSource(new DefinitionArray([], new ReflectionBasedAutowiring));
375
376
        return $source;
377
    }
378
}
379