Completed
Push — 8.x-3.x ( e67c0e...2e01b0 )
by Philipp
02:27
created

SchemaLoader::collectCacheMetadata()   B

Complexity

Conditions 8
Paths 9

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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