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

SchemaPluginBase::buildTypeMap()   B

Complexity

Conditions 5
Paths 1

Size

Total Lines 42
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

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