Passed
Push — master ( a0c229...64ab61 )
by Gabor
03:42
created

SymfonyAdapter::getServiceSetupData()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 20
ccs 12
cts 12
cp 1
rs 9.2
cc 4
eloc 11
nc 3
nop 2
crap 4
1
<?php
2
/**
3
 * WebHemi.
4
 *
5
 * PHP version 5.6
6
 *
7
 * @copyright 2012 - 2016 Gixx-web (http://www.gixx-web.com)
8
 * @license   https://opensource.org/licenses/MIT The MIT License (MIT)
9
 *
10
 * @link      http://www.gixx-web.com
11
 */
12
namespace WebHemi\Adapter\DependencyInjection\Symfony;
13
14
use RuntimeException;
15
use Symfony\Component\DependencyInjection\ContainerBuilder;
16
use Symfony\Component\DependencyInjection\Definition;
17
use Symfony\Component\DependencyInjection\Reference;
18
use WebHemi\Adapter\DependencyInjection\DependencyInjectionAdapterInterface;
19
use WebHemi\Config\ConfigInterface;
20
21
/**
22
 * Class SymfonyAdapter.
23
 */
24
class SymfonyAdapter implements DependencyInjectionAdapterInterface
25
{
26
    /** @var ContainerBuilder */
27
    private $container;
28
    /** @var array */
29
    private $configuration;
30
    /** @var string */
31
    private $moduleNamespace;
32
    /** @var array */
33
    private $servicesToDefine = [];
34
    /** @var array */
35
    private $instantiatedSharedServices = [];
36
    /** @var int */
37
    private static $parameterIndex = 0;
38
39
    /**
40
     * DependencyInjectionAdapterInterface constructor.
41
     *
42
     * @param ConfigInterface $configuration
43
     */
44 10
    public function __construct(ConfigInterface $configuration)
45
    {
46 10
        $this->container = new ContainerBuilder();
47 10
        $this->configuration = $configuration->toArray();
48
49 10
        if (isset($this->configuration['Global'])) {
50 9
            $this->registerServices($this->configuration['Global']);
51 9
        }
52 10
    }
53
54
    /**
55
     * Initializes the DI container from the config.
56
     *
57
     * @param array  $dependencies
58
     */
59 9
    private function registerServices(array $dependencies)
60
    {
61
        // Collect the name information about the services to be registered
62 9
        foreach ($dependencies as $alias => $setupData) {
63 9
            $this->servicesToDefine[$alias] = $this->getRealServiceClass($setupData, $alias);
64 9
        }
65
66 9
        foreach ($this->servicesToDefine as $alias => $serviceClass) {
67 9
            $this->registerService($alias, $serviceClass);
68 9
        }
69 9
    }
70
71
    /**
72
     * Gets real service class name.
73
     *
74
     * @param array  $setupData
75
     * @param string $alias
76
     * @return mixed
77
     */
78 9
    private function getRealServiceClass(array $setupData, $alias)
79
    {
80 9
        if (isset($setupData[self::SERVICE_CLASS])) {
81 9
            $serviceClass = $setupData[self::SERVICE_CLASS];
82 9
        } else {
83 7
            $serviceClass = $alias;
84
        }
85
86 9
        return $serviceClass;
87
    }
88
89
    /**
90
     * Registers the service.
91
     *
92
     * @param string $identifier
93
     * @param string $serviceClass
94
     */
95 9
    public function registerService($identifier, $serviceClass)
96
    {
97
        // Do nothing if the service has been already registered with the same alias.
98
        // It is allowed to register the same service multiple times with different aliases.
99 9
        if ($this->has($identifier)) {
100 7
            return;
101
        }
102
103 9
        $setUpData = $this->getServiceSetupData($identifier, $serviceClass);
104
105
        // Create the definition.
106 9
        $definition = new Definition($serviceClass);
107
108 9
        $sharedService = (bool) $setUpData[self::SERVICE_SHARE];
109 9
        $definition->setShared($sharedService);
110
111
        // Register the service.
112 9
        $service = $this->container->setDefinition($identifier, $definition);
113 9
        if ($sharedService) {
114 8
            $this->instantiatedSharedServices[$service->getClass()] = false;
115 8
        }
116
117
        // Add arguments.
118 9
        foreach ((array) $setUpData[self::SERVICE_ARGUMENTS] as $parameter) {
119 7
            $this->setServiceArgument($service, $parameter);
120 9
        }
121
122
        // Register method callings.
123 9
        foreach ((array) $setUpData[self::SERVICE_METHOD_CALL] as $method => $parameterList) {
124 2
            $this->addMethodCall($service, $method, $parameterList);
125 9
        }
126 9
    }
127
128
    /**
129
     * Gets the set up data for the service registration.
130
     *
131
     * @param string $identifier
132
     * @param string $serviceClass
133
     * @return array
134
     */
135 9
    private function getServiceSetupData($identifier, $serviceClass)
136
    {
137
        // Init settings.
138
        $setUpData = [
139 9
            self::SERVICE_CLASS       => $serviceClass,
140 9
            self::SERVICE_ARGUMENTS   => [],
141 9
            self::SERVICE_METHOD_CALL => [],
142
            // By default the Symfony DI shares all services. In WebHemi by default nothing is shared.
143 9
            self::SERVICE_SHARE       => false,
144 9
        ];
145
146
        // Override settings from the configuration if exists.
147 9
        if (isset($this->configuration['Global'][$identifier])) {
148 9
            $setUpData = array_merge($setUpData, $this->configuration['Global'][$identifier]);
149 9
        } elseif (!empty($this->moduleNamespace) && isset($this->configuration[$this->moduleNamespace][$identifier])) {
150 1
            $setUpData = array_merge($setUpData, $this->configuration[$this->moduleNamespace][$identifier]);
151 1
        }
152
153 9
        return $setUpData;
154
    }
155
156
    /**
157
     * Adds a method call for the service. It will be triggered as soon as the service had been initialized.
158
     *
159
     * @param Definition $service
160
     * @param string     $method
161
     * @param array      $parameterList
162
     */
163 2
    private function addMethodCall(Definition $service, $method, $parameterList = [])
164
    {
165
        // Check the parameter list for reference services
166 2
        foreach ($parameterList as &$parameter) {
167 2
            $parameter = $this->getReferenceServiceIfAvailable($parameter);
168 2
        }
169
170 2
        $service->addMethodCall($method, $parameterList);
171 2
    }
172
173
    /**
174
     * If possible create register the parameter as a service and give it back as a reference.
175
     *
176
     * @param mixed $classOrServiceName
177
     *
178
     * @return mixed|Reference
179
     */
180 8
    private function getReferenceServiceIfAvailable($classOrServiceName)
181
    {
182 8
        $reference = $classOrServiceName;
183
184
        // Check string parameter if it is a valid service or class name.
185 8
        if (!is_string($classOrServiceName)) {
186 7
            return $reference;
187
        }
188
189 8
        if (isset($this->servicesToDefine[$classOrServiceName])) {
190
            // The parameter is defined as a service but it is not yet registered; alias is given.
191 7
            $this->registerService($classOrServiceName, $this->servicesToDefine[$classOrServiceName]);
192 8
        } elseif (in_array($classOrServiceName, $this->servicesToDefine)) {
193
            // The parameter is defined as a service but it is not yet registered; real class is given.
194 1
            $referenceAlias = array_search($classOrServiceName, $this->servicesToDefine);
195 1
            $this->registerService($referenceAlias, $this->servicesToDefine[$referenceAlias]);
196 1
            $classOrServiceName = $referenceAlias;
197 8
        } elseif (class_exists($classOrServiceName)) {
198
            // The parameter is not a service, but it is a class that can be instantiated. e.g.: DateTime::class
199 6
            $this->container->register($classOrServiceName, $classOrServiceName);
200 6
        }
201
202 8
        if ($this->has($classOrServiceName)) {
203 7
            $reference = new Reference($classOrServiceName);
204 7
        }
205
206 8
        return $reference;
207
    }
208
209
    /**
210
     * Creates a safe normalized name.
211
     *
212
     * @param $className
213
     * @param $argumentName
214
     *
215
     * @return string
216
     */
217 8
    private function getNormalizedName($className, $argumentName)
218
    {
219 8
        $className = 'C_'.preg_replace('/[^a-z0-9]/', '', strtolower($className));
220 8
        $argumentName = 'A_'.preg_replace('/[^a-z0-9]/', '', strtolower($argumentName));
221
222 8
        return $className.'.'.$argumentName;
223
    }
224
225
    /**
226
     * Gets a service. It also tries to register the one without arguments which not yet registered.
227
     *
228
     * @param string $identifier
229
     *
230
     * @return object
231
     */
232 6
    public function get($identifier)
233
    {
234 6
        if (!$this->container->has($identifier) && class_exists($identifier)) {
235 1
            $this->registerService($identifier, $identifier);
236 1
        }
237
238 6
        $service = $this->container->get($identifier);
239 6
        $serviceClass = get_class($service);
240
241 6
        if (isset($this->instantiatedSharedServices[$serviceClass])) {
242 6
            $this->instantiatedSharedServices[$serviceClass] = true;
243 6
        }
244
245 6
        return $service;
246
    }
247
248
    /**
249
     * Returns true if the given service is defined.
250
     *
251
     * @param string $identifier
252
     *
253
     * @return bool
254
     */
255 9
    public function has($identifier)
256
    {
257 9
        return $this->container->has($identifier);
258
    }
259
260
    /**
261
     * Register module specific services.
262
     * If a service is already registered in the Global namespace, it will be skipped.
263
     *
264
     * @param string $moduleName
265
     * @return DependencyInjectionAdapterInterface
266
     */
267 5
    public function registerModuleServices($moduleName)
268
    {
269
        // TODO solve the case when the method is called multiple times with different argument -> unregister
270 5
        if ($moduleName != 'Global' && isset($this->configuration[$moduleName])) {
271 1
            $this->moduleNamespace = $moduleName;
272 1
            $this->registerServices($this->configuration[$moduleName]);
273 1
        }
274
275 5
        return $this;
276
    }
277
278
    /**
279
     * Sets service argument.
280
     *
281
     * @param string|Definition $service
282
     * @param mixed             $parameter
283
     *
284
     * @throws RuntimeException
285
     *
286
     * @return DependencyInjectionAdapterInterface
287
     */
288 8
    public function setServiceArgument($service, $parameter)
289
    {
290 8
        $service = $this->getRealService($service);
291 8
        $parameterName = $this->getRealParameterName($parameter);
292 8
        $serviceClass = $service->getClass();
293
294
        // Check if service is shared and is already initialized.
295 8
        $this->checkSharedServiceClassState($serviceClass);
296
297
        // Create a normalized name for the argument.
298 8
        $normalizedName = $this->getNormalizedName($serviceClass, $parameterName);
299
300
        // If the parameter marked as to be used as a scalar.
301 8
        if (is_scalar($parameter) && strpos((string)$parameter, '!:') === 0) {
302 5
            $parameter = substr((string)$parameter, 2);
303 5
        } else {
304
            // Otherwise check if the parameter is a service.
305 8
            $parameter = $this->getReferenceServiceIfAvailable($parameter);
306
        }
307
308 8
        $this->container->setParameter($normalizedName, $parameter);
309 8
        $service->addArgument('%'.$normalizedName.'%');
310
311 8
        return $this;
312
    }
313
314
    /**
315
     * Gets the real service instance.
316
     *
317
     * @param mixed $service
318
     * @return Definition
319
     */
320 8
    private function getRealService($service)
321
    {
322 8
        if (!$service instanceof Definition) {
323 5
            $service = $this->container->getDefinition($service);
324 5
        }
325
326 8
        return $service;
327
    }
328
329
    /**
330
     * Gets the real parameter name.
331
     *
332
     * @param $parameterName
333
     * @return mixed
334
     */
335 8
    private function getRealParameterName($parameterName)
336
    {
337 8
        if (!is_scalar($parameterName)) {
338 6
            $parameterName = self::$parameterIndex++;
339 6
        }
340
341 8
        return $parameterName;
342
    }
343
344
    /**
345
     * Checks whether the service is shared and initialized
346
     *
347
     * @param $serviceClass
348
     * @throws RuntimeException
349
     */
350 8
    private function checkSharedServiceClassState($serviceClass)
351
    {
352 8
        if (isset($this->instantiatedSharedServices[$serviceClass])
353 8
            && $this->instantiatedSharedServices[$serviceClass] === true
354 8
        ) {
355 1
            throw new RuntimeException('Cannot add argument to an already initialized service.');
356
        }
357 8
    }
358
}
359