Passed
Push — master ( 0c08a8...5ff919 )
by Gabor
03:13
created

ServiceAdapter::checkSharedServiceClassState()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

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