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

SchemaLoader::extractCacheMetadata()   B

Complexity

Conditions 8
Paths 9

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 11
nc 9
nop 1
dl 0
loc 20
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 Symfony\Component\HttpFoundation\RequestStack;
14
use Youshido\GraphQL\Schema\AbstractSchema;
15
use Youshido\GraphQL\Type\InputObject\AbstractInputObjectType;
16
use Youshido\GraphQL\Type\InterfaceType\AbstractInterfaceType;
17
use Youshido\GraphQL\Type\Object\AbstractObjectType;
18
19
/**
20
 * Loads and caches a generated GraphQL schema.
21
 */
22
class SchemaLoader {
23
24
  /**
25
   * The cache contexts manager service.
26
   *
27
   * @var \Drupal\Core\Cache\Context\CacheContextsManager
28
   */
29
  protected $contextsManager;
30
31
  /**
32
   * The schema plugin manager service.
33
   *
34
   * @var \Drupal\graphql\Plugin\GraphQL\SchemaPluginManager
35
   */
36
  protected $schemaManager;
37
38
  /**
39
   * The schema cache backend.
40
   *
41
   * @var \Drupal\Core\Cache\CacheBackendInterface
42
   */
43
  protected $schemaCache;
44
45
  /**
46
   * The cache metadata cache.
47
   *
48
   * @var \Drupal\Core\Cache\CacheBackendInterface
49
   */
50
  protected $metadataCache;
51
52
  /**
53
   * The service configuration.
54
   *
55
   * @var array
56
   */
57
  protected $config;
58
59
  /**
60
   * The request stack service.
61
   *
62
   * @var \Symfony\Component\HttpFoundation\RequestStack
63
   */
64
  protected $requestStack;
65
66
  /**
67
   * Static cache of loaded schemas.
68
   *
69
   * @var \Youshido\GraphQL\Schema\AbstractSchema[]
70
   */
71
  protected $schemas = [];
72
73
  /**
74
   * Static cache of loaded cache metadata.
75
   *
76
   * @var \Drupal\Core\Cache\RefinableCacheableDependencyInterface[]
77
   */
78
  protected $metadata = [];
79
80
  /**
81
   * SchemaLoader constructor.
82
   *
83
   * @param \Drupal\Core\Cache\Context\CacheContextsManager $contextsManager
84
   *   The cache contexts manager service.
85
   * @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
86
   *   The request stack service.
87
   * @param \Drupal\graphql\Plugin\GraphQL\SchemaPluginManager $schemaManager
88
   *   The schema plugin manager service.
89
   * @param \Drupal\Core\Cache\CacheBackendInterface $schemaCache
90
   *   The schema cache backend.
91
   * @param \Drupal\Core\Cache\CacheBackendInterface $metadataCache
92
   *   The metadata cache backend.
93
   * @param array $config
94
   *   The configuration provided through the services.yml.
95
   */
96
  public function __construct(
97
    CacheContextsManager $contextsManager,
98
    RequestStack $requestStack,
99
    SchemaPluginManager $schemaManager,
100
    CacheBackendInterface $schemaCache,
101
    CacheBackendInterface $metadataCache,
102
    array $config
103
  ) {
104
    $this->config = $config;
105
    $this->schemaManager = $schemaManager;
106
    $this->contextsManager = $contextsManager;
107
    $this->schemaCache = $schemaCache;
108
    $this->metadataCache = $metadataCache;
109
    $this->requestStack = $requestStack;
110
  }
111
112
  /**
113
   * Loads and caches the generated schema.
114
   *
115
   * @param string $name
116
   *   The name of the schema to load.
117
   *
118
   * @return \Youshido\GraphQL\Schema\AbstractSchema
119
   *   The generated GraphQL schema.
120
   */
121
  public function getSchema($name) {
122
    if (($schema = &$this->schemas[$name]) !== NULL) {
123
      return $schema;
124
    }
125
126
    $schemaCache = !empty($this->config['schema_cache']);
127
    if (!empty($schemaCache) && ($cache = $this->metadataCache->get($name)) && $cache->data) {
128
      $cid = $this->getCacheIdentifier($name, $cache->data);
129
130
      if (($cache = $this->schemaCache->get($cid)) && $cache->data) {
131
        return $schema = $cache->data;
132
      }
133
    }
134
135
    // Get the schema object from the plugin instance.
136
    $schema = $this->schemaManager->createInstance($name)->getSchema();
137
    if (empty($schemaCache)) {
138
      return $schema;
139
    }
140
141
    $metadata = $this->getCacheMetadata($name);
142
    if ($metadata->getCacheMaxAge() !== 0) {
143
      $tags = $metadata->getCacheTags();
144
      $expire = $this->maxAgeToExpire($metadata->getCacheMaxAge());
145
      $cid = $this->getCacheIdentifier($name, $metadata);
146
147
      // Write the cache entry for the schema cache entries.
148
      $this->schemaCache->set($cid, $schema, $expire, $tags);
149
    }
150
151
    return $schema;
152
  }
153
154
  /**
155
   * Retrieves the schema's cache metadata.
156
   *
157
   * @param string $name
158
   *   The name of the schema.
159
   * @return \Drupal\Core\Cache\RefinableCacheableDependencyInterface
160
   *   The cache metadata for the schema.
161
   */
162
  public function getCacheMetadata($name) {
163
    if (array_key_exists($name, $this->metadata)) {
164
      return $this->metadata[$name];
165
    }
166
167
    // The cache key is made up of all of the globally known cache contexts.
168
    if (!empty($this->config['schema_cache'])) {
169
      if (($metadataCache = $this->metadataCache->get($name)) && $metadataCache->data) {
170
        return $this->metadata[$name] = $metadataCache->data;
171
      }
172
    }
173
174
    /** @var \Drupal\Core\Cache\RefinableCacheableDependencyInterface $metadata */
175
    $schema = $this->getSchema($name);
176
    $metadata = $this->extractCacheMetadata($schema);
177
    $this->metadata[$name] = $metadata;
178
    if (empty($this->config['schema_cache'])) {
179
      return $this->metadata[$name];
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->metadata[$name]; of type Drupal\Core\Cache\Refina...Cache\CacheableMetadata adds the type Drupal\Core\Cache\CacheableMetadata to the return on line 179 which is incompatible with the return type documented by Drupal\graphql\GraphQL\S...oader::getCacheMetadata of type Drupal\Core\Cache\Refina...ableDependencyInterface.
Loading history...
180
    }
181
182
    if (($maxAge = $metadata->getCacheMaxAge()) !== 0) {
183
      $tags = $metadata->getCacheTags();
184
      $expire = $this->maxAgeToExpire($maxAge);
185
186
      // Write the cache entry for the schema cache metadata.
187
      $this->metadataCache->set($name, $metadata, $expire, $tags);
188
    }
189
190
    return $metadata;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $metadata; (Drupal\Core\Cache\CacheableMetadata) is incompatible with the return type documented by Drupal\graphql\GraphQL\S...oader::getCacheMetadata of type Drupal\Core\Cache\Refina...ableDependencyInterface.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
191
  }
192
193
  /**
194
   * Collects schema cache metadata from all types registered with the schema.
195
   *
196
   * The cache metadata is statically cached. This means that the schema may not
197
   * be modified after this method has been called.
198
   *
199
   * @param \Youshido\GraphQL\Schema\AbstractSchema $schema
200
   *   The schema to extract the cache metadata from.
201
   *
202
   * @return \Drupal\Core\Cache\CacheableMetadata
203
   *   The cache metadata collected from the schema's types.
204
   */
205
  protected function extractCacheMetadata(AbstractSchema $schema) {
206
    $metadata = new CacheableMetadata();
207
    $metadata->addCacheTags(['graphql_schema']);
208
209
    foreach (TypeCollector::collectTypes($schema) as $type) {
210
      if ($type instanceof CacheableEdgeInterface) {
211
        $metadata->addCacheableDependency($type->getSchemaCacheMetadata());
212
      }
213
214
      if ($type instanceof AbstractObjectType || $type instanceof AbstractInputObjectType || $type instanceof AbstractInterfaceType) {
215
        foreach ($type->getFields() as $field) {
216
          if ($field instanceof CacheableEdgeInterface) {
217
            $metadata->addCacheableDependency($field->getSchemaCacheMetadata());
218
          }
219
        }
220
      }
221
    }
222
223
    return $metadata;
224
  }
225
226
  /**
227
   * Maps a max age value to an "expire" value for the Cache API.
228
   *
229
   * @param int $maxAge
230
   *   A max age value.
231
   *
232
   * @return int
233
   *   A corresponding "expire" value.
234
   *
235
   * @see \Drupal\Core\Cache\CacheBackendInterface::set()
236
   */
237 View Code Duplication
  protected function maxAgeToExpire($maxAge) {
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...
238
    if ($maxAge === Cache::PERMANENT) {
239
      return Cache::PERMANENT;
240
    }
241
242
    return (int) $this->requestStack->getMasterRequest()->server->get('REQUEST_TIME') + $maxAge;
243
  }
244
245
  /**
246
   * Generates a cache identifier for the passed cache contexts.
247
   *
248
   * @param string $name
249
   *   The name of the schema.
250
   * @param \Drupal\Core\Cache\CacheableDependencyInterface $metadata
251
   *   Optional array of cache context tokens.
252
   *
253
   * @return string The generated cache identifier.
254
   *   The generated cache identifier.
255
   */
256
  protected function getCacheIdentifier($name, CacheableDependencyInterface $metadata) {
257
    $tokens = $metadata->getCacheContexts();
258
    $keys = $this->contextsManager->convertTokensToKeys($tokens)->getKeys();
259
    return implode(':', array_merge(['graphql', $name], array_values($keys)));
260
  }
261
262
}
263