Passed
Push — master ( 1678ad...6ac284 )
by Gabor
03:37
created

ServiceAdapter::getServiceSetupData()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 22
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5.025

Importance

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