Passed
Push — master ( a9856f...ec1f49 )
by Yo
01:54
created

compileAndProcessConfigurations()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 10
cts 10
cp 1
rs 8.8571
c 0
b 0
f 0
cc 6
eloc 9
nc 5
nop 2
crap 6
1
<?php
2
namespace Yoanm\SymfonyJsonRpcHttpServer\DependencyInjection;
3
4
use Symfony\Component\Config\Definition\Processor;
5
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
6
use Symfony\Component\DependencyInjection\ContainerBuilder;
7
use Symfony\Component\DependencyInjection\Definition;
8
use Symfony\Component\DependencyInjection\Exception\LogicException;
9
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
10
use Symfony\Component\DependencyInjection\Reference;
11
use Yoanm\JsonRpcServer\App\Creator\CustomExceptionCreator;
12
use Yoanm\JsonRpcServer\App\Creator\ResponseCreator;
13
use Yoanm\JsonRpcServer\App\Manager\MethodManager;
14
use Yoanm\JsonRpcServer\App\RequestHandler;
15
use Yoanm\JsonRpcServer\App\Serialization\RequestDenormalizer;
16
use Yoanm\JsonRpcServer\App\Serialization\ResponseNormalizer;
17
use Yoanm\JsonRpcServer\Infra\Endpoint\JsonRpcEndpoint;
18
use Yoanm\JsonRpcServer\Infra\Serialization\RawRequestSerializer;
19
use Yoanm\JsonRpcServer\Infra\Serialization\RawResponseSerializer;
20
use Yoanm\JsonRpcServerPsr11Resolver\Infra\Resolver\ContainerMethodResolver;
21
use Yoanm\SymfonyJsonRpcHttpServer\Endpoint\JsonRpcHttpEndpoint;
22
use Yoanm\SymfonyJsonRpcHttpServer\Resolver\ServiceNameResolver; // <= Must stay optional !
23
24
/**
25
 * Class JsonRpcHttpServerExtension
26
 *
27
 * /!\ In case you use the default resolver (yoanm/jsonrpc-server-sdk-psr11-resolver),
28
 * your JSON-RPC method services must be public in order to retrieve it later from container
29
 */
30
class JsonRpcHttpServerExtension implements ExtensionInterface, CompilerPassInterface
31
{
32
    // Use this service to inject string request
33
    const ENDPOINT_SERVICE_NAME = 'json_rpc_http_server.endpoint';
34
35
    // Use this tag to inject your own resolver
36
    const METHOD_RESOLVER_TAG = 'json_rpc_http_server.method_resolver';
37
38
    // Use this tag to inject your JSON-RPC methods into the default method resolver
39
    const JSONRPC_METHOD_TAG = 'json_rpc_http_server.jsonrpc_method';
40
41
    // In case you want to add mapping for a method, use the following service
42
    const SERVICE_NAME_RESOLVER_SERVICE_NAME = 'json_rpc_http_server.resolver.service_name';
43
    // And add an attribute with following key
44
    const JSONRPC_METHOD_TAG_METHOD_NAME_KEY = 'method';
45
46
    // Extension identifier (used in configuration for instance)
47
    const EXTENSION_IDENTIFIER = 'json_rpc_http_server';
48
49
50
    /** Private constants */
51
    const CUSTOM_METHOD_RESOLVER_CONTAINER_PARAM = self::EXTENSION_IDENTIFIER.'.custom_method_resolver';
52
    const METHODS_MAPPING_CONTAINER_PARAM = self::EXTENSION_IDENTIFIER.'.methods_mapping';
53
54
    /** @var bool */
55
    private $parseConfig = false;
56
57
    /**
58
     * @param bool|false $parseConfig If true, Config component is required
59
     */
60 15
    public function __construct(bool $parseConfig = false)
61
    {
62 15
        $this->parseConfig = $parseConfig;
63 15
    }
64
65
    /**
66
     * {@inheritdoc}
67
     */
68 14
    public function load(array $configs, ContainerBuilder $container)
69
    {
70 14
        $this->compileAndProcessConfigurations($configs, $container);
71
72
        // Use only references to avoid class instantiation
73
        // And don't use file configuration in order to not add Symfony\Component\Config as dependency
74 14
        $this->createPublicServiceDefinitions($container);
75 14
        $this->createInfraServiceDefinitions($container);
76 14
        $this->createAppServiceDefinitions($container);
77 14
    }
78
79
    /**
80
     * {@inheritdoc}
81
     */
82 15
    public function getNamespace()
83
    {
84 15
        return 'http://example.org/schema/dic/'.$this->getAlias();
85
    }
86
87
    /**
88
     * {@inheritdoc}
89
     */
90 1
    public function getXsdValidationBasePath()
91
    {
92 1
        return '';
93
    }
94
95
    /**
96
     * {@inheritdoc}
97
     */
98 15
    public function getAlias()
99
    {
100 15
        return self::EXTENSION_IDENTIFIER;
101
    }
102
103
    /**
104
     * {@inheritdoc}
105
     */
106 14
    public function process(ContainerBuilder $container)
107
    {
108 14
        $isContainerResolver = $this->aliasMethodResolver($container);
109 13
        if (true === $isContainerResolver) {
110 9
            $this->loadJsonRpcMethods($container);
111
        }
112 10
    }
113
114
    /**
115
     * @param ContainerBuilder $container
116
     */
117 14
    protected function createAppServiceDefinitions(ContainerBuilder $container)
118
    {
119
        // RequestDenormalizer
120 14
        $container->setDefinition(
121 14
            'json_rpc_http_server.sdk.app.serialization.request_denormalizer',
122 14
            new Definition(RequestDenormalizer::class)
123
        );
124
        // ResponseNormalizer
125 14
        $container->setDefinition(
126 14
            'json_rpc_http_server.sdk.app.serialization.response_normalizer',
127 14
            new Definition(ResponseNormalizer::class)
128
        );
129
        // ResponseCreator
130 14
        $container->setDefinition(
131 14
            'json_rpc_http_server.sdk.app.creator.response',
132 14
            new Definition(ResponseCreator::class)
133
        );
134
        // CustomExceptionCreator
135 14
        $container->setDefinition(
136 14
            'json_rpc_http_server.sdk.app.creator.custom_exception',
137 14
            new Definition(CustomExceptionCreator::class)
138
        );
139
140
        // MethodManager
141 14
        $container->setDefinition(
142 14
            'json_rpc_http_server.sdk.app.manager.method',
143 14
            new Definition(
144 14
                MethodManager::class,
145
                [
146 14
                    new Reference('json_rpc_http_server.infra.resolver.method'),
147 14
                    new Reference('json_rpc_http_server.sdk.app.creator.custom_exception')
148
                ]
149
            )
150
        );
151
        // RequestHandler
152 14
        $container->setDefinition(
153 14
            'json_rpc_http_server.sdk.app.handler.request',
154 14
            new Definition(
155 14
                RequestHandler::class,
156
                [
157 14
                    new Reference('json_rpc_http_server.sdk.app.manager.method'),
158 14
                    new Reference('json_rpc_http_server.sdk.app.creator.response')
159
                ]
160
            )
161
        );
162 14
    }
163
164
    /**
165
     * @param ContainerBuilder $container
166
     */
167 14
    protected function createInfraServiceDefinitions(ContainerBuilder $container)
168
    {
169
        // RawRequestSerializer
170 14
        $container->setDefinition(
171 14
            'json_rpc_http_server.sdk.infra.serialization.raw_request_serializer',
172 14
            new Definition(
173 14
                RawRequestSerializer::class,
174 14
                [new Reference('json_rpc_http_server.sdk.app.serialization.request_denormalizer')]
175
            )
176
        );
177
178
        // RawResponseSerializer
179 14
        $container->setDefinition(
180 14
            'json_rpc_http_server.sdk.infra.serialization.raw_response_serializer',
181 14
            new Definition(
182 14
                RawResponseSerializer::class,
183 14
                [new Reference('json_rpc_http_server.sdk.app.serialization.response_normalizer')]
184
            )
185
        );
186
        // JsonRpcEndpoint
187 14
        $container->setDefinition(
188 14
            'json_rpc_http_server.sdk.infra.endpoint',
189 14
            new Definition(
190 14
                JsonRpcEndpoint::class,
191
                [
192 14
                    new Reference('json_rpc_http_server.sdk.infra.serialization.raw_request_serializer'),
193 14
                    new Reference('json_rpc_http_server.sdk.app.handler.request'),
194 14
                    new Reference('json_rpc_http_server.sdk.infra.serialization.raw_response_serializer'),
195 14
                    new Reference('json_rpc_http_server.sdk.app.creator.response')
196
                ]
197
            )
198
        );
199
        // ContainerMethodResolver
200 14
        $container->setDefinition(
201 14
            'json_rpc_http_server.psr11.infra.resolver.method',
202 14
            (new Definition(
203 14
                ContainerMethodResolver::class,
204
                [
205 14
                    new Reference('service_container')
206
                ]
207 14
            ))->addMethodCall(
208 14
                'setServiceNameResolver',
209
                [
210 14
                    new Reference(self::SERVICE_NAME_RESOLVER_SERVICE_NAME)
211
                ]
212
            )
213
        );
214 14
    }
215
216
    /**
217
     * @param ContainerBuilder $container
218
     */
219 14
    protected function createPublicServiceDefinitions(ContainerBuilder $container)
220
    {
221
        // JsonRpcHttpEndpoint
222 14
        $container->setDefinition(
223 14
            self::ENDPOINT_SERVICE_NAME,
224 14
            (new Definition(
225 14
                JsonRpcHttpEndpoint::class,
226
                [
227 14
                    new Reference('json_rpc_http_server.sdk.infra.endpoint')
228
                ]
229 14
            ))->setPublic(true)
230
        );
231
        // ServiceNameResolver
232 14
        $container->setDefinition(
233 14
            self::SERVICE_NAME_RESOLVER_SERVICE_NAME,
234 14
            (new Definition(ServiceNameResolver::class))->setPublic(true)
235
        );
236 14
    }
237
238
    /**
239
     * @param ContainerBuilder $container
240
     *
241
     * @return bool Whether it is a ContainerResolver or not
242
     */
243 14
    private function aliasMethodResolver(ContainerBuilder $container)
244
    {
245 14
        $isContainerResolver = false;
246 14
        if ($container->hasParameter(self::CUSTOM_METHOD_RESOLVER_CONTAINER_PARAM)) {
247 2
            $resolverServiceId = $container->getParameter(self::CUSTOM_METHOD_RESOLVER_CONTAINER_PARAM);
248
        } else {
249 12
            $serviceIdList = array_keys($container->findTaggedServiceIds(self::METHOD_RESOLVER_TAG));
250 12
            $serviceCount = count($serviceIdList);
251 12
            if ($serviceCount > 0) {
252 3
                if ($serviceCount > 1) {
253 1
                    throw new LogicException(
254 1
                        sprintf(
255 1
                            'Only one method resolver could be defined, found following services : %s',
256 1
                            implode(', ', $serviceIdList)
257
                        )
258
                    );
259
                }
260
                // Use the first result
261 2
                $resolverServiceId = array_shift($serviceIdList);
262
            } else {
263
                // Use ArrayMethodResolver as default resolver
264 9
                $resolverServiceId = 'json_rpc_http_server.psr11.infra.resolver.method';
265 9
                $isContainerResolver = true;
266
            }
267
        }
268
269 13
        $container->setAlias('json_rpc_http_server.infra.resolver.method', $resolverServiceId);
270
271 13
        return $isContainerResolver;
272
    }
273
274
    /**
275
     * @param ContainerBuilder $container
276
     */
277 9
    private function loadJsonRpcMethods(ContainerBuilder $container)
278
    {
279
        // Check if methods have been defined by tags
280 9
        $methodServiceList = $container->findTaggedServiceIds(self::JSONRPC_METHOD_TAG);
281
282 9
        foreach ($methodServiceList as $externalServiceIdString => $tagAttributeList) {
283 3
            $serviceId = $this->cleanExternalServiceIdString($externalServiceIdString);
284 3
            $this->checkJsonRpcMethodService($container, $serviceId);
285 3
            $methodNameList = $this->extractMethodNameList($tagAttributeList, $serviceId);
286 3
            foreach ($methodNameList as $methodName) {
287 3
                $this->injectMethodMappingToServiceNameResolver($methodName, $serviceId, $container);
288
            }
289
        }
290
291 7
        if ($container->hasParameter(self::METHODS_MAPPING_CONTAINER_PARAM)) {
292 3
            foreach ($container->getParameter(self::METHODS_MAPPING_CONTAINER_PARAM) as $methodName => $mappingConfig) {
293 3
                $serviceId = $this->cleanExternalServiceIdString($mappingConfig['service']);
294 3
                $this->checkJsonRpcMethodService($container, $serviceId);
295 2
                $this->injectMethodMappingToServiceNameResolver($methodName, $serviceId, $container);
296 2
                foreach ($mappingConfig['aliases'] as $methodAlias) {
297 2
                    $this->injectMethodMappingToServiceNameResolver($methodAlias, $serviceId, $container);
298
                }
299
            }
300
        }
301 6
    }
302
303
    /**
304
     * @param array  $tagAttributeList
305
     * @param string $serviceId
306
     */
307 3
    private function extractMethodNameList(array $tagAttributeList, string $serviceId) : array
308
    {
309 3
        $methodNameList = [];
310 3
        foreach ($tagAttributeList as $tagAttributeKey => $tagAttributeData) {
311 3
            if (!array_key_exists(self::JSONRPC_METHOD_TAG_METHOD_NAME_KEY, $tagAttributeData)) {
312 1
                throw new LogicException(sprintf(
313
                    'Service "%s" is taggued as JSON-RPC method but does not have'
314 1
                    . ' method name defined under "%s" tag attribute key',
315 1
                    $serviceId,
316 1
                    self::JSONRPC_METHOD_TAG_METHOD_NAME_KEY
317
                ));
318
            }
319 3
            $methodNameList[] = $tagAttributeData[self::JSONRPC_METHOD_TAG_METHOD_NAME_KEY];
320
        }
321
322 3
        return $methodNameList;
323
    }
324
325
    /**
326
     * @param ContainerBuilder $container
327
     * @param string           $serviceId
328
     */
329 6
    private function checkJsonRpcMethodService(ContainerBuilder $container, string $serviceId)
330
    {
331
        // Check if given service is public => must be public in order to get it from container later
332 6
        if (!$container->getDefinition($serviceId)->isPublic()) {
333 2
            throw new LogicException(sprintf(
334
                'Service "%s" is taggued as JSON-RPC method but is not public. Service must be public in order'
335 2
                . ' to retrieve it later',
336 2
                $serviceId
337
            ));
338
        }
339 5
    }
340
341
    /**
342
     * @param array            $configs
343
     * @param ContainerBuilder $container
344
     */
345 14
    private function compileAndProcessConfigurations(array $configs, ContainerBuilder $container)
346
    {
347 14
        if (true === $this->parseConfig) {
348 5
            $configuration = new Configuration();
349 5
            $config = (new Processor())->processConfiguration($configuration, $configs);
350
351 5
            if (array_key_exists('method_resolver', $config) && $config['method_resolver']) {
352 2
                $container->setParameter(
353 2
                    self::CUSTOM_METHOD_RESOLVER_CONTAINER_PARAM,
354 2
                    $this->cleanExternalServiceIdString($config['method_resolver'])
355
                );
356
            }
357 5
            if (array_key_exists('methods_mapping', $config) && is_array($config['methods_mapping'])) {
358 5
                $container->setParameter(self::METHODS_MAPPING_CONTAINER_PARAM, $config['methods_mapping']);
359
            }
360
        }
361 14
    }
362
363
    /**
364
     * @param string           $methodName
365
     * @param string           $serviceId
366
     * @param ContainerBuilder $container
367
     */
368 5
    private function injectMethodMappingToServiceNameResolver(
369
        string $methodName,
370
        string $serviceId,
371
        ContainerBuilder $container
372
    ) {
373 5
        $container->getDefinition(self::SERVICE_NAME_RESOLVER_SERVICE_NAME)
374 5
            ->addMethodCall('addMethodMapping', [$methodName, $serviceId]);
375 5
    }
376
377 8
    private function cleanExternalServiceIdString(string $externalServiceIdString)
378
    {
379 8
        if ('@' === $externalServiceIdString[0]) {
380 2
            return substr($externalServiceIdString, 1);
381
        }
382
383 6
        return $externalServiceIdString;
384
    }
385
}
386