Completed
Pull Request — master (#13)
by Rafael
06:46
created

Configuration::configureSubscriptions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 19
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 15
dl 0
loc 19
ccs 0
cts 17
cp 0
rs 9.7666
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 2
1
<?php
2
3
/*******************************************************************************
4
 *  This file is part of the GraphQL Bundle package.
5
 *
6
 *  (c) YnloUltratech <[email protected]>
7
 *
8
 *  For the full copyright and license information, please view the LICENSE
9
 *  file that was distributed with this source code.
10
 ******************************************************************************/
11
12
namespace Ynlo\GraphQLBundle\DependencyInjection;
13
14
use GraphQL\Validator\Rules\QueryComplexity;
15
use GraphQL\Validator\Rules\QueryDepth;
16
use Symfony\Component\Config\Definition\Builder\NodeBuilder;
17
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
18
use Symfony\Component\Config\Definition\ConfigurationInterface;
19
use Ynlo\GraphQLBundle\Encoder\SecureIDEncoder;
20
use Ynlo\GraphQLBundle\Error\DefaultErrorFormatter;
21
use Ynlo\GraphQLBundle\Error\DefaultErrorHandler;
22
use Ynlo\GraphQLBundle\Subscription\PubSub\RedisPubSubHandler;
23
use Ynlo\GraphQLBundle\Subscription\Subscriber;
24
25
/**
26
 * Class Configuration
27
 */
28
class Configuration implements ConfigurationInterface
29
{
30
    /**
31
     * {@inheritdoc}
32
     */
33
    public function getConfigTreeBuilder()
34
    {
35
        $treeBuilder = new TreeBuilder();
36
        /** @var NodeBuilder $rootNode */
37
        $rootNode = $treeBuilder->root('graphql')->addDefaultsIfNotSet()->children();
0 ignored issues
show
Bug introduced by
The method addDefaultsIfNotSet() does not exist on Symfony\Component\Config...\Builder\NodeDefinition. It seems like you code against a sub-type of Symfony\Component\Config...\Builder\NodeDefinition such as Symfony\Component\Config...der\ArrayNodeDefinition. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

37
        $rootNode = $treeBuilder->root('graphql')->/** @scrutinizer ignore-call */ addDefaultsIfNotSet()->children();
Loading history...
38
        $this->configureEndpoints($rootNode);
39
        $this->configureSubscriptions($rootNode);
40
        $this->configureErrorHandling($rootNode);
41
        $this->configureCORS($rootNode);
42
        $this->configureGraphiQL($rootNode);
43
        $this->configurePlugins($rootNode);
44
        $this->configureSecurity($rootNode);
45
        $this->configureOthers($rootNode);
46
        $this->configureBCCompatibility($rootNode);
47
48
        return $treeBuilder;
49
    }
50
51
    protected function configureSubscriptions(NodeBuilder $root)
52
    {
53
        $subscriptions = $root->arrayNode('subscriptions')
54
                              ->info('Manage subscriptions settings')
55
                              ->addDefaultsIfNotSet()
56
                              ->children();
57
58
        $subscriptions->scalarNode('ttl')->defaultValue(Subscriber::DEFAULT_SUBSCRIPTION_TTL)
59
                      ->info('Time to live for subscriptions. The subscription will be deleted after this time, a heartbeat is required to keep-alive');
60
        $subscriptions->scalarNode('mercure_hub')->defaultValue('default');
61
        $subscriptions->scalarNode('pubsub_handler')->defaultValue(RedisPubSubHandler::class);
62
        $redis = $subscriptions->arrayNode('redis')->info('Configure redis server to use as subscription handler')
63
                               ->addDefaultsIfNotSet()
64
                               ->children();
65
66
        $redis->scalarNode('host')->defaultValue('localhost');
67
        $redis->integerNode('port')->defaultValue(6379);
68
        $redis->integerNode('prefix')->defaultValue('GraphQLSubscription:')
69
              ->info('Define custom prefix to avoid collisions between applications');
70
    }
71
72
    protected function configureErrorHandling(NodeBuilder $root)
73
    {
74
        $errorHandling = $root->arrayNode('error_handling')
75
                              ->info('It is important to handle errors and when possible, report these errors back to your users for information. ')
76
                              ->addDefaultsIfNotSet()
77
                              ->children();
78
79
        $errorHandling->enumNode('validation_messages')
80
                      ->values(['error', 'payload', 'both'])
81
                      ->info('Where should be displayed validation messages.')
82
                      ->defaultValue('error');
83
84
        //@deprecated since v1.1 snd should be deleted in 2.0, this is a compatibility flag
85
        $errorHandling->booleanNode('jwt_auth_failure_compatibility')
86
                      ->info('Keep BC with oldest version of JWT Authentication errors')
87
                      ->setDeprecated('Since v1.1 and will will be removed in the next mayor release')
88
                      ->defaultFalse();
89
90
        $errorHandling->booleanNode('show_trace')->info('Show error trace in debug mode')->defaultFalse();
91
92
        $errorHandling->scalarNode('formatter')
93
                      ->info('Formatter is responsible for converting instances of Error to an array')
94
                      ->defaultValue(DefaultErrorFormatter::class);
95
96
        $errorHandling->scalarNode('handler')
97
                      ->info('Handler is useful for error filtering and logging.')
98
                      ->defaultValue(DefaultErrorHandler::class);
99
100
        $controlledErrors = $errorHandling
101
            ->arrayNode('controlled_errors')
102
            ->info('List of controlled errors')
103
            ->addDefaultsIfNotSet()
104
            ->children();
105
106
        $map = $controlledErrors->arrayNode('map')->useAttributeAsKey('code')->arrayPrototype()->children();
107
        $map->scalarNode('message')->isRequired();
108
        $map->scalarNode('description')->isRequired();
109
        $map->scalarNode('category')->defaultValue('user');
110
111
        $autoload = $controlledErrors
112
            ->arrayNode('autoload')
113
            ->info('Autoload exceptions implementing ControlledErrorInterface')
114
            ->addDefaultsIfNotSet()
115
            ->canBeDisabled()
116
            ->children();
117
118
        $autoload
119
            ->variableNode('locations')
120
            ->defaultValue(['Exception', 'Error'])
121
            ->info('Default folder to find exceptions and errors implementing controlled interface.')
122
            ->beforeNormalization()
123
            ->ifString()
124
            ->then(
125
                function ($v) {
126
                    return [$v];
127
                }
128
            )
129
            ->end();
130
131
        $autoload
132
            ->variableNode('whitelist')
133
            ->info('White listed classes')
134
            ->defaultValue(['/App\\\\[Exception|Error]/', '/\w+Bundle\\\\[Exception|Error]/'])
135
            ->beforeNormalization()
136
            ->ifString()
137
            ->then(
138
                function ($v) {
139
                    return [$v];
140
                }
141
            )
142
            ->end()
143
            ->validate()
144
            ->ifTrue(
145
                function (array $value) {
146
                    foreach ($value as $val) {
147
                        try {
148
                            preg_match($val, null);
149
                        } catch (\Exception $exception) {
150
                            return true;
151
                        }
152
                    }
153
                }
154
            )->thenInvalid('Invalid regular expression');
155
156
        $autoload
157
            ->variableNode('blacklist')
158
            ->info('Black listed classes')
159
            ->beforeNormalization()
160
            ->ifString()
161
            ->then(
162
                function ($v) {
163
                    return [$v];
164
                }
165
            )
166
            ->end()
167
            ->validate()
168
            ->ifTrue(
169
                function (array $value) {
170
                    foreach ($value as $val) {
171
                        try {
172
                            preg_match($val, null);
173
                        } catch (\Exception $exception) {
174
                            return true;
175
                        }
176
                    }
177
                }
178
            )->thenInvalid('Invalid regular expression');
179
    }
180
181
    protected function configureEndpoints(NodeBuilder $root)
182
    {
183
        $endpoints = $root->arrayNode('endpoints')
184
                          ->useAttributeAsKey('name')
185
                          ->validate()
186
                          ->ifTrue(
187
                              function ($v) {
188
                                  return array_key_exists('default', $v);
189
                              }
190
                          )->thenInvalid('"default" can\'t be used as endpoint name, the system internally use this endpoint name to store the entire schema.')
191
                          ->end()
192
                          ->arrayPrototype()
0 ignored issues
show
Bug introduced by
The method arrayPrototype() does not exist on Symfony\Component\Config...\Builder\NodeDefinition. It seems like you code against a sub-type of Symfony\Component\Config...\Builder\NodeDefinition such as Symfony\Component\Config...der\ArrayNodeDefinition. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

192
                          ->/** @scrutinizer ignore-call */ arrayPrototype()
Loading history...
193
                          ->children();
194
195
        $endpoints->arrayNode('roles')
196
                  ->beforeNormalization()
197
                  ->ifString()
198
                  ->then(
199
                      function ($v) {
200
                          return preg_split('/\s*,\s*/', $v);
201
                      }
202
                  )
203
                  ->end()
204
                  ->prototype('scalar')
205
                  ->end();
206
207
        $endpoints->scalarNode('host')->example('^api\.backend\.');
208
        $endpoints->scalarNode('path')->example('/backend');
209
210
        $root->arrayNode('endpoint_alias')
211
             ->info('Use alias to refer to multiple endpoints using only one name')
212
             ->useAttributeAsKey('name')
213
             ->beforeNormalization()
214
             ->ifString()
215
             ->then(
216
                 function ($v) {
217
                     return preg_split('/\s*,\s*/', $v);
218
                 }
219
             )
220
             ->end()
221
             ->variablePrototype();
0 ignored issues
show
Bug introduced by
The method variablePrototype() does not exist on Symfony\Component\Config...\Builder\NodeDefinition. It seems like you code against a sub-type of Symfony\Component\Config...\Builder\NodeDefinition such as Symfony\Component\Config...der\ArrayNodeDefinition. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

221
             ->/** @scrutinizer ignore-call */ variablePrototype();
Loading history...
222
223
        $root->scalarNode('endpoint_default')->info('Endpoint to apply to all definitions without explicit endpoint.');
224
225
    }
226
227
    protected function configureGraphiQL(NodeBuilder $root)
228
    {
229
        $graphiql = $root->arrayNode('graphiql')->addDefaultsIfNotSet()->children();
230
231
        $graphiql->scalarNode('title')
232
                 ->defaultValue('GraphQL API Explorer');
233
234
        $graphiql
235
            ->scalarNode('data_warning_message')
236
            ->defaultValue('Heads up! GraphQL Explorer makes use of your <strong>real</strong>, <strong>live</strong>, <strong>production</strong> data.');
237
        $graphiql->booleanNode('data_warning_dismissible')->defaultTrue();
238
        $graphiql->enumNode('data_warning_style')->values(['info', 'warning', 'danger'])->defaultValue('danger');
239
240
        $graphiql->scalarNode('template')->defaultValue('@YnloGraphQL/explorer.html.twig');
241
        $graphiql->scalarNode('default_query')->defaultNull()->info('An optional GraphQL string to use when no query exists from a previous session. If none is provided, GraphiQL will use its own default query.');
242
243
        $graphiql->scalarNode('favicon')->info('Url or path to favicon');
244
245
        $docs = $graphiql->arrayNode('documentation')->info('Display external API documentation link')->addDefaultsIfNotSet()->children();
246
        $docs->scalarNode('link')->info('Url, route or path.');
247
        $docs->scalarNode('btn_label')->defaultValue('Documentation');
248
        $docs->scalarNode('btn_class')->defaultValue('btn btn-outline-success');
249
250
        $authentication = $graphiql->arrayNode('authentication')->addDefaultsIfNotSet()->children();
251
        $authentication
252
            ->booleanNode('required')
253
            ->info(
254
                'The API require credentials to make any requests, 
255
if this value is FALSE and a provider is specified the authentication is optional.'
256
            )
257
            ->defaultFalse();
258
259
        $authentication->scalarNode('login_message')
260
                       ->defaultValue('Start exploring GraphQL API queries using your account’s data now.');
261
262
        $authenticationProvider = $authentication->arrayNode('provider')->children();
263
264
        //the updated version of `jwt` to use lexik authentication bundle
265
        $lexikJwt = $authenticationProvider->arrayNode('lexik_jwt')
266
                                           ->canBeEnabled()
267
                                           ->children();
268
269
        $lexikJwt->scalarNode('user_provider')
270
                 ->isRequired()
271
                 ->info('Name of the user provider to use');
272
273
        $lexikJwt->scalarNode('username_label')
274
                 ->defaultValue('Username');
275
276
        $lexikJwt->scalarNode('password_label')
277
                 ->defaultValue('Password');
278
279
        $authenticationProvider->scalarNode('custom')
280
                               ->defaultNull()
281
                               ->info('Configure custom service to use as authentication provider');
282
283
        //deprecated since v1.1 and should be deleted in v2.0
284
        $jwt = $authenticationProvider->arrayNode('jwt')
285
                                      ->setDeprecated('Use lexik_jwt instead, this provider will be removed in the next mayor release.')
286
                                      ->canBeEnabled()
287
                                      ->children();
288
289
        $jwtLogin = $jwt->arrayNode('login')->children();
290
291
        $jwtLogin->scalarNode('url')
292
                 ->info('Route name or URI to make the login process to retrieve the token.')
293
                 ->isRequired();
294
295
        $jwtLogin->scalarNode('username_parameter')
296
                 ->defaultValue('username');
297
298
        $jwtLogin->scalarNode('username_label')
299
                 ->defaultValue('Username');
300
301
        $jwtLogin->scalarNode('password_parameter')
302
                 ->defaultValue('password');
303
304
        $jwtLogin->scalarNode('password_label')
305
                 ->defaultValue('Password');
306
307
        $jwtLogin->enumNode('parameters_in')
308
                 ->values(['form', 'query', 'header'])
309
                 ->info('How pass parameters to request the token')
310
                 ->defaultValue('form');
311
312
        $jwtLogin->scalarNode('response_token_path')
313
                 ->defaultValue('token')
314
                 ->info('Where the token should be located in the response in case of JSON, set null if the response is the token.');
315
316
        $jwtRequests = $jwt->arrayNode('requests')->addDefaultsIfNotSet()->children();
317
318
        $jwtRequests->enumNode('token_in')
319
                    ->values(['query', 'header'])
320
                    ->info('Where should be located the token on every request')
321
                    ->defaultValue('header');
322
323
        $jwtRequests->scalarNode('token_name')
324
                    ->defaultValue('Authorization')
325
                    ->info('Name of the token in query or header name');
326
327
        $jwtRequests->scalarNode('token_template')
328
                    ->defaultValue('Bearer {token}')
329
                    ->info('Customize how the token should be send,  use the place holder {token} to replace for current token');
330
331
    }
332
333
    protected function configureCORS(NodeBuilder $root)
334
    {
335
        $cors = $root->arrayNode('cors')->canBeEnabled()->children();
336
        $cors->booleanNode('allow_credentials')->defaultTrue();
337
        $cors->variableNode('allow_headers')->defaultValue(['Origin', 'Content-Type', 'Accept', 'Authorization']);
338
        $cors->integerNode('max_age')->defaultValue(3600);
339
        $cors->variableNode('allow_methods')->defaultValue(['POST', 'GET', 'OPTIONS']);
340
        $cors->variableNode('allow_origins')->defaultValue(['*']);
341
    }
342
343
    protected function configurePlugins(NodeBuilder $root)
344
    {
345
        $this->configurePluginPaginationGlobalConfig($root);
346
        $this->configurePluginNamespaceGlobalConfig($root);
347
    }
348
349
    protected function configurePluginPaginationGlobalConfig(NodeBuilder $root)
350
    {
351
        $pagination = $root->arrayNode('pagination')->addDefaultsIfNotSet()->children();
352
        $pagination->integerNode('limit')
353
                   ->defaultValue(100)->info('Maximum limit allowed for all paginations');
354
    }
355
356
    protected function configurePluginNamespaceGlobalConfig(NodeBuilder $root)
357
    {
358
        $namespaces = $root->arrayNode('namespaces')
359
                           ->info(
360
                               'Group GraphQL schema using namespaced schemas. 
361
On large schemas is  helpful to keep schemas grouped by bundle and node'
362
                           )
363
                           ->canBeEnabled()
364
                           ->addDefaultsIfNotSet()
365
                           ->children();
366
367
        $bundles = $namespaces->arrayNode('bundles')
368
                              ->info('Group each bundle into a separate schema definition')
369
                              ->canBeDisabled()
370
                              ->addDefaultsIfNotSet()
371
                              ->children();
372
373
        $bundles->scalarNode('query_suffix')
374
                ->info('The following suffix will be used for bundle query groups')
375
                ->defaultValue('BundleQuery');
376
377
        $bundles->scalarNode('mutation_suffix')
378
                ->info('The following suffix will be used for bundle mutation groups')
379
                ->defaultValue('BundleMutation');
380
381
        $bundles->scalarNode('subscription_suffix')
382
                ->info('The following suffix will be used for bundle subscription groups')
383
                ->defaultValue('BundleSubscription');
384
385
        $bundles->variableNode('ignore')
386
                ->info('The following bundles will be ignore for grouping, all definitions will be placed in the root query or mutation')
387
                ->defaultValue(['AppBundle']);
388
389
        $bundles->arrayNode('aliases')
390
                ->info(
391
                    'Define aliases for bundles to set definitions inside other desired bundle name. 
392
Can be used to group multiple bundles or publish a bundle with a different name'
393
                )
394
                ->example('SecurityBundle: AppBundle')
395
                ->useAttributeAsKey('name')
396
                ->prototype('scalar');
397
398
399
        $nodes = $namespaces->arrayNode('nodes')
400
                            ->info('Group queries and mutations of the same node into a node specific schema definition.')
401
                            ->addDefaultsIfNotSet()
402
                            ->canBeDisabled()
403
                            ->children();
404
405
        $nodes->scalarNode('query_suffix')
406
              ->info('The following suffix will be used to create the name for queries to the same node')
407
              ->defaultValue('Query');
408
409
        $nodes->scalarNode('mutation_suffix')
410
              ->info('The following suffix will be used to create the name for mutations to the same node')
411
              ->defaultValue('Mutation');
412
413
        $nodes->scalarNode('subscription_suffix')
414
              ->info('The following suffix will be used to create the name for subscriptions to the same node')
415
              ->defaultValue('Subscriptions');
416
417
        $nodes->variableNode('ignore')
418
              ->info('The following nodes will be ignore for grouping, all definitions will be placed in the root query or mutation')
419
              ->defaultValue(['Node']);
420
421
        $nodes->arrayNode('aliases')
422
              ->info(
423
                  'Define aliases for nodes to set definitions inside other desired node name. 
424
Can be used to group multiple nodes or publish a node with a different group name'
425
              )
426
              ->example('InvoiceItem: Invoice')
427
              ->useAttributeAsKey('name')
428
              ->prototype('scalar');
429
    }
430
431
    private function configureSecurity(NodeBuilder $rootNode)
432
    {
433
        $securityNode = $rootNode
434
            ->arrayNode('security')
435
            ->canBeEnabled()
436
            ->children();
437
438
        $validationRulesNode = $securityNode
439
            ->arrayNode('validation_rules')
440
            ->addDefaultsIfNotSet()
441
            ->children();
442
        $validationRulesNode
443
            ->integerNode('query_complexity')
444
            ->info('Query complexity score before execution. (Recommended >= 200)')
445
            ->min(0)
446
            ->defaultValue(QueryComplexity::DISABLED);
447
        $validationRulesNode
448
            ->integerNode('query_depth')
449
            ->info('Max depth of the query. (Recommended >= 11)')
450
            ->min(0)
451
            ->defaultValue(QueryDepth::DISABLED);
452
        $validationRulesNode
453
            ->booleanNode('disable_introspection')
454
            ->defaultFalse();
455
    }
456
457
    private function configureOthers(NodeBuilder $rootNode)
458
    {
459
        $rootNode
460
            ->scalarNode('id_encoder')
461
            ->defaultValue(SecureIDEncoder::class)
462
            ->info('Service used to encode nodes identifiers, must implements IDEncoderInterface');
463
    }
464
465
    private function configureBCCompatibility(NodeBuilder $rootNode)
466
    {
467
        $bcNode = $rootNode
468
            ->arrayNode('bc')
469
            ->info('Backward compatibility layer to keep deprecated features during some time to upgrade API consumers progressively.')
470
            ->addDefaultsIfNotSet()
471
            ->children();
472
473
        $bcNode->variableNode('filters')
474
               ->info('Keep deprecated "filters" argument in collections')
475
               ->setDeprecated('v1.2')
476
               ->defaultFalse();
477
478
        $bcNode->variableNode('orderBy')
479
               ->info('Keep deprecated "orderBy" argument in collections')
480
               ->setDeprecated('v1.2')
481
               ->defaultFalse();
482
    }
483
}
484