Completed
Push — 8.x-3.x ( 0f7d85...eb3bcc )
by Sebastian
01:31
created

SchemaPluginBase::getFields()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 1
dl 0
loc 12
rs 9.8666
c 0
b 0
f 0
1
<?php
2
3
namespace Drupal\graphql\Plugin\GraphQL\Schemas;
4
5
use Drupal\Component\Plugin\PluginBase;
6
use Drupal\Core\Cache\CacheableDependencyInterface;
7
use Drupal\Core\Language\LanguageManagerInterface;
8
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
9
use Drupal\Core\Session\AccountProxyInterface;
10
use Drupal\graphql\GraphQL\Execution\ResolveContext;
11
use Drupal\graphql\GraphQL\QueryProvider\QueryProviderInterface;
12
use Drupal\graphql\Plugin\FieldPluginManager;
13
use Drupal\graphql\Plugin\MutationPluginManager;
14
use Drupal\graphql\Plugin\SubscriptionPluginManager;
15
use Drupal\graphql\Plugin\SchemaBuilderInterface;
16
use Drupal\graphql\Plugin\SchemaPluginInterface;
17
use Drupal\graphql\Plugin\TypePluginManagerAggregator;
18
use GraphQL\Language\AST\DocumentNode;
19
use GraphQL\Server\OperationParams;
20
use GraphQL\Server\ServerConfig;
21
use GraphQL\Type\Definition\ObjectType;
22
use GraphQL\Type\Definition\ResolveInfo;
23
use GraphQL\Type\Schema;
24
use GraphQL\Type\SchemaConfig;
25
use GraphQL\Validator\DocumentValidator;
26
use Psr\Log\LoggerInterface;
27
use Symfony\Component\DependencyInjection\ContainerInterface;
28
29
abstract class SchemaPluginBase extends PluginBase implements SchemaPluginInterface, SchemaBuilderInterface, ContainerFactoryPluginInterface, CacheableDependencyInterface {
30
31
  /**
32
   * The field plugin manager.
33
   *
34
   * @var \Drupal\graphql\Plugin\FieldPluginManager
35
   */
36
  protected $fieldManager;
37
38
  /**
39
   * The mutation plugin manager.
40
   *
41
   * @var \Drupal\graphql\Plugin\MutationPluginManager
42
   */
43
  protected $mutationManager;
44
45
  /**
46
   * The subscription plugin manager.
47
   *
48
   * @var \Drupal\graphql\Plugin\SubscriptionPluginManager
49
   */
50
  protected $subscriptionManager;
51
52
  /**
53
   * The type manager aggregator service.
54
   *
55
   * @var \Drupal\graphql\Plugin\TypePluginManagerAggregator
56
   */
57
  protected $typeManagers;
58
59
  /**
60
   * Static cache of field definitions.
61
   *
62
   * @var array
63
   */
64
  protected $fields = [];
65
66
  /**
67
   * Static cache of mutation definitions.
68
   *
69
   * @var array
70
   */
71
  protected $mutations = [];
72
73
  /**
74
   * Static cache of subscription definitions.
75
   *
76
   * @var array
77
   */
78
  protected $subscriptions = [];
79
80
  /**
81
   * Static cache of type instances.
82
   *
83
   * @var array
84
   */
85
  protected $types = [];
86
87
  /**
88
   * The service parameters
89
   *
90
   * @var array
91
   */
92
  protected $parameters;
93
94
  /**
95
   * The query provider service.
96
   *
97
   * @var \Drupal\graphql\GraphQL\QueryProvider\QueryProviderInterface
98
   */
99
  protected $queryProvider;
100
101
  /**
102
   * The current user.
103
   *
104
   * @var \Drupal\Core\Session\AccountProxyInterface
105
   */
106
  protected $currentUser;
107
108
  /**
109
   * The logger service.
110
   *
111
   * @var \Psr\Log\LoggerInterface
112
   */
113
  protected $logger;
114
115
  /**
116
   * @var \Drupal\Core\Language\LanguageManagerInterface
117
   */
118
  protected $languageManager;
119
120
  /**
121
   * {@inheritdoc}
122
   */
123
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
124
    return new static(
125
      $configuration,
126
      $plugin_id,
127
      $plugin_definition,
128
      $container->get('plugin.manager.graphql.field'),
129
      $container->get('plugin.manager.graphql.mutation'),
130
      $container->get('plugin.manager.graphql.subscription'),
131
      $container->get('graphql.type_manager_aggregator'),
132
      $container->get('graphql.query_provider'),
133
      $container->get('current_user'),
134
      $container->get('logger.channel.graphql'),
135
      $container->get('language_manager'),
136
      $container->getParameter('graphql.config')
137
    );
138
  }
139
140
  /**
141
   * SchemaPluginBase constructor.
142
   *
143
   * @param array $configuration
144
   *   The plugin configuration array.
145
   * @param string $pluginId
146
   *   The plugin id.
147
   * @param array $pluginDefinition
148
   *   The plugin definition array.
149
   * @param \Drupal\graphql\Plugin\FieldPluginManager $fieldManager
150
   *   The field plugin manager.
151
   * @param \Drupal\graphql\Plugin\MutationPluginManager $mutationManager
152
   *   The mutation plugin manager.
153
   * @param \Drupal\graphql\Plugin\SubscriptionPluginManager $subscriptionManager
154
   *   The subscription plugin manager.
155
   * @param \Drupal\graphql\Plugin\TypePluginManagerAggregator $typeManagers
156
   *   The type manager aggregator service.
157
   * @param \Drupal\graphql\GraphQL\QueryProvider\QueryProviderInterface $queryProvider
158
   *   The query provider service.
159
   * @param \Drupal\Core\Session\AccountProxyInterface $currentUser
160
   *   The current user.
161
   * @param \Psr\Log\LoggerInterface $logger
162
   *   The logger service.
163
   * @param \Drupal\Core\Language\LanguageManagerInterface $languageManager
164
   *   The language manager service.
165
   * @param array $parameters
166
   *   The service parameters.
167
   */
168
  public function __construct(
169
    $configuration,
170
    $pluginId,
171
    $pluginDefinition,
172
    FieldPluginManager $fieldManager,
173
    MutationPluginManager $mutationManager,
174
    SubscriptionPluginManager $subscriptionManager,
175
    TypePluginManagerAggregator $typeManagers,
176
    QueryProviderInterface $queryProvider,
177
    AccountProxyInterface $currentUser,
178
    LoggerInterface $logger,
179
    LanguageManagerInterface $languageManager,
180
    array $parameters
181
  ) {
182
    parent::__construct($configuration, $pluginId, $pluginDefinition);
183
    $this->fieldManager = $fieldManager;
184
    $this->mutationManager = $mutationManager;
185
    $this->subscriptionManager = $subscriptionManager;
186
    $this->typeManagers = $typeManagers;
187
    $this->queryProvider = $queryProvider;
188
    $this->currentUser = $currentUser;
189
    $this->parameters = $parameters;
190
    $this->logger = $logger;
191
    $this->languageManager = $languageManager;
192
  }
193
194
  /**
195
   * {@inheritdoc}
196
   */
197
  public function getSchema() {
198
    $config = new SchemaConfig();
199
200
    if ($this->hasMutations()) {
201
      $config->setMutation(new ObjectType([
202
        'name' => 'Mutation',
203
        'fields' => function () {
204
          return $this->getMutations();
205
        },
206
      ]));
207
    }
208
209
    if ($this->hasSubscriptions()) {
210
      $config->setSubscription(new ObjectType([
211
        'name' => 'Subscription',
212
        'fields' => function () {
213
          return $this->getSubscriptions();
214
        },
215
      ]));
216
    }
217
218
    $config->setQuery(new ObjectType([
219
      'name' => 'Query',
220
      'fields' => function () {
221
        return $this->getFields('Root');
222
      },
223
    ]));
224
225
    $config->setTypes(function () {
226
      return $this->getTypes();
227
    });
228
229
    $config->setTypeLoader(function ($name) {
230
      return $this->getType($name);
231
    });
232
233
    return new Schema($config);
234
  }
235
236
  /**
237
   * {@inheritdoc}
238
   */
239
  public function validateSchema() {
240
    return NULL;
241
  }
242
243
  /**
244
   * {@inheritdoc}
245
   */
246
  public function getServer() {
247
    // If the current user has appropriate permissions, allow to bypass
248
    // the secure fields restriction.
249
    $globals['bypass field security'] = $this->currentUser->hasPermission('bypass graphql field security');
0 ignored issues
show
Coding Style Comprehensibility introduced by
$globals was never initialized. Although not strictly required by PHP, it is generally a good practice to add $globals = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
250
251
    // Create the server config.
252
    $config = ServerConfig::create();
253
254
    // Each document (e.g. in a batch query) gets its own resolve context. This
255
    // allows us to collect the cache metadata and contextual values (e.g.
256
    // inheritance for language) for each query separately.
257
    $config->setContext(function ($params, $document, $operation) use ($globals) {
0 ignored issues
show
Unused Code introduced by
The parameter $params is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $document is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $operation is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
258
      // Each document (e.g. in a batch query) gets its own resolve context. This
259
      // allows us to collect the cache metadata and contextual values (e.g.
260
      // inheritance for language) for each query separately.
261
      $context = new ResolveContext($globals, [
262
        'language' => $this->languageManager->getCurrentLanguage()->getId(),
263
      ]);
264
265
      $context->addCacheTags(['graphql']);
266
267
      // Always add the language_url cache context.
268
      $context->addCacheContexts([
269
        'languages:language_url',
270
        'languages:language_interface',
271
        'languages:language_content',
272
        'user.permissions',
273
      ]);
274
275
      return $context;
276
    });
277
278
    $config->setValidationRules(function (OperationParams $params, DocumentNode $document, $operation) {
0 ignored issues
show
Unused Code introduced by
The parameter $document is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $operation is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
279
      if (isset($params->queryId) && empty($params->getOriginalInput('query'))) {
280
        // Assume that pre-parsed documents are already validated. This allows
281
        // us to store pre-validated query documents e.g. for persisted queries
282
        // effectively improving performance by skipping run-time validation.
283
        return [];
284
      }
285
286
      return array_values(DocumentValidator::defaultRules());
287
    });
288
289
    $config->setPersistentQueryLoader([$this->queryProvider, 'getQuery']);
290
    $config->setQueryBatching(TRUE);
291
    $config->setDebug(!!$this->parameters['development']);
292
    $config->setSchema($this->getSchema());
293
294
    // Always log the errors.
295
    $config->setErrorsHandler(function (array $errors, callable $formatter) {
296
      /** @var \GraphQL\Error\Error $error */
297
      foreach ($errors as $error) {
298
        $this->logger->error($error->getMessage());
299
      }
300
301
      return array_map($formatter, $errors);
302
    });
303
304
    return $config;
305
  }
306
307
  /**
308
  /**
309
   * {@inheritdoc}
310
   */
311
  public function hasFields($type) {
312
    return isset($this->pluginDefinition['field_association_map'][$type]);
313
  }
314
315
  /**
316
   * {@inheritdoc}
317
   */
318
  public function hasMutations() {
319
    return !empty($this->pluginDefinition['mutation_map']);
320
  }
321
322
  /**
323
   * {@inheritdoc}
324
   */
325
  public function hasSubscriptions() {
326
    return !empty($this->pluginDefinition['subscription_map']);
327
  }
328
329
  /**
330
   * {@inheritdoc}
331
   */
332
  public function hasType($name) {
333
    return isset($this->pluginDefinition['type_map'][$name]);
334
  }
335
336
  /**
337
   * {@inheritdoc}
338
   */
339
  public function getFields($type) {
340
    $association = $this->pluginDefinition['field_association_map'];
341
    $fields = $this->pluginDefinition['field_map'];
342
343
    if (isset($association[$type])) {
344
      return $this->processFields(array_map(function ($id) use ($fields) {
345
        return $fields[$id];
346
      }, $association[$type]));
347
    }
348
349
    return [];
350
  }
351
352
  /**
353
   * {@inheritdoc}
354
   */
355
  public function getMutations() {
356
    return $this->processMutations($this->pluginDefinition['mutation_map']);
357
  }
358
359
  /**
360
   * {@inheritdoc}
361
   */
362
  public function getSubscriptions() {
363
    return $this->processSubscriptions($this->pluginDefinition['subscription_map']);
364
  }
365
366
  /**
367
   * {@inheritdoc}
368
   */
369
  public function getTypes() {
370
    return array_map(function ($name) {
371
      return $this->getType($name);
372
    }, array_keys($this->pluginDefinition['type_map']));
373
  }
374
375
  /**
376
   * {@inheritdoc}
377
   */
378
  public function getSubTypes($name) {
379
    $association = $this->pluginDefinition['type_association_map'];
380
    return isset($association[$name]) ? $association[$name] : [];
381
  }
382
383
  /**
384
   * {@inheritdoc}
385
   */
386
  public function resolveType($name, $value, ResolveContext $context, ResolveInfo $info) {
387
    $association = $this->pluginDefinition['type_association_map'];
388
    $types = $this->pluginDefinition['type_map'];
389
    if (!isset($association[$name])) {
390
      return NULL;
391
    }
392
393
    foreach ($association[$name] as $type) {
394
      // TODO: Try to avoid loading the type for the check. Consider to make it static!
395
      if (isset($types[$type]) && $instance = $this->buildType($types[$type])) {
396
        if ($instance->isTypeOf($value, $context, $info)) {
0 ignored issues
show
Bug introduced by
The method isTypeOf() does not exist on GraphQL\Type\Definition\Type. Did you maybe mean isType()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
397
          return $instance;
398
        }
399
      }
400
    }
401
402
    return NULL;
403
  }
404
405
  /**
406
   * {@inheritdoc}
407
   */
408
  public function getType($name) {
409
    $types = $this->pluginDefinition['type_map'];
410
    $references = $this->pluginDefinition['type_reference_map'];
411
    if (isset($types[$name])) {
412
      return $this->buildType($this->pluginDefinition['type_map'][$name]);
413
    }
414
415
    do {
416
      if (isset($references[$name])) {
417
        return $this->buildType($types[$references[$name]]);
418
      }
419
    } while (($pos = strpos($name, ':')) !== FALSE && $name = substr($name, 0, $pos));
420
421
    throw new \LogicException(sprintf('Missing type %s.', $name));
422
  }
423
424
  /**
425
   * {@inheritdoc}
426
   */
427
  public function processMutations(array $mutations) {
428
    return array_map([$this, 'buildMutation'], $mutations);
429
  }
430
431
  /**
432
   * {@inheritdoc}
433
   */
434
  public function processSubscriptions(array $subscriptions) {
435
    return array_map([$this, 'buildSubscription'], $subscriptions);
436
  }
437
438
  /**
439
   * {@inheritdoc}
440
   */
441
  public function processFields(array $fields) {
442
    return array_map([$this, 'buildField'], $fields);
443
  }
444
445
  /**
446
   * {@inheritdoc}
447
   */
448
  public function processArguments(array $args) {
449
    return array_filter(array_map(function ($arg) {
450
      try {
451
        $type = $this->processType($arg['type']);
452
      }
453
      catch (\Exception $e) {
454
        // Allow optional arguments that are removed if the input type is
455
        // not defined.
456
        if (empty($arg['optional'])) {
457
          throw $e;
458
        }
459
460
        return NULL;
461
      }
462
463
      return [
464
        'type' => $type,
465
      ] + $arg;
466
    }, $args));
467
  }
468
469
  /**
470
   * {@inheritdoc}
471
   */
472
  public function processType(array $type) {
473
    list($type, $decorators) = $type;
474
475
    return array_reduce($decorators, function ($type, $decorator) {
476
      return $decorator($type);
477
    }, $this->getType($type));
478
  }
479
480
  /**
481
   * Retrieves the type instance for the given reference.
482
   *
483
   * @param array $type
484
   *   The type reference.
485
   *
486
   * @return \GraphQL\Type\Definition\Type
487
   *   The type instance.
488
   */
489
  protected function buildType($type) {
490
    if (!isset($this->types[$type['id']])) {
491
      $creator = [$type['class'], 'createInstance'];
492
      $manager = $this->typeManagers->getTypeManager($type['type']);
493
      $this->types[$type['id']] = $creator($this, $manager, $type['definition'], $type['id']);
494
    }
495
496
    return $this->types[$type['id']];
497
  }
498
499
  /**
500
   * Retrieves the field definition for a given field reference.
501
   *
502
   * @param array $field
503
   *   The type reference.
504
   *
505
   * @return array
506
   *   The field definition.
507
   */
508 View Code Duplication
  protected function buildField($field) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
509
    if (!isset($this->fields[$field['id']])) {
510
      $creator = [$field['class'], 'createInstance'];
511
      $this->fields[$field['id']] = $creator($this, $this->fieldManager, $field['definition'], $field['id']);
512
    }
513
514
    return $this->fields[$field['id']];
515
  }
516
517
  /**
518
   * Retrieves the mutation definition for a given field reference.
519
   *
520
   * @param array $mutation
521
   *   The mutation reference.
522
   *
523
   * @return array
524
   *   The mutation definition.
525
   */
526 View Code Duplication
  protected function buildMutation($mutation) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
527
    if (!isset($this->mutations[$mutation['id']])) {
528
      $creator = [$mutation['class'], 'createInstance'];
529
      $this->mutations[$mutation['id']] = $creator($this, $this->mutationManager, $mutation['definition'], $mutation['id']);
530
    }
531
532
    return $this->mutations[$mutation['id']];
533
  }
534
535
  /**
536
   * Retrieves the subscription definition for a given field reference.
537
   *
538
   * @param array $mutation
0 ignored issues
show
Bug introduced by
There is no parameter named $mutation. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
539
   *   The subscription reference.
540
   *
541
   * @return array
542
   *   The subscription definition.
543
   */
544 View Code Duplication
  protected function buildSubscription($subscription) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
545
    if (!isset($this->subscriptions[$subscription['id']])) {
546
      $creator = [$subscription['class'], 'createInstance'];
547
      $this->subscriptions[$subscription['id']] = $creator($this, $this->subscriptionManager, $subscription['definition'], $subscription['id']);
548
    }
549
550
    return $this->subscriptions[$subscription['id']];
551
  }
552
553
  /**
554
   * {@inheritdoc}
555
   */
556
  public function getCacheContexts() {
557
    return [];
558
  }
559
560
  /**
561
   * {@inheritdoc}
562
   */
563
  public function getCacheTags() {
564
    return $this->pluginDefinition['schema_cache_tags'];
565
  }
566
567
  /**
568
   * {@inheritdoc}
569
   */
570
  public function getCacheMaxAge() {
571
    return $this->pluginDefinition['schema_cache_max_age'];
572
  }
573
}
574