Passed
Push — master ( d26101...1eb4af )
by Gabor
03:11
created

SymfonyAdapter   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 282
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 35
c 1
b 0
f 0
lcom 1
cbo 4
dl 0
loc 282
ccs 111
cts 111
cp 1
rs 9

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
A initContainer() 0 17 4
B registerService() 0 43 6
A addMethodCall() 0 9 2
B getReferenceServiceIfAvailable() 0 28 6
A getNormalizedName() 0 7 1
A get() 0 15 4
A has() 0 4 1
B setServiceArgument() 0 25 3
A getRealService() 0 8 2
A getRealParameterName() 0 8 2
A checkSharedServiceClassState() 0 8 3
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 array */
31
    private $servicesToDefine = [];
32
    /** @var array */
33
    private $instantiatedSharedServices = [];
34
    /** @var int */
35
    private static $parameterIndex = 0;
36
37
    /**
38
     * DependencyInjectionAdapterInterface constructor.
39
     *
40
     * @param ConfigInterface $configuration
41
     */
42 10
    public function __construct(ConfigInterface $configuration)
43
    {
44 10
        $this->container = new ContainerBuilder();
45 10
        $this->configuration = $configuration->toArray();
46
47 10
        $this->initContainer();
48 10
    }
49
50
    /**
51
     * Initializes the DI container from the config.
52
     */
53 10
    private function initContainer()
54
    {
55
        // Collect the name information about the services to be registered
56 10
        foreach ($this->configuration as $alias => $setupData) {
57 9
            if (isset($setupData[self::SERVICE_CLASS])) {
58 9
                $serviceClass = $setupData[self::SERVICE_CLASS];
59 9
            } else {
60 7
                $serviceClass = $alias;
61
            }
62
63 9
            $this->servicesToDefine[$alias] = $serviceClass;
64 10
        }
65
66 10
        foreach ($this->servicesToDefine as $alias => $serviceClass) {
67 9
            $this->registerService($alias, $serviceClass);
68 10
        }
69 10
    }
70
71
    /**
72
     * Register the service.
73
     *
74
     * @param string $identifier
75
     * @param string $serviceClass
76
     */
77 9
    public function registerService($identifier, $serviceClass)
78 1
    {
79
        // Do nothing if the service has been already registered with the same alias.
80
        // It is allowed to register the same service multiple times with different aliases.
81 9
        if ($this->has($identifier)) {
82 7
            return;
83
        }
84
85
        // Init settings.
86
        $setUpData = [
87 9
            self::SERVICE_CLASS       => $serviceClass,
88 9
            self::SERVICE_ARGUMENTS   => [],
89 9
            self::SERVICE_METHOD_CALL => [],
90
            // By default the Symfony DI shares all services. In WebHemi by default nothing is shared.
91 9
            self::SERVICE_SHARE       => false,
92 9
        ];
93
        // Override settings from the configuration if exists.
94 9
        if (isset($this->configuration[$identifier])) {
95 9
            $setUpData = array_merge($setUpData, $this->configuration[$identifier]);
96 9
        }
97
98
        // Create the definition.
99 9
        $definition = new Definition($serviceClass);
100
101 9
        $sharedService = (bool) $setUpData[self::SERVICE_SHARE];
102 9
        $definition->setShared($sharedService);
103
104
        // Register the service.
105 9
        $service = $this->container->setDefinition($identifier, $definition);
106 9
        if ($sharedService) {
107 8
            $this->instantiatedSharedServices[$service->getClass()] = false;
108 8
        }
109
110
        // Add arguments.
111 9
        foreach ((array) $setUpData[self::SERVICE_ARGUMENTS] as $parameter) {
112 7
            $this->setServiceArgument($service, $parameter);
113 9
        }
114
115
        // Register method callings.
116 9
        foreach ((array) $setUpData[self::SERVICE_METHOD_CALL] as $method => $parameterList) {
117 2
            $this->addMethodCall($service, $method, $parameterList);
118 9
        }
119 9
    }
120
121
    /**
122
     * Adds a method call for the service. It will be triggered as soon as the service had been initialized.
123
     *
124
     * @param Definition $service
125
     * @param string     $method
126
     * @param array      $parameterList
127
     */
128 2
    private function addMethodCall(Definition $service, $method, $parameterList = [])
129
    {
130
        // Check the parameter list for reference services
131 2
        foreach ($parameterList as &$parameter) {
132 2
            $parameter = $this->getReferenceServiceIfAvailable($parameter);
133 2
        }
134
135 2
        $service->addMethodCall($method, $parameterList);
136 2
    }
137
138
    /**
139
     * If possible create register the parameter as a service and give it back as a reference.
140
     *
141
     * @param mixed $classOrServiceName
142
     *
143
     * @return mixed|Reference
144
     */
145 8
    private function getReferenceServiceIfAvailable($classOrServiceName)
146
    {
147 8
        $reference = $classOrServiceName;
148
149
        // Check string parameter if it is a valid service or class name.
150 8
        if (!is_string($classOrServiceName)) {
151 7
            return $reference;
152
        }
153
154 8
        if (isset($this->servicesToDefine[$classOrServiceName])) {
155
            // The parameter is defined as a service but it is not yet registered; alias is given.
156 7
            $this->registerService($classOrServiceName, $this->servicesToDefine[$classOrServiceName]);
157 8
        } elseif (in_array($classOrServiceName, $this->servicesToDefine)) {
158
            // The parameter is defined as a service but it is not yet registered; real class is given.
159 1
            $referenceAlias = array_search($classOrServiceName, $this->servicesToDefine);
160 1
            $this->registerService($referenceAlias, $this->servicesToDefine[$referenceAlias]);
161 1
            $classOrServiceName = $referenceAlias;
162 8
        } elseif (class_exists($classOrServiceName)) {
163
            // The parameter is not a service, but it is a class that can be instantiated. e.g.: DateTime::class
164 6
            $this->container->register($classOrServiceName, $classOrServiceName);
165 6
        }
166
167 8
        if ($this->has($classOrServiceName)) {
168 7
            $reference = new Reference($classOrServiceName);
169 7
        }
170
171 8
        return $reference;
172
    }
173
174
    /**
175
     * Creates a safe normalized name.
176
     *
177
     * @param $className
178
     * @param $argumentName
179
     *
180
     * @return string
181
     */
182 8
    private function getNormalizedName($className, $argumentName)
183
    {
184 8
        $className = 'C_'.preg_replace('/[^a-z0-9]/', '', strtolower($className));
185 8
        $argumentName = 'A_'.preg_replace('/[^a-z0-9]/', '', strtolower($argumentName));
186
187 8
        return $className.'.'.$argumentName;
188
    }
189
190
    /**
191
     * Gets a service. It also tries to register the one without arguments which not yet registered.
192
     *
193
     * @param string $identifier
194
     *
195
     * @return object
196
     */
197 6
    public function get($identifier)
198
    {
199 6
        if (!$this->container->has($identifier) && class_exists($identifier)) {
200 1
            $this->registerService($identifier, $identifier);
201 1
        }
202
203 6
        $service = $this->container->get($identifier);
204 6
        $serviceClass = get_class($service);
205
206 6
        if (isset($this->instantiatedSharedServices[$serviceClass])) {
207 6
            $this->instantiatedSharedServices[$serviceClass] = true;
208 6
        }
209
210 6
        return $service;
211
    }
212
213
    /**
214
     * Returns true if the given service is defined.
215
     *
216
     * @param string $identifier
217
     *
218
     * @return bool
219
     */
220 9
    public function has($identifier)
221
    {
222 9
        return $this->container->has($identifier);
223
    }
224
225
    /**
226
     * Sets service argument.
227
     *
228
     * @param string|Definition $service
229
     * @param mixed             $parameter
230
     *
231
     * @throws RuntimeException
232
     *
233
     * @return DependencyInjectionAdapterInterface
234
     */
235 8
    public function setServiceArgument($service, $parameter)
236
    {
237 8
        $service = $this->getRealService($service);
238 8
        $parameterName = $this->getRealParameterName($parameter);
239 8
        $serviceClass = $service->getClass();
240
241
        // Check if service is shared and is already initialized.
242 8
        $this->checkSharedServiceClassState($serviceClass);
243
244
        // Create a normalized name for the argument.
245 8
        $normalizedName = $this->getNormalizedName($serviceClass, $parameterName);
246
247
        // If the parameter marked as to be used as a scalar.
248 8
        if (is_scalar($parameter) && strpos((string)$parameter, '!:') === 0) {
249 5
            $parameter = substr((string)$parameter, 2);
250 5
        } else {
251
            // Otherwise check if the parameter is a service.
252 8
            $parameter = $this->getReferenceServiceIfAvailable($parameter);
253
        }
254
255 8
        $this->container->setParameter($normalizedName, $parameter);
256 8
        $service->addArgument('%'.$normalizedName.'%');
257
258 8
        return $this;
259
    }
260
261
    /**
262
     * Gets the real service instance.
263
     *
264
     * @param mixed $service
265
     * @return Definition
266
     */
267 8
    private function getRealService($service)
268
    {
269 8
        if (!$service instanceof Definition) {
270 5
            $service = $this->container->getDefinition($service);
271 5
        }
272
273 8
        return $service;
274
    }
275
276
    /**
277
     * Gets the real parameter name.
278
     *
279
     * @param $parameterName
280
     * @return mixed
281
     */
282 8
    private function getRealParameterName($parameterName)
283
    {
284 8
        if (!is_scalar($parameterName)) {
285 6
            $parameterName = self::$parameterIndex++;
286 6
        }
287
288 8
        return $parameterName;
289
    }
290
291
    /**
292
     * Checks whether the service is shared and initialized
293
     *
294
     * @param $serviceClass
295
     * @throws RuntimeException
296
     */
297 8
    private function checkSharedServiceClassState($serviceClass)
298
    {
299 8
        if (isset($this->instantiatedSharedServices[$serviceClass])
300 8
            && $this->instantiatedSharedServices[$serviceClass] === true
301 8
        ) {
302 1
            throw new RuntimeException('Cannot add argument to an already initialized service.');
303
        }
304 8
    }
305
}
306