Completed
Push — master ( 00dee1...af288c )
by Matthieu
14s
created

Container::get()   A

Complexity

Conditions 4
Paths 3

Duplication

Lines 0
Ratio 0 %

Size

Total Lines 18
Code Lines 9

Importance

Changes 0
Metric Value
cc 4
eloc 9
nc 3
nop 1
dl 0
loc 18
rs 9.2
c 0
b 0
f 0
1
<?php
2
3
namespace DI;
4
5
use DI\Definition\Definition;
6
use DI\Definition\FactoryDefinition;
7
use DI\Definition\Helper\DefinitionHelper;
8
use DI\Definition\InstanceDefinition;
9
use DI\Definition\ObjectDefinition;
10
use DI\Definition\Resolver\DefinitionResolver;
11
use DI\Definition\Resolver\ResolverDispatcher;
12
use DI\Definition\Source\CachedDefinitionSource;
13
use DI\Definition\Source\DefinitionArray;
14
use DI\Definition\Source\DefinitionSource;
15
use DI\Definition\Source\MutableDefinitionSource;
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
 * @author Matthieu Napoli <matthieu@mnapoli.fr>
34
 */
35
class Container implements ContainerInterface, FactoryInterface, \DI\InvokerInterface
36
{
37
    /**
38
     * Map of entries that are already resolved.
39
     * @var array
40
     */
41
    private $resolvedEntries = [];
42
43
    /**
44
     * @var DefinitionSource
45
     */
46
    private $definitionSource;
47
48
    /**
49
     * @var DefinitionResolver
50
     */
51
    private $definitionResolver;
52
53
    /**
54
     * Array of entries being resolved. Used to avoid circular dependencies and infinite loops.
55
     * @var array
56
     */
57
    private $entriesBeingResolved = [];
58
59
    /**
60
     * @var \Invoker\InvokerInterface|null
61
     */
62
    private $invoker;
63
64
    /**
65
     * Container that wraps this container. If none, points to $this.
66
     *
67
     * @var ContainerInterface
68
     */
69
    private $wrapperContainer;
70
71
    /**
72
     * Use `$container = new Container()` if you want a container with the default configuration.
73
     *
74
     * If you want to customize the container's behavior, you are discouraged to create and pass the
75
     * dependencies yourself, the ContainerBuilder class is here to help you instead.
76
     *
77
     * @see ContainerBuilder
78
     *
79
     * @param ContainerInterface $wrapperContainer If the container is wrapped by another container.
80
     */
81
    public function __construct(
82
        DefinitionSource $definitionSource = null,
83
        ProxyFactory $proxyFactory = null,
84
        ContainerInterface $wrapperContainer = null
85
    ) {
86
        $this->wrapperContainer = $wrapperContainer ?: $this;
87
88
        $this->definitionSource = $definitionSource ?: $this->createDefaultDefinitionSource();
89
        $proxyFactory = $proxyFactory ?: new ProxyFactory(false);
90
        $this->definitionResolver = new ResolverDispatcher($this->wrapperContainer, $proxyFactory);
91
92
        // Auto-register the container
93
        $this->resolvedEntries[self::class] = $this;
94
        $this->resolvedEntries[FactoryInterface::class] = $this;
95
        $this->resolvedEntries[InvokerInterface::class] = $this;
96
        $this->resolvedEntries[ContainerInterface::class] = $this;
97
    }
98
99
    /**
100
     * Returns an entry of the container by its name.
101
     *
102
     * @param string $name Entry name or a class name.
103
     *
104
     * @throws InvalidArgumentException The name parameter must be of type string.
105
     * @throws DependencyException Error while resolving the entry.
106
     * @throws NotFoundException No entry found for the given name.
107
     * @return mixed
108
     */
109
    public function get($name)
110
    {
111
        // If the entry is already resolved we return it
112
        if (isset($this->resolvedEntries[$name]) || array_key_exists($name, $this->resolvedEntries)) {
113
            return $this->resolvedEntries[$name];
114
        }
115
116
        $definition = $this->definitionSource->getDefinition($name);
117
        if (! $definition) {
118
            throw new NotFoundException("No entry or class found for '$name'");
119
        }
120
121
        $value = $this->resolveDefinition($definition);
122
123
        $this->resolvedEntries[$name] = $value;
124
125
        return $value;
126
    }
127
128
    /**
129
     * Build an entry of the container by its name.
130
     *
131
     * This method behave like get() except resolves the entry again every time.
132
     * For example if the entry is a class then a new instance will be created each time.
133
     *
134
     * This method makes the container behave like a factory.
135
     *
136
     * @param string $name       Entry name or a class name.
137
     * @param array  $parameters Optional parameters to use to build the entry. Use this to force specific parameters
138
     *                           to specific values. Parameters not defined in this array will be resolved using
139
     *                           the container.
140
     *
141
     * @throws InvalidArgumentException The name parameter must be of type string.
142
     * @throws DependencyException Error while resolving the entry.
143
     * @throws NotFoundException No entry found for the given name.
144
     * @return mixed
145
     */
146
    public function make($name, array $parameters = [])
147
    {
148 View Code Duplication
        if (! is_string($name)) {
149
            throw new InvalidArgumentException(sprintf(
150
                'The name parameter must be of type string, %s given',
151
                is_object($name) ? get_class($name) : gettype($name)
152
            ));
153
        }
154
155
        $definition = $this->definitionSource->getDefinition($name);
156
        if (! $definition) {
157
            // If the entry is already resolved we return it
158
            if (array_key_exists($name, $this->resolvedEntries)) {
159
                return $this->resolvedEntries[$name];
160
            }
161
162
            throw new NotFoundException("No entry or class found for '$name'");
163
        }
164
165
        return $this->resolveDefinition($definition, $parameters);
166
    }
167
168
    /**
169
     * Test if the container can provide something for the given name.
170
     *
171
     * @param string $name Entry name or a class name.
172
     *
173
     * @throws InvalidArgumentException The name parameter must be of type string.
174
     * @return bool
175
     */
176
    public function has($name)
177
    {
178 View Code Duplication
        if (! is_string($name)) {
179
            throw new InvalidArgumentException(sprintf(
180
                'The name parameter must be of type string, %s given',
181
                is_object($name) ? get_class($name) : gettype($name)
182
            ));
183
        }
184
185
        if (array_key_exists($name, $this->resolvedEntries)) {
186
            return true;
187
        }
188
189
        $definition = $this->definitionSource->getDefinition($name);
190
        if ($definition === null) {
191
            return false;
192
        }
193
194
        return $this->definitionResolver->isResolvable($definition);
195
    }
196
197
    /**
198
     * Inject all dependencies on an existing instance.
199
     *
200
     * @param object $instance Object to perform injection upon
201
     * @throws InvalidArgumentException
202
     * @throws DependencyException Error while injecting dependencies
203
     * @return object $instance Returns the same instance
204
     */
205
    public function injectOn($instance)
206
    {
207
        $objectDefinition = $this->definitionSource->getDefinition(get_class($instance));
208
        if (! $objectDefinition instanceof ObjectDefinition) {
209
            return $instance;
210
        }
211
212
        $definition = new InstanceDefinition($instance, $objectDefinition);
213
214
        $this->definitionResolver->resolve($definition);
215
216
        return $instance;
217
    }
218
219
    /**
220
     * Call the given function using the given parameters.
221
     *
222
     * Missing parameters will be resolved from the container.
223
     *
224
     * @param callable $callable   Function to call.
225
     * @param array    $parameters Parameters to use. Can be indexed by the parameter names
226
     *                             or not indexed (same order as the parameters).
227
     *                             The array can also contain DI definitions, e.g. DI\get().
228
     *
229
     * @return mixed Result of the function.
230
     */
231
    public function call($callable, array $parameters = [])
232
    {
233
        return $this->getInvoker()->call($callable, $parameters);
234
    }
235
236
    /**
237
     * Define an object or a value in the container.
238
     *
239
     * @param string $name Entry name
240
     * @param mixed|DefinitionHelper $value Value, use definition helpers to define objects
241
     */
242
    public function set(string $name, $value)
243
    {
244
        if ($value instanceof DefinitionHelper) {
245
            $value = $value->getDefinition($name);
246
        } elseif ($value instanceof \Closure) {
247
            $value = new FactoryDefinition($name, $value);
248
        }
249
250
        if ($value instanceof Definition) {
251
            $this->setDefinition($name, $value);
252
        } else {
253
            $this->resolvedEntries[$name] = $value;
254
        }
255
    }
256
257
    /**
258
     * Resolves a definition.
259
     *
260
     * Checks for circular dependencies while resolving the definition.
261
     *
262
     * @throws DependencyException Error while resolving the entry.
263
     * @return mixed
264
     */
265
    private function resolveDefinition(Definition $definition, array $parameters = [])
266
    {
267
        $entryName = $definition->getName();
268
269
        // Check if we are already getting this entry -> circular dependency
270
        if (isset($this->entriesBeingResolved[$entryName])) {
271
            throw new DependencyException("Circular dependency detected while trying to resolve entry '$entryName'");
272
        }
273
        $this->entriesBeingResolved[$entryName] = true;
274
275
        // Resolve the definition
276
        try {
277
            $value = $this->definitionResolver->resolve($definition, $parameters);
278
        } catch (Exception $exception) {
279
            unset($this->entriesBeingResolved[$entryName]);
280
            throw $exception;
281
        }
282
283
        unset($this->entriesBeingResolved[$entryName]);
284
285
        return $value;
286
    }
287
288
    private function setDefinition(string $name, Definition $definition)
289
    {
290
        if ($this->definitionSource instanceof CachedDefinitionSource) {
291
            throw new \LogicException('You cannot set a definition at runtime on a container that has a cache configured. Doing so would risk caching the definition for the next execution, where it might be different. You can either put your definitions in a file, remove the cache or ->set() a raw value directly (PHP object, string, int, ...) instead of a PHP-DI definition.');
292
        }
293
294
        if (! $this->definitionSource instanceof MutableDefinitionSource) {
295
            // This can happen if you instantiate the container yourself
296
            throw new \LogicException('The container has not been initialized correctly');
297
        }
298
299
        // Clear existing entry if it exists
300
        if (array_key_exists($name, $this->resolvedEntries)) {
301
            unset($this->resolvedEntries[$name]);
302
        }
303
304
        $this->definitionSource->addDefinition($definition);
305
    }
306
307
    private function getInvoker() : \Invoker\InvokerInterface
308
    {
309
        if (! $this->invoker) {
310
            $parameterResolver = new ResolverChain([
311
                new DefinitionParameterResolver($this->definitionResolver),
312
                new NumericArrayResolver,
313
                new AssociativeArrayResolver,
314
                new DefaultValueResolver,
315
                new TypeHintContainerResolver($this->wrapperContainer),
316
            ]);
317
318
            $this->invoker = new Invoker($parameterResolver, $this);
319
        }
320
321
        return $this->invoker;
322
    }
323
324
    private function createDefaultDefinitionSource() : DefinitionSource
325
    {
326
        $source = new SourceChain([new ReflectionBasedAutowiring]);
327
        $source->setMutableDefinitionSource(new DefinitionArray([], new ReflectionBasedAutowiring));
328
329
        return $source;
330
    }
331
}
332