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