Completed
Pull Request — 8.x-3.x (#525)
by Sebastian
02:12
created

SchemaPluginBase::resolveType()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 8
nc 5
nop 4
dl 0
loc 16
rs 8.8571
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\Component\Utility\SortArray;
7
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
8
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
9
use Drupal\graphql\Plugin\FieldPluginManager;
10
use Drupal\graphql\Plugin\MutationPluginManager;
11
use Drupal\graphql\Plugin\SchemaBuilderInterface;
12
use Drupal\graphql\Plugin\SchemaPluginInterface;
13
use Drupal\graphql\Plugin\TypePluginManagerAggregator;
14
use GraphQL\Type\Definition\ObjectType;
15
use GraphQL\Type\Schema;
16
use GraphQL\Type\SchemaConfig;
17
use Symfony\Component\DependencyInjection\ContainerInterface;
18
19
abstract class SchemaPluginBase extends PluginBase implements SchemaPluginInterface, SchemaBuilderInterface, ContainerFactoryPluginInterface {
20
  use DependencySerializationTrait;
21
22
  /**
23
   * @var \Drupal\graphql\Plugin\FieldPluginManager
24
   */
25
  protected $fieldManager;
26
27
  /**
28
   * @var \Drupal\graphql\Plugin\MutationPluginManager
29
   */
30
  protected $mutationManager;
31
32
  /**
33
   * @var \Drupal\graphql\Plugin\TypePluginManagerAggregator
34
   */
35
  protected $typeManagers;
36
37
  /**
38
   * @var array
39
   */
40
  protected $typeMap;
41
42
  /**
43
   * @var array
44
   */
45
  protected $typeReferenceMap;
46
47
  /**
48
   * @var array
49
   */
50
  protected $fieldAssocationMap;
51
52
  /**
53
   * @var array
54
   */
55
  protected $typeAssocationMap;
56
57
  /**
58
   * @var array
59
   */
60
  protected $fieldMap;
61
62
  /**
63
   * @var array
64
   */
65
  protected $mutationMap;
66
67
  /**
68
   * @var array
69
   */
70
  protected $fields = [];
71
72
  /**
73
   * @var array
74
   */
75
  protected $mutations = [];
76
77
  /**
78
   * @var array
79
   */
80
  protected $types = [];
81
82
  /**
83
   * {@inheritdoc}
84
   */
85
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
86
    return new static(
87
      $configuration,
88
      $plugin_id,
89
      $plugin_definition,
90
      $container->get('plugin.manager.graphql.field'),
91
      $container->get('plugin.manager.graphql.mutation'),
92
      $container->get('graphql.type_manager_aggregator')
93
    );
94
  }
95
96
  /**
97
   * SchemaPluginBase constructor.
98
   *
99
   * @param array $configuration
100
   *   The plugin configuration array.
101
   * @param string $pluginId
102
   *   The plugin id.
103
   * @param array $pluginDefinition
104
   *   The plugin definition array.
105
   * @param \Drupal\graphql\Plugin\FieldPluginManager $fieldManager
106
   * @param \Drupal\graphql\Plugin\MutationPluginManager $mutationManager
107
   * @param \Drupal\graphql\Plugin\TypePluginManagerAggregator $typeManagers
108
   */
109
  public function __construct(
110
    $configuration,
111
    $pluginId,
112
    $pluginDefinition,
113
    FieldPluginManager $fieldManager,
114
    MutationPluginManager $mutationManager,
115
    TypePluginManagerAggregator $typeManagers
116
  ) {
117
    parent::__construct($configuration, $pluginId, $pluginDefinition);
118
119
    $this->fieldManager = $fieldManager;
120
    $this->mutationManager = $mutationManager;
121
    $this->typeManagers = $typeManagers;
122
  }
123
124
  /**
125
   * {@inheritdoc}
126
   */
127
  public function getSchema() {
128
    $config = new SchemaConfig();
129
130
    // Construct the optimized plugin definitions for building the schema.
131
    // TODO: Add caching.
132
    $this->typeMap = $this->buildTypeMap(iterator_to_array($this->typeManagers));
133
    $this->typeReferenceMap = $this->buildTypeReferenceMap($this->typeMap);
134
    $this->typeAssocationMap = $this->buildTypeAssociationMap($this->typeMap);
135
    $this->fieldAssocationMap = $this->buildFieldAssociationMap($this->fieldManager, $this->typeMap);
136
    $this->fieldMap = $this->buildFieldMap($this->fieldManager, $this->fieldAssocationMap);
137
    $this->mutationMap = $this->buildMutationMap($this->mutationManager);
138
139
    if ($this->hasMutations()) {
140
      $config->setMutation(new ObjectType([
141
        'name' => 'MutationRoot',
142
        'fields' => function () {
143
          return $this->getMutations();
144
        },
145
      ]));
146
    }
147
148
    $config->setQuery(new ObjectType([
149
      'name' => 'QueryRoot',
150
      'fields' => function () {
151
        return $this->getFields('Root');
152
      },
153
    ]));
154
155
    $config->setTypes(function () {
156
      return $this->getTypes();
157
    });
158
159
    $config->setTypeLoader(function ($name) {
160
      return $this->getType($name);
161
    });
162
163
    return new Schema($config);
164
  }
165
166
  /**
167
   * @return bool
168
   */
169
  public function hasFields($type) {
170
    return isset($this->fieldAssocationMap[$type]);
171
  }
172
173
  /**
174
   * @return bool
175
   */
176
  public function hasMutations() {
177
    return !empty($this->mutationMap);
178
  }
179
180
  /**
181
   * @return bool
182
   */
183
  public function hasType($name) {
184
    return isset($this->typeMap[$name]);
185
  }
186
187
  /**
188
   * @return array
189
   */
190
  public function getFields($parent) {
191
    if (isset($this->fieldAssocationMap[$parent])) {
192
      return $this->processFields(array_map(function ($id) {
193
        return $this->fieldMap[$id];
194
      }, $this->fieldAssocationMap[$parent]));
195
    }
196
197
    return [];
198
  }
199
200
  /**
201
   * @return array
202
   */
203
  public function getMutations() {
204
    return $this->processMutations($this->mutationMap);
205
  }
206
207
  /**
208
   * @return array
209
   */
210
  public function getTypes() {
211
    return array_map(function ($name) {
212
      return $this->getType($name);
213
    }, array_keys($this->typeMap));
214
  }
215
216
  /**
217
   * Retrieve the list of derivatives associated with a composite type.
218
   *
219
   * @return string[]
220
   *   The list of possible sub typenames.
221
   */
222
  public function getSubTypes($name) {
223
    return isset($this->typeAssocationMap[$name]) ? $this->typeAssocationMap[$name] : [];
224
  }
225
226
  /**
227
   * Resolve the matching type.
228
   */
229
  public function resolveType($name, $value, $context, $info) {
230
    if (!isset($this->typeAssocationMap[$name])) {
231
      return NULL;
232
    }
233
234
    foreach ($this->typeAssocationMap[$name] as $type) {
235
      // TODO: Avoid loading the type for the check. Make it static!
236
      if (isset($this->typeMap, $type) && $instance = $this->buildType($this->typeMap[$type])) {
237
        if ($instance->isTypeOf($value, $context, $info)) {
238
          return $instance;
239
        }
240
      }
241
    }
242
243
    return NULL;
244
  }
245
246
  /**
247
   * @param $name
248
   *
249
   * @return mixed
250
   */
251
  public function getType($name) {
252
    if (isset($this->typeMap[$name])) {
253
      return $this->buildType($this->typeMap[$name]);
254
    }
255
256
    do {
257
      if (isset($this->typeReferenceMap[$name])) {
258
        return $this->buildType($this->typeMap[$this->typeReferenceMap[$name]]);
259
      }
260
    } while (($pos = strpos($name, ':')) !== FALSE && $name = substr($name, 0, $pos));
261
262
    throw new \LogicException(sprintf('Missing type %s.', $name));
263
  }
264
265
  /**
266
   * @param $mutations
267
   *
268
   * @return array
269
   */
270
  public function processMutations($mutations) {
271
    return array_map([$this, 'buildMutation'], $mutations);
272
  }
273
274
  /**
275
   * @param $fields
276
   *
277
   * @return array
278
   */
279
  public function processFields($fields) {
280
    return array_map([$this, 'buildField'], $fields);
281
  }
282
283
  /**
284
   * @param $args
285
   *
286
   * @return array
287
   */
288
  public function processArguments($args) {
289
    return array_map(function ($arg) {
290
      return [
291
        'type' => $this->processType($arg['type']),
292
      ] + $arg;
293
    }, $args);
294
  }
295
296
  /**
297
   * @param $type
298
   *
299
   * @return mixed
300
   */
301
  public function processType($type) {
302
    list($type, $decorators) = $type;
303
304
    return array_reduce($decorators, function ($type, $decorator) {
305
      return $decorator($type);
306
    }, $this->getType($type));
307
  }
308
309
  /**
310
   * @param $type
311
   *
312
   * @return \Drupal\graphql\Plugin\GraphQL\Types\TypePluginBase
313
   */
314
  protected function buildType($type) {
315
    if (!isset($this->types[$type['id']])) {
316
      $creator = [$type['class'], 'createInstance'];
317
      $manager = $this->typeManagers->getTypeManager($type['type']);
318
      $this->types[$type['id']] = $creator($this, $manager, $type['definition'], $type['id']);
319
    }
320
321
    return $this->types[$type['id']];
322
  }
323
324
  /**
325
   * @param $field
326
   *
327
   * @return mixed
328
   */
329 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...
330
    if (!isset($this->fields[$field['id']])) {
331
      $creator = [$field['class'], 'createInstance'];
332
      $this->fields[$field['id']] = $creator($this, $this->fieldManager, $field['definition'], $field['id']);
333
    }
334
335
    return $this->fields[$field['id']];
336
  }
337
338
  /**
339
   * @param $mutation
340
   *
341
   * @return mixed
342
   */
343 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...
344
    if (!isset($this->mutations[$mutation['id']])) {
345
      $creator = [$mutation['class'], 'createInstance'];
346
      $this->mutations[$mutation['id']] = $creator($this, $this->mutationManager, $mutation['definition'], $mutation['id']);
347
    }
348
349
    return $this->mutations[$mutation['id']];
350
  }
351
352
  /**
353
   * @return array
354
   */
355
  protected function buildTypeMap(array $managers) {
356
    // First collect all definitions by their name, overwriting those with
357
    // lower weights by their higher weighted counterparts. We also collect
358
    // the class from the plugin definition to be able to statically create
359
    // the type instance without loading the plugin managers at all at
360
    // run-time.
361
    $types = array_reduce(array_keys($managers), function ($carry, $type) use ($managers) {
362
      $manager = $managers[$type];
363
      $definitions = $manager->getDefinitions();
364
365
      return array_reduce(array_keys($definitions), function ($carry, $id) use ($type, $definitions) {
366
        $current = $definitions[$id];
367
        $name = $current['name'];
368
369
        if (empty($carry[$name]) || $carry[$name]['weight'] < $current['weight']) {
370
          $carry[$name] = [
371
            'type' => $type,
372
            'id' => $id,
373
            'class' => $current['class'],
374
            'weight' => !empty($current['weight']) ? $current['weight'] : 0,
375
            'reference' => !empty($current['type']) ? $current['type'] : NULL,
376
          ];
377
        }
378
379
        return $carry;
380
      }, $carry);
381
    }, []);
382
383
    // Retrieve the plugins run-time definitions. These will help us to prevent
384
    // plugin instantiation at run-time unless a plugin is actually called from
385
    // the graphql query execution. Plugins should take care of not having to
386
    // instantiate their plugin instances during schema composition.
387
    return array_map(function ($type) use ($managers) {
388
      $manager = $managers[$type['type']];
389
      /** @var \Drupal\graphql\Plugin\TypePluginInterface $instance */
390
      $instance = $manager->getInstance(['id' => $type['id']]);
391
392
      return $type + [
393
        'definition' => $instance->getDefinition(),
394
      ] + $type;
395
    }, $types);
396
  }
397
398
  /**
399
   * @return array
400
   */
401
  protected function buildTypeReferenceMap(array $types) {
402 View Code Duplication
    $references = array_reduce(array_keys($types), function ($references, $name) use ($types) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
403
      $current = $types[$name];
404
      $reference = $current['reference'];
405
406
      if (!empty($reference) && (empty($references[$reference]) || $references[$reference]['weight'] < $current['weight'])) {
407
        $references[$reference] = [
408
          'name' => $name,
409
          'weight' => !empty($current['weight']) ? $current['weight'] : 0,
410
        ];
411
      }
412
413
      return $references;
414
    }, []);
415
416
    return array_map(function ($reference) {
417
      return $reference['name'];
418
    }, $references);
419
  }
420
421
  /**
422
   * @return array
423
   */
424
  protected function buildFieldAssociationMap(FieldPluginManager $manager, $types) {
425
    $definitions = $manager->getDefinitions();
426
427
    $fields = array_reduce(array_keys($definitions), function ($carry, $id) use ($definitions) {
428
      $current = $definitions[$id];
429
      $parents = $current['parents'] ?: ['Root'];
430
431
      return array_reduce($parents, function ($carry, $parent) use ($current, $id) {
432
        // Allow plugins to define a different name for each parent.
433
        if (strpos($parent, ':') !== FALSE) {
434
          list($parent, $name) = explode(':', $parent);
435
        }
436
437
        $name = isset($name) ? $name : $current['name'];
438
        if (empty($carry[$parent][$name]) || $carry[$parent][$name]['weight'] < $current['weight']) {
439
          $carry[$parent][$name] = [
440
            'id' => $id,
441
            'weight' => !empty($current['weight']) ? $current['weight'] : 0,
442
          ];
443
        }
444
445
        return $carry;
446
      }, $carry);
447
    }, []);
448
449
    // Only return fields for types that are actually fieldable.
450
    $fieldable = [GRAPHQL_TYPE_PLUGIN, GRAPHQL_INTERFACE_PLUGIN];
451
    $fields = array_intersect_key($fields, array_filter($types, function ($type) use ($fieldable) {
452
      return in_array($type['type'], $fieldable);
453
    }) + ['Root' => NULL]);
454
455
    // We only need the plugin ids in this map.
456
    return array_map(function ($fields) {
457
      return array_map(function ($field) {
458
        return $field['id'];
459
      }, $fields);
460
    }, $fields);
461
  }
462
463
  /**
464
   * @return array
465
   */
466
  protected function buildTypeAssociationMap(array $types) {
467
    $assocations = array_filter(array_map(function ($type) use ($types) {
468
      // If this is an object type, just return a mapping for it's interfaces.
469
      if ($type['type'] === 'type') {
470
        return array_map(function () use ($type) {
471
          return [$type['definition']['name']];
472
        }, array_flip($type['definition']['interfaces']));
473
      }
474
475
      // For interfaces, find all object types that declare to implement it.
476
      if ($type['type'] === 'interface') {
477
        return [$type['definition']['name'] => array_values(array_map(function ($type) {
478
          return $type['definition']['name'];
479 View Code Duplication
        }, array_filter($types, function ($subType) use ($type) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
480
          return $subType['type'] === 'type' && in_array($type['definition']['name'], $subType['definition']['interfaces']);
481
        })))];
482
      }
483
484
      // Union types combine the two approaches above.
485
      if ($type['type'] === 'union') {
486
        $explicit = $type['definition']['types'];
487
488
        $implicit = array_values(array_map(function ($type) {
489
          return $type['definition']['name'];
490 View Code Duplication
        }, array_filter($types, function ($subType) use ($type) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
491
          return $subType['type'] === 'type' && in_array($type['definition']['name'], $subType['definition']['unions']);
492
        })));
493
494
        return [$type['definition']['name'] => array_merge($explicit, $implicit)];
495
      }
496
497
      return [];
498
    }, $types));
499
500
    $assocations = array_map('array_unique', array_reduce($assocations, 'array_merge_recursive', []));
501
    $assocations = array_map(function ($parent) use ($types) {
502
      $children = array_map(function ($child) use ($types) {
503
        return $types[$child] + ['name' => $child];
504
      }, $parent);
505
506
      uasort($children,[SortArray::class, 'sortByWeightElement']);
507
      $children = array_reverse($children);
508
509
      return array_map(function ($child) {
510
        return $child['name'];
511
      }, $children);
512
    }, $assocations);
513
514
    return $assocations;
515
  }
516
517
  /**
518
   * @return array
519
   */
520
  protected function buildFieldMap(FieldPluginManager $manager, $association) {
521
    return array_reduce($association, function ($carry, $fields) use ($manager) {
522
      return array_reduce($fields, function ($carry, $id) use ($manager) {
523
        if (!isset($carry[$id])) {
524
          $instance = $manager->getInstance(['id' => $id]);
525
          $definition = $manager->getDefinition($id);
526
527
          $carry[$id] = [
528
            'id' => $id,
529
            'class' => $definition['class'],
530
            'definition' => $instance->getDefinition(),
531
          ];
532
        }
533
534
        return $carry;
535
      }, $carry);
536
    }, []);
537
  }
538
539
  /**
540
   * @return array
541
   */
542
  protected function buildMutationMap(MutationPluginManager $manager) {
543
    $definitions = $manager->getDefinitions();
544 View Code Duplication
    $mutations = array_reduce(array_keys($definitions), function ($carry, $id) use ($definitions) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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
      $current = $definitions[$id];
546
      $name = $current['name'];
547
548
      if (empty($carry[$name]) || $carry[$name]['weight'] < $current['weight']) {
549
        $carry[$name] = [
550
          'id' => $id,
551
          'class' => $current['class'],
552
          'weight' => !empty($current['weight']) ? $current['weight'] : 0,
553
        ];
554
      }
555
556
      return $carry;
557
    }, []);
558
559
    return array_map(function ($definition) use ($manager) {
560
      $id = $definition['id'];
561
      $instance = $manager->getInstance(['id' => $id]);
562
563
      return [
564
        'definition' => $instance->getDefinition(),
565
      ] + $definition;
566
    }, $mutations);
567
  }
568
}
569