Completed
Pull Request — 8.x-3.x (#525)
by Philipp
03:19
created

SchemaBuilder::buildTypeReferenceMap()   B

Complexity

Conditions 5
Paths 1

Size

Total Lines 20
Code Lines 13

Duplication

Lines 13
Ratio 65 %

Importance

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