Passed
Branch master (30ae21)
by Gabor
03:15
created

SymfonyAdapter::get()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 15
c 0
b 0
f 0
ccs 10
cts 10
cp 1
rs 9.2
cc 4
eloc 8
nc 4
nop 1
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
     * Register 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
        // Init settings.
104
        $setUpData = [
105 9
            self::SERVICE_CLASS       => $serviceClass,
106 9
            self::SERVICE_ARGUMENTS   => [],
107 9
            self::SERVICE_METHOD_CALL => [],
108
            // By default the Symfony DI shares all services. In WebHemi by default nothing is shared.
109 9
            self::SERVICE_SHARE       => false,
110 9
        ];
111
112
        // Override settings from the configuration if exists.
113 9
        if (isset($this->configuration['Global'][$identifier])) {
114 9
            $setUpData = array_merge($setUpData, $this->configuration['Global'][$identifier]);
115 9
        } elseif (!empty($this->moduleNamespace) && isset($this->configuration[$this->moduleNamespace][$identifier])) {
116 1
            $setUpData = array_merge($setUpData, $this->configuration[$this->moduleNamespace][$identifier]);
117 1
        }
118
119
        // Create the definition.
120 9
        $definition = new Definition($serviceClass);
121
122 9
        $sharedService = (bool) $setUpData[self::SERVICE_SHARE];
123 9
        $definition->setShared($sharedService);
124
125
        // Register the service.
126 9
        $service = $this->container->setDefinition($identifier, $definition);
127 9
        if ($sharedService) {
128 8
            $this->instantiatedSharedServices[$service->getClass()] = false;
129 8
        }
130
131
        // Add arguments.
132 9
        foreach ((array) $setUpData[self::SERVICE_ARGUMENTS] as $parameter) {
133 7
            $this->setServiceArgument($service, $parameter);
134 9
        }
135
136
        // Register method callings.
137 9
        foreach ((array) $setUpData[self::SERVICE_METHOD_CALL] as $method => $parameterList) {
138 2
            $this->addMethodCall($service, $method, $parameterList);
139 9
        }
140 9
    }
141
142
    /**
143
     * Adds a method call for the service. It will be triggered as soon as the service had been initialized.
144
     *
145
     * @param Definition $service
146
     * @param string     $method
147
     * @param array      $parameterList
148
     */
149 2
    private function addMethodCall(Definition $service, $method, $parameterList = [])
150
    {
151
        // Check the parameter list for reference services
152 2
        foreach ($parameterList as &$parameter) {
153 2
            $parameter = $this->getReferenceServiceIfAvailable($parameter);
154 2
        }
155
156 2
        $service->addMethodCall($method, $parameterList);
157 2
    }
158
159
    /**
160
     * If possible create register the parameter as a service and give it back as a reference.
161
     *
162
     * @param mixed $classOrServiceName
163
     *
164
     * @return mixed|Reference
165
     */
166 8
    private function getReferenceServiceIfAvailable($classOrServiceName)
167
    {
168 8
        $reference = $classOrServiceName;
169
170
        // Check string parameter if it is a valid service or class name.
171 8
        if (!is_string($classOrServiceName)) {
172 7
            return $reference;
173
        }
174
175 8
        if (isset($this->servicesToDefine[$classOrServiceName])) {
176
            // The parameter is defined as a service but it is not yet registered; alias is given.
177 7
            $this->registerService($classOrServiceName, $this->servicesToDefine[$classOrServiceName]);
178 8
        } elseif (in_array($classOrServiceName, $this->servicesToDefine)) {
179
            // The parameter is defined as a service but it is not yet registered; real class is given.
180 1
            $referenceAlias = array_search($classOrServiceName, $this->servicesToDefine);
181 1
            $this->registerService($referenceAlias, $this->servicesToDefine[$referenceAlias]);
182 1
            $classOrServiceName = $referenceAlias;
183 8
        } elseif (class_exists($classOrServiceName)) {
184
            // The parameter is not a service, but it is a class that can be instantiated. e.g.: DateTime::class
185 6
            $this->container->register($classOrServiceName, $classOrServiceName);
186 6
        }
187
188 8
        if ($this->has($classOrServiceName)) {
189 7
            $reference = new Reference($classOrServiceName);
190 7
        }
191
192 8
        return $reference;
193
    }
194
195
    /**
196
     * Creates a safe normalized name.
197
     *
198
     * @param $className
199
     * @param $argumentName
200
     *
201
     * @return string
202
     */
203 8
    private function getNormalizedName($className, $argumentName)
204
    {
205 8
        $className = 'C_'.preg_replace('/[^a-z0-9]/', '', strtolower($className));
206 8
        $argumentName = 'A_'.preg_replace('/[^a-z0-9]/', '', strtolower($argumentName));
207
208 8
        return $className.'.'.$argumentName;
209
    }
210
211
    /**
212
     * Gets a service. It also tries to register the one without arguments which not yet registered.
213
     *
214
     * @param string $identifier
215
     *
216
     * @return object
217
     */
218 6
    public function get($identifier)
219
    {
220 6
        if (!$this->container->has($identifier) && class_exists($identifier)) {
221 1
            $this->registerService($identifier, $identifier);
222 1
        }
223
224 6
        $service = $this->container->get($identifier);
225 6
        $serviceClass = get_class($service);
226
227 6
        if (isset($this->instantiatedSharedServices[$serviceClass])) {
228 6
            $this->instantiatedSharedServices[$serviceClass] = true;
229 6
        }
230
231 6
        return $service;
232
    }
233
234
    /**
235
     * Returns true if the given service is defined.
236
     *
237
     * @param string $identifier
238
     *
239
     * @return bool
240
     */
241 9
    public function has($identifier)
242
    {
243 9
        return $this->container->has($identifier);
244
    }
245
246
    /**
247
     * Register module specific services.
248
     * If a service is already registered in the Global namespace, it will be skipped.
249
     *
250
     * @param string $moduleName
251
     * @return DependencyInjectionAdapterInterface
252
     */
253 6
    public function registerModuleServices($moduleName)
254
    {
255
        // TODO solve the case when the method is called multiple times with different argument -> unregister
256 6
        if ($moduleName != 'Global' && isset($this->configuration[$moduleName])) {
257 1
            $this->moduleNamespace = $moduleName;
258 1
            $this->registerServices($this->configuration[$moduleName]);
259 1
        }
260
261 6
        return $this;
262
    }
263
264
    /**
265
     * Sets service argument.
266
     *
267
     * @param string|Definition $service
268
     * @param mixed             $parameter
269
     *
270
     * @throws RuntimeException
271
     *
272
     * @return DependencyInjectionAdapterInterface
273
     */
274 8
    public function setServiceArgument($service, $parameter)
275
    {
276 8
        $service = $this->getRealService($service);
277 8
        $parameterName = $this->getRealParameterName($parameter);
278 8
        $serviceClass = $service->getClass();
279
280
        // Check if service is shared and is already initialized.
281 8
        $this->checkSharedServiceClassState($serviceClass);
282
283
        // Create a normalized name for the argument.
284 8
        $normalizedName = $this->getNormalizedName($serviceClass, $parameterName);
285
286
        // If the parameter marked as to be used as a scalar.
287 8
        if (is_scalar($parameter) && strpos((string)$parameter, '!:') === 0) {
288 5
            $parameter = substr((string)$parameter, 2);
289 5
        } else {
290
            // Otherwise check if the parameter is a service.
291 8
            $parameter = $this->getReferenceServiceIfAvailable($parameter);
292
        }
293
294 8
        $this->container->setParameter($normalizedName, $parameter);
295 8
        $service->addArgument('%'.$normalizedName.'%');
296
297 8
        return $this;
298
    }
299
300
    /**
301
     * Gets the real service instance.
302
     *
303
     * @param mixed $service
304
     * @return Definition
305
     */
306 8
    private function getRealService($service)
307
    {
308 8
        if (!$service instanceof Definition) {
309 5
            $service = $this->container->getDefinition($service);
310 5
        }
311
312 8
        return $service;
313
    }
314
315
    /**
316
     * Gets the real parameter name.
317
     *
318
     * @param $parameterName
319
     * @return mixed
320
     */
321 8
    private function getRealParameterName($parameterName)
322
    {
323 8
        if (!is_scalar($parameterName)) {
324 6
            $parameterName = self::$parameterIndex++;
325 6
        }
326
327 8
        return $parameterName;
328
    }
329
330
    /**
331
     * Checks whether the service is shared and initialized
332
     *
333
     * @param $serviceClass
334
     * @throws RuntimeException
335
     */
336 8
    private function checkSharedServiceClassState($serviceClass)
337
    {
338 8
        if (isset($this->instantiatedSharedServices[$serviceClass])
339 8
            && $this->instantiatedSharedServices[$serviceClass] === true
340 8
        ) {
341 1
            throw new RuntimeException('Cannot add argument to an already initialized service.');
342
        }
343 8
    }
344
}
345