ServiceProviderCompilationPass::process()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 4
nop 1
dl 0
loc 11
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Bnf\SymfonyServiceProviderCompilerPass;
4
5
use Interop\Container\ServiceProviderInterface;
6
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
7
use Symfony\Component\DependencyInjection\ContainerBuilder;
8
use Symfony\Component\DependencyInjection\Definition;
9
use Symfony\Component\DependencyInjection\Reference;
10
11
class ServiceProviderCompilationPass implements CompilerPassInterface
12
{
13
    /**
14
     * @var Registry
15
     */
16
    private $registry;
17
18
    /**
19
     * @var string
20
     */
21
    private $registryServiceName;
22
23
    /**
24
     * @param Registry $registry
25
     * @param string $registryServiceName
26
     */
27
    public function __construct(Registry $registry, string $registryServiceName = 'service_provider_registry')
28
    {
29
        $this->registry = $registry;
30
        $this->registryServiceName = $registryServiceName;
31
    }
32
33
    /**
34
     * You can modify the container here before it is dumped to PHP code.
35
     *
36
     * @param ContainerBuilder $container
37
     */
38
    public function process(ContainerBuilder $container)
39
    {
40
        // Now, let's store the registry in the container (an empty version of it... it has to be dynamically added at runtime):
41
        $this->registerRegistry($container);
42
43
        foreach ($this->registry as $serviceProviderKey => $serviceProvider) {
44
            $this->registerFactories($serviceProviderKey, $serviceProvider, $container);
45
        }
46
47
        foreach ($this->registry as $serviceProviderKey => $serviceProvider) {
48
            $this->registerExtensions($serviceProviderKey, $serviceProvider, $container);
49
        }
50
    }
51
52
    private function registerRegistry(ContainerBuilder $container)
53
    {
54
        $definition = new Definition(Registry::class);
55
        $definition->setSynthetic(true);
56
        $definition->setPublic(true);
57
58
        $container->setDefinition($this->registryServiceName, $definition);
59
    }
60
61
    private function registerFactories($serviceProviderKey, ServiceProviderInterface $serviceProvider, ContainerBuilder $container)
62
    {
63
        $serviceFactories = $serviceProvider->getFactories();
64
65
        foreach ($serviceFactories as $serviceName => $callable) {
66
            $this->registerService($serviceName, $serviceProviderKey, $callable, $container);
67
        }
68
    }
69
70
    private function registerExtensions($serviceProviderKey, ServiceProviderInterface $serviceProvider, ContainerBuilder $container)
71
    {
72
        $serviceFactories = $serviceProvider->getExtensions();
73
74
        foreach ($serviceFactories as $serviceName => $callable) {
75
            $this->extendService($serviceName, $serviceProviderKey, $callable, $container);
76
        }
77
    }
78
79
    private function registerService($serviceName, $serviceProviderKey, $callable, ContainerBuilder $container)
80
    {
81
        if (!$container->has($serviceName)) {
82
            // Create a new definition
83
            $factoryDefinition = new Definition();
84
            $container->setDefinition($serviceName, $factoryDefinition);
85
        } else {
86
            // Merge into an existing definition to keep possible addMethodCall/properties configurations
87
            // (which act like a service extension)
88
            // Retrieve the existing factory and overwrite it.
89
            $factoryDefinition = $container->findDefinition($serviceName);
90
            if ($factoryDefinition->isAutowired()) {
91
                $factoryDefinition->setAutowired(false);
92
            }
93
        }
94
95
        $className = $this->getReturnType($this->getReflection($callable), $serviceName);
96
        $factoryDefinition->setClass($className);
97
        $factoryDefinition->setPublic(true);
98
99
        $staticallyCallable = $this->getStaticallyCallable($callable);
100
        if ($staticallyCallable !== null) {
101
            $factoryDefinition->setFactory($staticallyCallable);
102
            $factoryDefinition->setArguments([
103
                new Reference('service_container')
104
            ]);
105
        } else {
106
            $factoryDefinition->setFactory([ new Reference($this->registryServiceName), 'createService' ]);
107
            $factoryDefinition->setArguments([
108
                $serviceProviderKey,
109
                $serviceName,
110
                new Reference('service_container')
111
            ]);
112
        }
113
    }
114
115
    private function extendService($serviceName, $serviceProviderKey, $callable, ContainerBuilder $container)
116
    {
117
        $finalServiceName = $serviceName;
118
        $innerName = null;
119
120
        $reflection = $this->getReflection($callable);
121
        $className = $this->getReturnType($reflection, $serviceName);
122
123
        $factoryDefinition = new Definition($className);
124
        $factoryDefinition->setClass($className);
125
        $factoryDefinition->setPublic(true);
126
127
        if ($container->has($serviceName)) {
128
            list($finalServiceName, $previousServiceName) = $this->getDecoratedServiceName($serviceName, $container);
129
            $innerName = $finalServiceName . '.inner';
130
131
            $factoryDefinition->setDecoratedService($previousServiceName, $innerName);
132
        } elseif ($reflection->getNumberOfRequiredParameters() > 1) {
133
            throw new \Exception('A registered extension for the service "' . $serviceName . '" requires the service to be available, which is missing.');
134
        }
135
136
        $staticallyCallable = $this->getStaticallyCallable($callable);
137
        if ($staticallyCallable !== null) {
138
            $factoryDefinition->setFactory($staticallyCallable);
139
            $factoryDefinition->setArguments([
140
                new Reference('service_container')
141
            ]);
142
        } else {
143
            $factoryDefinition->setFactory([ new Reference($this->registryServiceName), 'extendService' ]);
144
            $factoryDefinition->setArguments([
145
                $serviceProviderKey,
146
                $serviceName,
147
                new Reference('service_container')
148
            ]);
149
        }
150
151
        if ($innerName !== null) {
152
            $factoryDefinition->addArgument(new Reference($innerName));
153
        }
154
155
        $container->setDefinition($finalServiceName, $factoryDefinition);
156
    }
157
158
    /**
159
     * @param callable $callable
160
     * @return array|string|null
161
     */
162
    private function getStaticallyCallable(callable $callable)
163
    {
164
        if (is_string($callable)) {
165
            return $callable;
166
        }
167
        if (is_array($callable) && isset($callable[0]) && is_string($callable[0])) {
168
            return $callable;
169
        }
170
171
        return null;
172
    }
173
174
    private function getReturnType(\ReflectionFunctionAbstract $reflection, string $serviceName): string
175
    {
176
        return $reflection->getReturnType() ?: $serviceName;
177
    }
178
179
    private function getReflection(callable $callable): \ReflectionFunctionAbstract
180
    {
181
        if (is_array($callable) && count($callable) === 2) {
182
            return new \ReflectionMethod($callable[0], $callable[1]);
183
        }
184
        if (is_object($callable) && !$callable instanceof \Closure) {
185
            return new \ReflectionMethod($callable, '__invoke');
186
        }
187
188
        return new \ReflectionFunction($callable);
189
    }
190
191
    private function getDecoratedServiceName($serviceName, ContainerBuilder $container)
192
    {
193
        $counter = 1;
194
        while ($container->has($serviceName . '_decorated_' . $counter)) {
195
            $counter++;
196
        }
197
        return [
198
            $serviceName . '_decorated_' . $counter,
199
            $counter === 1 ? $serviceName : $serviceName . '_decorated_' . ($counter-1)
200
        ];
201
    }
202
}
203