Completed
Push — master ( 47c396...980b0e )
by Yo
02:21
created

JsonRpcHttpServerExtension::aliasMethodResolver()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 29
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 4

Importance

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