1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\DependencyInjection;
13
14use Psr\Container\ContainerInterface as PsrContainerInterface;
15use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException;
16use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
17
18/**
19 * @author Robin Chalas <robin.chalas@gmail.com>
20 * @author Nicolas Grekas <p@tchwork.com>
21 */
22class ServiceLocator implements PsrContainerInterface
23{
24    private $factories;
25    private $loading = [];
26    private $externalId;
27    private $container;
28
29    /**
30     * @param callable[] $factories
31     */
32    public function __construct(array $factories)
33    {
34        $this->factories = $factories;
35    }
36
37    /**
38     * {@inheritdoc}
39     */
40    public function has($id)
41    {
42        return isset($this->factories[$id]);
43    }
44
45    /**
46     * {@inheritdoc}
47     */
48    public function get($id)
49    {
50        if (!isset($this->factories[$id])) {
51            throw new ServiceNotFoundException($id, end($this->loading) ?: null, null, [], $this->createServiceNotFoundMessage($id));
52        }
53
54        if (isset($this->loading[$id])) {
55            $ids = array_values($this->loading);
56            $ids = \array_slice($this->loading, array_search($id, $ids));
57            $ids[] = $id;
58
59            throw new ServiceCircularReferenceException($id, $ids);
60        }
61
62        $this->loading[$id] = $id;
63        try {
64            return $this->factories[$id]();
65        } finally {
66            unset($this->loading[$id]);
67        }
68    }
69
70    public function __invoke($id)
71    {
72        return isset($this->factories[$id]) ? $this->get($id) : null;
73    }
74
75    /**
76     * @internal
77     */
78    public function withContext($externalId, Container $container)
79    {
80        $locator = clone $this;
81        $locator->externalId = $externalId;
82        $locator->container = $container;
83
84        return $locator;
85    }
86
87    private function createServiceNotFoundMessage($id)
88    {
89        if ($this->loading) {
90            return sprintf('The service "%s" has a dependency on a non-existent service "%s". This locator %s', end($this->loading), $id, $this->formatAlternatives());
91        }
92
93        $class = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS, 3);
94        $class = isset($class[2]['object']) ? \get_class($class[2]['object']) : null;
95        $externalId = $this->externalId ?: $class;
96
97        $msg = [];
98        $msg[] = sprintf('Service "%s" not found:', $id);
99
100        if (!$this->container) {
101            $class = null;
102        } elseif ($this->container->has($id) || isset($this->container->getRemovedIds()[$id])) {
103            $msg[] = 'even though it exists in the app\'s container,';
104        } else {
105            try {
106                $this->container->get($id);
107                $class = null;
108            } catch (ServiceNotFoundException $e) {
109                if ($e->getAlternatives()) {
110                    $msg[] = sprintf('did you mean %s? Anyway,', $this->formatAlternatives($e->getAlternatives(), 'or'));
111                } else {
112                    $class = null;
113                }
114            }
115        }
116        if ($externalId) {
117            $msg[] = sprintf('the container inside "%s" is a smaller service locator that %s', $externalId, $this->formatAlternatives());
118        } else {
119            $msg[] = sprintf('the current service locator %s', $this->formatAlternatives());
120        }
121
122        if (!$class) {
123            // no-op
124        } elseif (is_subclass_of($class, ServiceSubscriberInterface::class)) {
125            $msg[] = sprintf('Unless you need extra laziness, try using dependency injection instead. Otherwise, you need to declare it using "%s::getSubscribedServices()".', preg_replace('/([^\\\\]++\\\\)++/', '', $class));
126        } else {
127            $msg[] = 'Try using dependency injection instead.';
128        }
129
130        return implode(' ', $msg);
131    }
132
133    private function formatAlternatives(array $alternatives = null, $separator = 'and')
134    {
135        $format = '"%s"%s';
136        if (null === $alternatives) {
137            if (!$alternatives = array_keys($this->factories)) {
138                return 'is empty...';
139            }
140            $format = sprintf('only knows about the %s service%s.', $format, 1 < \count($alternatives) ? 's' : '');
141        }
142        $last = array_pop($alternatives);
143
144        return sprintf($format, $alternatives ? implode('", "', $alternatives) : $last, $alternatives ? sprintf(' %s "%s"', $separator, $last) : '');
145    }
146}
147