Passed
Push — master ( 2800c5...3c7c2a )
by Gabor
05:01
created

SymfonyAdapter::getServiceSetupData()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4.432

Importance

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