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

SchemaBuilder::buildField()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 8
Ratio 100 %

Importance

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