Passed
Push — master ( 749b16...167cfa )
by Gabor
04:47
created

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