Completed
Pull Request — 8.x-3.x (#1144)
by
unknown
01:33
created

SchemaPluginBase::hasType()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 3
rs 10
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
    $processFields = array_map([$this, 'buildField'], $fields);
443
    foreach($processFields as $key => $processField) {
444
      if($processField === false) unset($processFields[$key]);
445
    }
446
    return $processFields;
447
  }
448
449
  /**
450
   * {@inheritdoc}
451
   */
452
  public function processArguments(array $args) {
453
    return array_filter(array_map(function ($arg) {
454
      try {
455
        $type = $this->processType($arg['type']);
456
      }
457
      catch (\Exception $e) {
458
        // Allow optional arguments that are removed if the input type is
459
        // not defined.
460
        if (empty($arg['optional'])) {
461
          throw $e;
462
        }
463
464
        return NULL;
465
      }
466
467
      return [
468
        'type' => $type,
469
      ] + $arg;
470
    }, $args));
471
  }
472
473
  /**
474
   * {@inheritdoc}
475
   */
476
  public function processType(array $type) {
477
    list($type, $decorators) = $type;
478
479
    return array_reduce($decorators, function ($type, $decorator) {
480
      return $decorator($type);
481
    }, $this->getType($type));
482
  }
483
484
  /**
485
   * Retrieves the type instance for the given reference.
486
   *
487
   * @param array $type
488
   *   The type reference.
489
   *
490
   * @return \GraphQL\Type\Definition\Type
491
   *   The type instance.
492
   */
493
  protected function buildType($type) {
494
    if (!isset($this->types[$type['id']])) {
495
      $creator = [$type['class'], 'createInstance'];
496
      $manager = $this->typeManagers->getTypeManager($type['type']);
497
      $this->types[$type['id']] = $creator($this, $manager, $type['definition'], $type['id']);
498
    }
499
500
    return $this->types[$type['id']];
501
  }
502
503
  /**
504
   * Retrieves the field definition for a given field reference.
505
   *
506
   * @param array $field
507
   *   The type reference.
508
   *
509
   * @return array
510
   *   The field definition.
511
   */
512
  protected function buildField($field) {
513
    if($field['definition']['type'][0] == "layout_section") return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by Drupal\graphql\Plugin\Gr...aPluginBase::buildField of type array.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
514
    if (!isset($this->fields[$field['id']])) {
515
      $creator = [$field['class'], 'createInstance'];
516
      $this->fields[$field['id']] = $creator($this, $this->fieldManager, $field['definition'], $field['id']);
517
    }
518
519
    return $this->fields[$field['id']];
520
  }
521
522
  /**
523
   * Retrieves the mutation definition for a given field reference.
524
   *
525
   * @param array $mutation
526
   *   The mutation reference.
527
   *
528
   * @return array
529
   *   The mutation definition.
530
   */
531 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...
532
    if (!isset($this->mutations[$mutation['id']])) {
533
      $creator = [$mutation['class'], 'createInstance'];
534
      $this->mutations[$mutation['id']] = $creator($this, $this->mutationManager, $mutation['definition'], $mutation['id']);
535
    }
536
537
    return $this->mutations[$mutation['id']];
538
  }
539
540
  /**
541
   * Retrieves the subscription definition for a given field reference.
542
   *
543
   * @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...
544
   *   The subscription reference.
545
   *
546
   * @return array
547
   *   The subscription definition.
548
   */
549 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...
550
    if (!isset($this->subscriptions[$subscription['id']])) {
551
      $creator = [$subscription['class'], 'createInstance'];
552
      $this->subscriptions[$subscription['id']] = $creator($this, $this->subscriptionManager, $subscription['definition'], $subscription['id']);
553
    }
554
555
    return $this->subscriptions[$subscription['id']];
556
  }
557
558
  /**
559
   * {@inheritdoc}
560
   */
561
  public function getCacheContexts() {
562
    return [];
563
  }
564
565
  /**
566
   * {@inheritdoc}
567
   */
568
  public function getCacheTags() {
569
    return $this->pluginDefinition['schema_cache_tags'];
570
  }
571
572
  /**
573
   * {@inheritdoc}
574
   */
575
  public function getCacheMaxAge() {
576
    return $this->pluginDefinition['schema_cache_max_age'];
577
  }
578
}
579