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

FieldPluginBase::resolve()   C

Complexity

Conditions 8
Paths 13

Size

Total Lines 29
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 17
nc 13
nop 4
dl 0
loc 29
rs 5.3846
c 0
b 0
f 0
1
<?php
2
3
namespace Drupal\graphql\Plugin\GraphQL\Fields;
4
5
use Drupal\Component\Plugin\PluginBase;
6
use Drupal\Component\Render\MarkupInterface;
7
use Drupal\Core\Cache\CacheableDependencyInterface;
8
use Drupal\Core\Cache\CacheableMetadata;
9
use Drupal\Core\Render\RenderContext;
10
use Drupal\graphql\GraphQL\Execution\ResolveContext;
11
use Drupal\graphql\GraphQL\ValueWrapperInterface;
12
use Drupal\graphql\Plugin\FieldPluginInterface;
13
use Drupal\graphql\Plugin\FieldPluginManager;
14
use Drupal\graphql\Plugin\GraphQL\Traits\ArgumentAwarePluginTrait;
15
use Drupal\graphql\Plugin\GraphQL\Traits\CacheablePluginTrait;
16
use Drupal\graphql\Plugin\GraphQL\Traits\DeprecatablePluginTrait;
17
use Drupal\graphql\Plugin\GraphQL\Traits\DescribablePluginTrait;
18
use Drupal\graphql\Plugin\GraphQL\Traits\TypedPluginTrait;
19
use Drupal\graphql\Plugin\LanguageNegotiation\LanguageNegotiationGraphQL;
20
use Drupal\graphql\Plugin\SchemaBuilderInterface;
21
use GraphQL\Deferred;
22
use GraphQL\Type\Definition\ListOfType;
23
use GraphQL\Type\Definition\NonNull;
24
use GraphQL\Type\Definition\ResolveInfo;
25
26
abstract class FieldPluginBase extends PluginBase implements FieldPluginInterface {
27
  use CacheablePluginTrait;
28
  use DescribablePluginTrait;
29
  use TypedPluginTrait;
30
  use ArgumentAwarePluginTrait;
31
  use DeprecatablePluginTrait;
32
33
  /**
34
   * The language context service.
35
   *
36
   * @var \Drupal\graphql\GraphQLLanguageContext
37
   */
38
  protected $languageContext;
39
40
  /**
41
   * The renderer service.
42
   *
43
   * @var \Drupal\Core\Render\RendererInterface
44
   */
45
  protected $renderer;
46
47
  /**
48
   * {@inheritdoc}
49
   */
50
  public static function createInstance(SchemaBuilderInterface $builder, FieldPluginManager $manager, $definition, $id) {
51
    return [
52
      'description' => $definition['description'],
53
      'contexts' => $definition['contexts'],
54
      'deprecationReason' => $definition['deprecationReason'],
55
      'type' => $builder->processType($definition['type']),
56
      'args' => $builder->processArguments($definition['args']),
57
      'resolve' => function ($value, array $args, ResolveContext $context, ResolveInfo $info) use ($manager, $id) {
58
        $instance = $manager->getInstance(['id' => $id]);
59
        return $instance->resolve($value, $args, $context, $info);
60
      },
61
    ];
62
  }
63
64
  /**
65
   * Get the language context instance.
66
   *
67
   * @return \Drupal\graphql\GraphQLLanguageContext
68
   *   The language context service.
69
   */
70
  protected function getLanguageContext() {
71
    if (!isset($this->languageContext)) {
72
      $this->languageContext = \Drupal::service('graphql.language_context');
73
    }
74
    return $this->languageContext;
75
  }
76
77
  /**
78
   * Get the renderer service.
79
   *
80
   * @return \Drupal\Core\Render\RendererInterface
81
   */
82
  protected function getRenderer() {
83
    if (!isset($this->renderer)) {
84
      $this->renderer = \Drupal::service('renderer');
85
    }
86
    return $this->renderer;
87
  }
88
89
  /**
90
   * {@inheritdoc}
91
   */
92
  public function getDefinition() {
93
    $definition = $this->getPluginDefinition();
94
95
    return [
96
      'type' => $this->buildType($definition),
97
      'description' => $this->buildDescription($definition),
98
      'args' => $this->buildArguments($definition),
99
      'deprecationReason' => $this->buildDeprecationReason($definition),
100
      'contexts' => $this->buildCacheContexts($definition),
101
    ];
102
  }
103
104
  /**
105
   * {@inheritdoc}
106
   */
107
  public function resolve($value, array $args, ResolveContext $context, ResolveInfo $info) {
108
    $definition = $this->getPluginDefinition();
109
110
    // If not resolving in a trusted environment, check if the field is secure.
111
    if (!$context->getGlobal('development', FALSE) && !$context->getGlobal('bypass field security', FALSE)) {
112
      if (empty($definition['secure'])) {
113
        throw new \Exception(sprintf("Unable to resolve insecure field '%s'.", $info->fieldName));
114
      }
115
    }
116
117
    foreach ($definition['contextual_arguments'] as $argument) {
118
      if (array_key_exists($argument, $args) && !is_null($args[$argument])) {
119
        $context->setContext($argument, $args[$argument], $info);
120
      }
121
      else {
122
        $args[$argument] = $context->getContext($argument, $info);
123
      }
124
    }
125
126
    if ($this->isLanguageAwareField()) {
127
      return $this->getLanguageContext()
128
        ->executeInLanguageContext(function () use ($value, $args, $context, $info) {
129
          return $this->resolveDeferred([$this, 'resolveValues'], $value, $args, $context, $info);
130
        }, $context->getContext('language', $info));
131
    }
132
    else {
133
      return $this->resolveDeferred([$this, 'resolveValues'], $value, $args, $context, $info);
134
    }
135
  }
136
137
  /**
138
   * Indicator if the field is language aware.
139
   *
140
   * Checks for 'languages:*' cache contexts on the fields definition.
141
   *
142
   * @return bool
143
   *   The fields language awareness status.
144
   */
145
  protected function isLanguageAwareField() {
146
    return (boolean) count(array_filter($this->getPluginDefinition()['response_cache_contexts'], function ($context) {
147
      return strpos($context, 'languages:') === 0;
148
    }));
149
  }
150
151
  /**
152
   * {@inheritdoc}
153
   */
154
  protected function resolveDeferred(callable $callback, $value, array $args, ResolveContext $context, ResolveInfo $info) {    
155
    $renderContext = new RenderContext();
156
    return $this->getRenderer()->executeInRenderContext($renderContext, function () use ($value, $args, $context, $info) {
157
      $result = $callback($value, $args, $context, $info);
158
159
      if (is_callable($result)) {
160
        return new Deferred(function () use ($result, $value, $args, $context, $info) {
161
          return $this->resolveDeferred($result, $value, $args, $context, $info);
162
        });
163
      }
164
165
      $result = iterator_to_array($result);
166
167
      // Only collect cache metadata if this is a query. All other operation types
168
      // are not cacheable anyways.
169
      if ($info->operation->operation === 'query') {
170
        $dependencies = $this->getCacheDependencies($result, $value, $args, $context, $info);
171
        foreach ($dependencies as $dependency) {
172
          $context->addCacheableDependency($dependency);
173
        }
174
        if (!$renderContext->isEmpty()) {
175
          $context->addCacheableDependency($renderContext->pop());
176
        }
177
      }
178
179
      return $this->unwrapResult($result, $info);
180
    }
181
  }
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected '}', expecting ',' or ')'
Loading history...
182
183
  /**
184
   * Unwrap the resolved values.
185
   *
186
   * @param array $result
187
   *   The resolved values.
188
   * @param \GraphQL\Type\Definition\ResolveInfo $info
189
   *   The resolve info object.
190
   *
191
   * @return mixed
192
   *   The extracted values (an array of values in case this is a list, an
193
   *   arbitrary value if it isn't).
194
   */
195
  protected function unwrapResult($result, ResolveInfo $info) {
196
    $result = array_map(function ($item) {
197
      return $item instanceof ValueWrapperInterface ? $item->getValue() : $item;
198
    }, $result);
199
200
    $result = array_map(function ($item) {
201
      return $item instanceof MarkupInterface ? $item->__toString() : $item;
202
    }, $result);
203
204
    // If this is a list, return the result as an array.
205
    $type = $info->returnType;
206
    if ($type instanceof ListOfType || ($type instanceof NonNull && $type->getWrappedType() instanceof ListOfType)) {
207
      return $result;
208
    }
209
210
    return !empty($result) ? reset($result) : NULL;
211
  }
212
213
  /**
214
   * Retrieve the list of cache dependencies for a given value and arguments.
215
   *
216
   * @param array $result
217
   *   The result of the field.
218
   * @param mixed $parent
219
   *   The parent value.
220
   * @param array $args
221
   *   The arguments passed to the field.
222
   * @param \Drupal\graphql\GraphQL\Execution\ResolveContext $context
223
   *   The resolve context.
224
   * @param \GraphQL\Type\Definition\ResolveInfo $info
225
   *   The resolve info object.
226
   *
227
   * @return array
228
   *   A list of cacheable dependencies.
229
   */
230
  protected function getCacheDependencies(array $result, $parent, array $args, ResolveContext $context, ResolveInfo $info) {
231
    $self = new CacheableMetadata();
232
    $definition = $this->getPluginDefinition();
233
    if (!empty($definition['response_cache_contexts'])) {
234
      $self->addCacheContexts($definition['response_cache_contexts']);
235
    }
236
237
    if (!empty($definition['response_cache_tags'])) {
238
      $self->addCacheTags($definition['response_cache_tags']);
239
    }
240
241
    if (isset($definition['response_cache_max_age'])) {
242
      $self->mergeCacheMaxAge($definition['response_cache_max_age']);
243
    }
244
245
    return array_merge([$self], array_filter($result, function ($item) {
246
      return $item instanceof CacheableDependencyInterface;
247
    }));
248
  }
249
250
  /**
251
   * Retrieve the list of field values.
252
   *
253
   * Always returns a list of field values. Even for single value fields.
254
   * Single/multi field handling is responsibility of the base class.
255
   *
256
   * @param mixed $value
257
   *   The current object value.
258
   * @param array $args
259
   *   Field arguments.
260
   * @param $context
261
   *   The resolve context.
262
   * @param \GraphQL\Type\Definition\ResolveInfo $info
263
   *   The resolve info object.
264
   *
265
   * @return \Generator
266
   *   The value generator.
267
   */
268
  protected function resolveValues($value, array $args, ResolveContext $context, ResolveInfo $info) {
269
    // Allow overriding this class without having to declare this method.
270
    yield NULL;
271
  }
272
273
}
274