Completed
Pull Request — 8.x-3.x (#442)
by Sebastian
01:51
created

SchemaLoader::extractResponseCacheMetadata()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 8

Duplication

Lines 12
Ratio 100 %

Importance

Changes 0
Metric Value
cc 1
eloc 8
nc 1
nop 1
dl 12
loc 12
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace Drupal\graphql\GraphQL\Schema;
4
5
use Drupal\graphql\GraphQL\Utility\TypeCollector;
6
use Drupal\Core\Cache\Cache;
7
use Drupal\Core\Cache\CacheableDependencyInterface;
8
use Drupal\Core\Cache\CacheableMetadata;
9
use Drupal\Core\Cache\CacheBackendInterface;
10
use Drupal\Core\Cache\Context\CacheContextsManager;
11
use Drupal\graphql\Plugin\GraphQL\SchemaPluginManager;
12
use Drupal\graphql\Plugin\GraphQL\TypeSystemPluginInterface;
13
use Drupal\graphql\Plugin\GraphQL\TypeSystemPluginReferenceInterface;
14
use Symfony\Component\HttpFoundation\RequestStack;
15
use Youshido\GraphQL\Schema\AbstractSchema;
16
use Youshido\GraphQL\Type\InputObject\AbstractInputObjectType;
17
use Youshido\GraphQL\Type\InterfaceType\AbstractInterfaceType;
18
use Youshido\GraphQL\Type\Object\AbstractObjectType;
19
20
/**
21
 * Loads and caches a generated GraphQL schema.
22
 */
23
class SchemaLoader {
24
25
  /**
26
   * The cache contexts manager service.
27
   *
28
   * @var \Drupal\Core\Cache\Context\CacheContextsManager
29
   */
30
  protected $contextsManager;
31
32
  /**
33
   * The schema plugin manager service.
34
   *
35
   * @var \Drupal\graphql\Plugin\GraphQL\SchemaPluginManager
36
   */
37
  protected $schemaManager;
38
39
  /**
40
   * The schema cache backend.
41
   *
42
   * @var \Drupal\Core\Cache\CacheBackendInterface
43
   */
44
  protected $schemaCache;
45
46
  /**
47
   * The cache metadata cache.
48
   *
49
   * @var \Drupal\Core\Cache\CacheBackendInterface
50
   */
51
  protected $metadataCache;
52
53
  /**
54
   * The service configuration.
55
   *
56
   * @var array
57
   */
58
  protected $config;
59
60
  /**
61
   * The request stack service.
62
   *
63
   * @var \Symfony\Component\HttpFoundation\RequestStack
64
   */
65
  protected $requestStack;
66
67
  /**
68
   * Static cache of loaded schemas.
69
   *
70
   * @var \Drupal\graphql\Plugin\GraphQL\SchemaPluginInterface[]
71
   */
72
  protected $schemas = [];
73
74
  /**
75
   * Static cache of loaded cache metadata.
76
   *
77
   * @var \Drupal\Core\Cache\RefinableCacheableDependencyInterface[]
78
   */
79
  protected $metadata = [];
80
81
  /**
82
   * Constructs a SchemaLoader object.
83
   *
84
   * @param \Drupal\Core\Cache\Context\CacheContextsManager $contextsManager
85
   *   The cache contexts manager service.
86
   * @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
87
   *   The request stack service.
88
   * @param \Drupal\graphql\Plugin\GraphQL\SchemaPluginManager $schemaManager
89
   *   The schema plugin manager service.
90
   * @param \Drupal\Core\Cache\CacheBackendInterface $schemaCache
91
   *   The schema cache backend.
92
   * @param \Drupal\Core\Cache\CacheBackendInterface $metadataCache
93
   *   The metadata cache backend.
94
   * @param array $config
95
   *   The configuration provided through the services.yml.
96
   */
97
  public function __construct(
98
    CacheContextsManager $contextsManager,
99
    RequestStack $requestStack,
100
    SchemaPluginManager $schemaManager,
101
    CacheBackendInterface $schemaCache,
102
    CacheBackendInterface $metadataCache,
103
    array $config
104
  ) {
105
    $this->config = $config;
106
    $this->schemaManager = $schemaManager;
107
    $this->contextsManager = $contextsManager;
108
    $this->schemaCache = $schemaCache;
109
    $this->metadataCache = $metadataCache;
110
    $this->requestStack = $requestStack;
111
  }
112
113
  /**
114
   * Loads and caches the generated schema.
115
   *
116
   * @param string $name
117
   *   The name of the schema to load.
118
   *
119
   * @return \Drupal\graphql\Plugin\GraphQL\SchemaPluginInterface
120
   *   The generated GraphQL schema.
121
   */
122
  public function getSchema($name) {
123
    if (array_key_exists($name, $this->schemas)) {
124
      return $this->schemas[$name];
125
    }
126
127
    // The cache key is made up of all of the globally known cache contexts.
128
    if (!empty($this->config['schema_cache'])) {
129
      if (($contextCache = $this->metadataCache->get("$name:schema")) && $contextCache->data) {
130
        $cid = $this->getCacheIdentifier($name, $contextCache->data);
131
132
        if (($schema = $this->schemaCache->get($cid)) && $schema->data) {
133
          return $this->schemas[$name] = $schema->data;
134
        }
135
      }
136
    }
137
138
    $this->schemas[$name] = $this->schemaManager->createInstance($name)->getSchema();
139
    // If the schema is not cacheable, just return it directly.
140
    if (empty($this->config['schema_cache'])) {
141
      return $this->schemas[$name];
142
    }
143
144
    // Compute the cache identifier, tag and expiry time.
145
    $schemaCacheMetadata = $this->getSchemaCacheMetadata($name);
146
    if ($schemaCacheMetadata->getCacheMaxAge() !== 0) {
147
      $tags = $schemaCacheMetadata->getCacheTags();
148
      $expire = $this->maxAgeToExpire($schemaCacheMetadata->getCacheMaxAge());
149
      $cid = $this->getCacheIdentifier($name, $schemaCacheMetadata);
150
151
      // Write the cache entry for the schema cache entries.
152
      $this->schemaCache->set($cid, $this->schemas[$name], $expire, $tags);
153
    }
154
155
    return $this->schemas[$name];
156
  }
157
158
  /**
159
   * Retrieves the schema's cache metadata.
160
   *
161
   * @param string $name
162
   *   The name of the schema.
163
   * @return \Drupal\Core\Cache\CacheableDependencyInterface
164
   *   The cache metadata for the schema.
165
   */
166
  public function getSchemaCacheMetadata($name) {
167
    return $this->getCacheMetadata($name, "$name:schema", function (AbstractSchema $schema) {
168
      return $this->extractSchemaCacheMetadata($schema);
169
    });
170
  }
171
172
  /**
173
   * Retrieves the schema's response cache metadata.
174
   *
175
   * @param string $name
176
   *   The name of the schema.
177
   * @return \Drupal\Core\Cache\RefinableCacheableDependencyInterface
178
   *   The cache metadata for the schema's responses.
179
   */
180
  public function getResponseCacheMetadata($name) {
181
    return $this->getCacheMetadata($name, "$name:response", function (AbstractSchema $schema) {
182
      return $this->extractResponseCacheMetadata($schema);
183
    })->addCacheableDependency($this->getSchemaCacheMetadata($name));
184
  }
185
186
  /**
187
   * Helper function to load cache metadata from a schema.
188
   *
189
   * @param string $name
190
   *   The name of the schema.
191
   * @param string $cid
192
   *   The cache identifier for caching the metadata
193
   * @param callable $callback
194
   *   Callback to return the cache metadata from the schema.
195
   *
196
   * @return \Drupal\Core\Cache\RefinableCacheableDependencyInterface
197
   *   The cache metadata.
198
   */
199
  protected function getCacheMetadata($name, $cid, callable $callback) {
200
    if (array_key_exists($cid, $this->metadata)) {
201
      return $this->metadata[$cid];
202
    }
203
204
    // The cache key is made up of all of the globally known cache contexts.
205
    if (!empty($this->config['schema_cache'])) {
206
      if (($metadataCache = $this->metadataCache->get($cid)) && $metadataCache->data) {
207
        return $this->metadata[$name] = $metadataCache->data;
208
      }
209
    }
210
211
    /** @var \Drupal\Core\Cache\RefinableCacheableDependencyInterface $metadata */
212
    $schema = $this->getSchema($name);
213
    $metadata = $callback($schema);
214
    $this->metadata[$cid] = $metadata;
215
    if (empty($this->config['schema_cache'])) {
216
      return $this->metadata[$cid];
217
    }
218
219
    // Use the schema cache metadata to determine cache expiry and tags.
220
    $schemaCacheMetadata = $this->getSchemaCacheMetadata($name);
221
    if ($schemaCacheMetadata->getCacheMaxAge() !== 0) {
222
      $tags = $schemaCacheMetadata->getCacheTags();
223
      $expire = $this->maxAgeToExpire($schemaCacheMetadata->getCacheMaxAge());
224
225
      // Write the cache entry for the response cache metadata.
226
      $this->metadataCache->set($cid, $metadata, $expire, $tags);
227
    }
228
229
    return $metadata;
230
  }
231
232
  /**
233
   * Collects schema cache metadata from all types registered with the schema.
234
   *
235
   * The cache metadata is statically cached. This means that the schema may not
236
   * be modified after this method has been called.
237
   *
238
   * @param \Youshido\GraphQL\Schema\AbstractSchema $schema
239
   *   The schema to extract the cache metadata from.
240
   *
241
   * @return \Drupal\Core\Cache\CacheableMetadata
242
   *   The cache metadata collected from the schema's types.
243
   */
244 View Code Duplication
  protected function extractSchemaCacheMetadata(AbstractSchema $schema) {
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...
245
    $metadata = new CacheableMetadata();
246
    $metadata->setCacheMaxAge(Cache::PERMANENT);
247
    $metadata->addCacheTags(['graphql_schema']);
248
249
    $metadata->addCacheableDependency($this->collectCacheMetadata($schema, function (TypeSystemPluginInterface $item) {
250
      return $item->getSchemaCacheMetadata();
251
    }));
252
253
    return $metadata;
254
  }
255
256
  /**
257
   * Collects result cache metadata from all types registered with the schema.
258
   *
259
   * The cache metadata is statically cached. This means that the schema may not
260
   * be modified after this method has been called.
261
   *
262
   * @param \Youshido\GraphQL\Schema\AbstractSchema $schema
263
   *   The schema to extract the cache metadata from.
264
   *
265
   * @return \Drupal\Core\Cache\CacheableMetadata
266
   *   The cache metadata collected from the schema's types.
267
   */
268 View Code Duplication
  protected function extractResponseCacheMetadata(AbstractSchema $schema) {
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...
269
    $metadata = new CacheableMetadata();
270
    $metadata->setCacheMaxAge(Cache::PERMANENT);
271
    $metadata->addCacheTags(['graphql_response']);
272
    $metadata->addCacheContexts(['gql']);
273
274
    $metadata->addCacheableDependency($this->collectCacheMetadata($schema, function (TypeSystemPluginInterface $item) {
275
      return $item->getResponseCacheMetadata();
276
    }));
277
278
    return $metadata;
279
  }
280
281
  /**
282
   * Recursively collects cache metadata from the generated schema.
283
   *
284
   * @param \Youshido\GraphQL\Schema\AbstractSchema $schema
285
   *   The schema.
286
   * @param callable $extract
287
   *   Callback to extract cache metadata from a plugin within the schema.
288
   *
289
   * @return \Drupal\Core\Cache\CacheableMetadata
290
   *   The collected cache metadata.
291
   */
292
  protected function collectCacheMetadata(AbstractSchema $schema, callable $extract) {
293
    $metadata = new CacheableMetadata();
294
295
    foreach (TypeCollector::collectTypes($schema) as $type) {
296 View Code Duplication
      if ($type instanceof TypeSystemPluginReferenceInterface && ($plugin = $type->getPlugin()) instanceof TypeSystemPluginInterface) {
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...
297
        $metadata->addCacheableDependency($extract($plugin));
298
      }
299
300
      if ($type instanceof AbstractObjectType || $type instanceof AbstractInputObjectType || $type instanceof AbstractInterfaceType) {
301
        foreach ($type->getFields() as $field) {
302 View Code Duplication
          if ($field instanceof TypeSystemPluginReferenceInterface && ($plugin = $field->getPlugin()) instanceof TypeSystemPluginInterface) {
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...
303
            $metadata->addCacheableDependency($extract($plugin));
304
          }
305
        }
306
      }
307
    }
308
309
    return $metadata;
310
  }
311
312
  /**
313
   * Maps a max age value to an "expire" value for the Cache API.
314
   *
315
   * @param int $maxAge
316
   *   A max age value.
317
   *
318
   * @return int
319
   *   A corresponding "expire" value.
320
   *
321
   * @see \Drupal\Core\Cache\CacheBackendInterface::set()
322
   */
323
  protected function maxAgeToExpire($maxAge) {
324
    return ($maxAge === Cache::PERMANENT) ? Cache::PERMANENT : (int) $this->requestStack->getMasterRequest()->server->get('REQUEST_TIME') + $maxAge;
325
  }
326
327
  /**
328
   * Generates a cache identifier for the passed cache contexts.
329
   *
330
   * @param string $name
331
   *   The name of the schema.
332
   * @param \Drupal\Core\Cache\CacheableDependencyInterface $metadata
333
   *   Optional array of cache context tokens.
334
   *
335
   * @return string The generated cache identifier.
336
   *   The generated cache identifier.
337
   */
338
  protected function getCacheIdentifier($name, CacheableDependencyInterface $metadata) {
339
    $tokens = $metadata->getCacheContexts();
340
    $keys = $this->contextsManager->convertTokensToKeys($tokens)->getKeys();
341
    return implode(':', array_merge(['graphql', $name], array_values($keys)));
342
  }
343
344
}
345