Completed
Push — master ( 28dea3...014bac )
by Yo
01:59
created

extractMethodNameList()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3

Importance

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