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\Compiler;
13
14use Symfony\Component\DependencyInjection\Alias;
15use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
16use Symfony\Component\DependencyInjection\ContainerBuilder;
17use Symfony\Component\DependencyInjection\Definition;
18use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
19use Symfony\Component\DependencyInjection\Reference;
20use Symfony\Component\DependencyInjection\ServiceLocator;
21
22/**
23 * Applies the "container.service_locator" tag by wrapping references into ServiceClosureArgument instances.
24 *
25 * @author Nicolas Grekas <p@tchwork.com>
26 */
27final class ServiceLocatorTagPass extends AbstractRecursivePass
28{
29    protected function processValue($value, $isRoot = false)
30    {
31        if (!$value instanceof Definition || !$value->hasTag('container.service_locator')) {
32            return parent::processValue($value, $isRoot);
33        }
34
35        if (!$value->getClass()) {
36            $value->setClass(ServiceLocator::class);
37        }
38
39        $arguments = $value->getArguments();
40        if (!isset($arguments[0]) || !\is_array($arguments[0])) {
41            throw new InvalidArgumentException(sprintf('Invalid definition for service "%s": an array of references is expected as first argument when the "container.service_locator" tag is set.', $this->currentId));
42        }
43
44        $i = 0;
45
46        foreach ($arguments[0] as $k => $v) {
47            if ($v instanceof ServiceClosureArgument) {
48                continue;
49            }
50            if (!$v instanceof Reference) {
51                throw new InvalidArgumentException(sprintf('Invalid definition for service "%s": an array of references is expected as first argument when the "container.service_locator" tag is set, "%s" found for key "%s".', $this->currentId, \is_object($v) ? \get_class($v) : \gettype($v), $k));
52            }
53
54            if ($i === $k) {
55                unset($arguments[0][$k]);
56
57                $k = (string) $v;
58                ++$i;
59            } elseif (\is_int($k)) {
60                $i = null;
61            }
62            $arguments[0][$k] = new ServiceClosureArgument($v);
63        }
64        ksort($arguments[0]);
65
66        $value->setArguments($arguments);
67
68        $id = 'service_locator.'.ContainerBuilder::hash($value);
69
70        if ($isRoot) {
71            if ($id !== $this->currentId) {
72                $this->container->setAlias($id, new Alias($this->currentId, false));
73            }
74
75            return $value;
76        }
77
78        $this->container->setDefinition($id, $value->setPublic(false));
79
80        return new Reference($id);
81    }
82
83    /**
84     * @param Reference[] $refMap
85     * @param string|null $callerId
86     *
87     * @return Reference
88     */
89    public static function register(ContainerBuilder $container, array $refMap, $callerId = null)
90    {
91        foreach ($refMap as $id => $ref) {
92            if (!$ref instanceof Reference) {
93                throw new InvalidArgumentException(sprintf('Invalid service locator definition: only services can be referenced, "%s" found for key "%s". Inject parameter values using constructors instead.', \is_object($ref) ? \get_class($ref) : \gettype($ref), $id));
94            }
95            $refMap[$id] = new ServiceClosureArgument($ref);
96        }
97        ksort($refMap);
98
99        $locator = (new Definition(ServiceLocator::class))
100            ->addArgument($refMap)
101            ->setPublic(false)
102            ->addTag('container.service_locator');
103
104        if (null !== $callerId && $container->hasDefinition($callerId)) {
105            $locator->setBindings($container->getDefinition($callerId)->getBindings());
106        }
107
108        if (!$container->hasDefinition($id = 'service_locator.'.ContainerBuilder::hash($locator))) {
109            $container->setDefinition($id, $locator);
110        }
111
112        if (null !== $callerId) {
113            $locatorId = $id;
114            // Locators are shared when they hold the exact same list of factories;
115            // to have them specialized per consumer service, we use a cloning factory
116            // to derivate customized instances from the prototype one.
117            $container->register($id .= '.'.$callerId, ServiceLocator::class)
118                ->setPublic(false)
119                ->setFactory([new Reference($locatorId), 'withContext'])
120                ->addArgument($callerId)
121                ->addArgument(new Reference('service_container'));
122        }
123
124        return new Reference($id);
125    }
126}
127