Passed
Push — master ( caaf65...a9856f )
by Yo
01:56
created

createAppServiceDefinitions()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 42
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 42
ccs 25
cts 25
cp 1
rs 8.8571
c 0
b 0
f 0
cc 1
eloc 24
nc 1
nop 1
crap 1
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 13
    public function __construct(bool $parseConfig = false)
61
    {
62 13
        $this->parseConfig = $parseConfig;
63 13
    }
64
65
    /**
66
     * {@inheritdoc}
67
     */
68 12
    public function load(array $configs, ContainerBuilder $container)
69
    {
70 12
        $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 12
        $this->createPublicServiceDefinitions($container);
75 12
        $this->createInfraServiceDefinitions($container);
76 12
        $this->createAppServiceDefinitions($container);
77 12
    }
78
79
    /**
80
     * {@inheritdoc}
81
     */
82 13
    public function getNamespace()
83
    {
84 13
        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 13
    public function getAlias()
99
    {
100 13
        return self::EXTENSION_IDENTIFIER;
101
    }
102
103
    /**
104
     * {@inheritdoc}
105
     */
106 12
    public function process(ContainerBuilder $container)
107
    {
108 12
        $isContainerResolver = $this->aliasMethodResolver($container);
109 11
        if (true === $isContainerResolver) {
110 8
            $this->loadJsonRpcMethods($container);
111
        }
112 8
    }
113
114
    /**
115
     * @param ContainerBuilder $container
116
     */
117 12
    protected function createAppServiceDefinitions(ContainerBuilder $container)
118
    {
119
        // RequestDenormalizer
120 12
        $container->setDefinition(
121 12
            'json_rpc_http_server.sdk.app.serialization.request_denormalizer',
122 12
            new Definition(RequestDenormalizer::class)
123
        );
124
        // ResponseNormalizer
125 12
        $container->setDefinition(
126 12
            'json_rpc_http_server.sdk.app.serialization.response_normalizer',
127 12
            new Definition(ResponseNormalizer::class)
128
        );
129
        // ResponseCreator
130 12
        $container->setDefinition(
131 12
            'json_rpc_http_server.sdk.app.creator.response',
132 12
            new Definition(ResponseCreator::class)
133
        );
134
        // CustomExceptionCreator
135 12
        $container->setDefinition(
136 12
            'json_rpc_http_server.sdk.app.creator.custom_exception',
137 12
            new Definition(CustomExceptionCreator::class)
138
        );
139
140
        // MethodManager
141 12
        $container->setDefinition(
142 12
            'json_rpc_http_server.sdk.app.manager.method',
143 12
            new Definition(
144 12
                MethodManager::class,
145
                [
146 12
                    new Reference('json_rpc_http_server.infra.resolver.method'),
147 12
                    new Reference('json_rpc_http_server.sdk.app.creator.custom_exception')
148
                ]
149
            )
150
        );
151
        // RequestHandler
152 12
        $container->setDefinition(
153 12
            'json_rpc_http_server.sdk.app.handler.request',
154 12
            new Definition(
155 12
                RequestHandler::class,
156
                [
157 12
                    new Reference('json_rpc_http_server.sdk.app.manager.method'),
158 12
                    new Reference('json_rpc_http_server.sdk.app.creator.response')
159
                ]
160
            )
161
        );
162 12
    }
163
164
    /**
165
     * @param ContainerBuilder $container
166
     */
167 12
    protected function createInfraServiceDefinitions(ContainerBuilder $container)
168
    {
169
        // RawRequestSerializer
170 12
        $container->setDefinition(
171 12
            'json_rpc_http_server.sdk.infra.serialization.raw_request_serializer',
172 12
            new Definition(
173 12
                RawRequestSerializer::class,
174 12
                [new Reference('json_rpc_http_server.sdk.app.serialization.request_denormalizer')]
175
            )
176
        );
177
178
        // RawResponseSerializer
179 12
        $container->setDefinition(
180 12
            'json_rpc_http_server.sdk.infra.serialization.raw_response_serializer',
181 12
            new Definition(
182 12
                RawResponseSerializer::class,
183 12
                [new Reference('json_rpc_http_server.sdk.app.serialization.response_normalizer')]
184
            )
185
        );
186
        // JsonRpcEndpoint
187 12
        $container->setDefinition(
188 12
            'json_rpc_http_server.sdk.infra.endpoint',
189 12
            new Definition(
190 12
                JsonRpcEndpoint::class,
191
                [
192 12
                    new Reference('json_rpc_http_server.sdk.infra.serialization.raw_request_serializer'),
193 12
                    new Reference('json_rpc_http_server.sdk.app.handler.request'),
194 12
                    new Reference('json_rpc_http_server.sdk.infra.serialization.raw_response_serializer'),
195 12
                    new Reference('json_rpc_http_server.sdk.app.creator.response')
196
                ]
197
            )
198
        );
199
        // ContainerMethodResolver
200 12
        $container->setDefinition(
201 12
            'json_rpc_http_server.psr11.infra.resolver.method',
202 12
            (new Definition(
203 12
                ContainerMethodResolver::class,
204
                [
205 12
                    new Reference('service_container')
206
                ]
207 12
            ))->addMethodCall(
208 12
                'setServiceNameResolver',
209
                [
210 12
                    new Reference(self::SERVICE_NAME_RESOLVER_SERVICE_NAME)
211
                ]
212
            )
213
        );
214 12
    }
215
216
    /**
217
     * @param ContainerBuilder $container
218
     */
219 12
    protected function createPublicServiceDefinitions(ContainerBuilder $container)
220
    {
221
        // JsonRpcHttpEndpoint
222 12
        $container->setDefinition(
223 12
            self::ENDPOINT_SERVICE_NAME,
224 12
            (new Definition(
225 12
                JsonRpcHttpEndpoint::class,
226
                [
227 12
                    new Reference('json_rpc_http_server.sdk.infra.endpoint')
228
                ]
229 12
            ))->setPublic(true)
230
        );
231
        // ServiceNameResolver
232 12
        $container->setDefinition(
233 12
            self::SERVICE_NAME_RESOLVER_SERVICE_NAME,
234 12
            (new Definition(ServiceNameResolver::class))->setPublic(true)
235
        );
236 12
    }
237
238
    /**
239
     * @param ContainerBuilder $container
240
     *
241
     * @return bool Whether it is a ContainerResolver or not
242
     */
243 12
    private function aliasMethodResolver(ContainerBuilder $container)
244
    {
245 12
        $isContainerResolver = false;
246 12
        if ($container->hasParameter(self::CUSTOM_METHOD_RESOLVER_CONTAINER_PARAM)) {
247 1
            $resolverServiceId = $container->getParameter(self::CUSTOM_METHOD_RESOLVER_CONTAINER_PARAM);
248
        } else {
249 11
            $serviceIdList = array_keys($container->findTaggedServiceIds(self::METHOD_RESOLVER_TAG));
250 11
            $serviceCount = count($serviceIdList);
251 11
            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 8
                $resolverServiceId = 'json_rpc_http_server.psr11.infra.resolver.method';
265 8
                $isContainerResolver = true;
266
            }
267
        }
268
269 11
        $container->setAlias('json_rpc_http_server.infra.resolver.method', $resolverServiceId);
270
271 11
        return $isContainerResolver;
272
    }
273
274
    /**
275
     * @param ContainerBuilder $container
276
     */
277 8
    private function loadJsonRpcMethods(ContainerBuilder $container)
278
    {
279
        // Check if methods have been defined by tags
280 8
        $methodServiceList = $container->findTaggedServiceIds(self::JSONRPC_METHOD_TAG);
281
282 8
        foreach ($methodServiceList as $serviceId => $tagAttributeList) {
283 3
            $this->checkJsonRpcMethodService($container, $serviceId);
284 3
            $methodNameList = $this->extractMethodNameList($tagAttributeList, $serviceId);
285 3
            foreach ($methodNameList as $methodName) {
286 3
                $this->injectMethodMappingToServiceNameResolver($methodName, $serviceId, $container);
287
            }
288
        }
289
290 6
        if ($container->hasParameter(self::METHODS_MAPPING_CONTAINER_PARAM)) {
291 2
            foreach ($container->getParameter(self::METHODS_MAPPING_CONTAINER_PARAM) as $methodName => $mappingConfig) {
292 2
                $serviceId = $mappingConfig['service'];
293 2
                $this->checkJsonRpcMethodService($container, $serviceId);
294 1
                $this->injectMethodMappingToServiceNameResolver($methodName, $serviceId, $container);
295 1
                foreach ($mappingConfig['aliases'] as $methodAlias) {
296 1
                    $this->injectMethodMappingToServiceNameResolver($methodAlias, $serviceId, $container);
297
                }
298
            }
299
        }
300 5
    }
301
302
    /**
303
     * @param array  $tagAttributeList
304
     * @param string $serviceId
305
     */
306 3
    private function extractMethodNameList(array $tagAttributeList, string $serviceId) : array
307
    {
308 3
        $methodNameList = [];
309 3
        foreach ($tagAttributeList as $tagAttributeKey => $tagAttributeData) {
310 3
            if (!array_key_exists(self::JSONRPC_METHOD_TAG_METHOD_NAME_KEY, $tagAttributeData)) {
311 1
                throw new LogicException(sprintf(
312
                    'Service "%s" is taggued as JSON-RPC method but does not have'
313 1
                    . ' method name defined under "%s" tag attribute key',
314 1
                    $serviceId,
315 1
                    self::JSONRPC_METHOD_TAG_METHOD_NAME_KEY
316
                ));
317
            }
318 3
            $methodNameList[] = $tagAttributeData[self::JSONRPC_METHOD_TAG_METHOD_NAME_KEY];
319
        }
320
321 3
        return $methodNameList;
322
    }
323
324
    /**
325
     * @param ContainerBuilder $container
326
     * @param string           $serviceId
327
     */
328 5
    private function checkJsonRpcMethodService(ContainerBuilder $container, string $serviceId)
329
    {
330
        // Check if given service is public => must be public in order to get it from container later
331 5
        if (!$container->getDefinition($serviceId)->isPublic()) {
332 2
            throw new LogicException(sprintf(
333
                'Service "%s" is taggued as JSON-RPC method but is not public. Service must be public in order'
334 2
                . ' to retrieve it later',
335 2
                $serviceId
336
            ));
337
        }
338 4
    }
339
340
    /**
341
     * @param array            $configs
342
     * @param ContainerBuilder $container
343
     */
344 12
    private function compileAndProcessConfigurations(array $configs, ContainerBuilder $container)
345
    {
346 12
        if (true === $this->parseConfig) {
347 3
            $configuration = new Configuration();
348 3
            $config = (new Processor())->processConfiguration($configuration, $configs);
349
350 3
            if (array_key_exists('method_resolver', $config) && $config['method_resolver']) {
351 1
                $container->setParameter(self::CUSTOM_METHOD_RESOLVER_CONTAINER_PARAM, $config['method_resolver']);
352
            }
353 3
            if (array_key_exists('methods_mapping', $config) && is_array($config['methods_mapping'])) {
354 3
                $container->setParameter(self::METHODS_MAPPING_CONTAINER_PARAM, $config['methods_mapping']);
355
            }
356
        }
357 12
    }
358
359
    /**
360
     * @param string           $methodName
361
     * @param string           $serviceId
362
     * @param ContainerBuilder $container
363
     */
364 4
    private function injectMethodMappingToServiceNameResolver(
365
        string $methodName,
366
        string $serviceId,
367
        ContainerBuilder $container
368
    ) {
369 4
        $container->getDefinition(self::SERVICE_NAME_RESOLVER_SERVICE_NAME)
370 4
            ->addMethodCall('addMethodMapping', [$methodName, $serviceId]);
371 4
    }
372
}
373