Completed
Pull Request — 8.x-3.x (#519)
by Sebastian
08:21 queued 04:08
created

SchemaLoader::getSchemaCacheMetadata()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 5
rs 9.4285
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 (array_key_exists($name, $this->schemas)) {
123
      return $this->schemas[$name];
124
    }
125
126
    // The cache key is made up of all of the globally known cache contexts.
127
    if (!empty($this->config['schema_cache'])) {
128
      if (($contextCache = $this->metadataCache->get($name)) && $contextCache->data) {
129
        $cid = $this->getCacheIdentifier($name, $contextCache->data);
130
131
        if (($schema = $this->schemaCache->get($cid)) && $schema->data) {
132
          return $this->schemas[$name] = $schema->data;
133
        }
134
      }
135
    }
136
137
    $this->schemas[$name] = $this->schemaManager->createInstance($name)->getSchema();
138
    // If the schema is not cacheable, just return it directly.
139
    if (empty($this->config['schema_cache'])) {
140
      return $this->schemas[$name];
141
    }
142
143
    // Compute the cache identifier, tag and expiry time.
144
    $schemaCacheMetadata = $this->getCacheMetadata($name);
145
    if ($schemaCacheMetadata->getCacheMaxAge() !== 0) {
146
      $tags = $schemaCacheMetadata->getCacheTags();
147
      $expire = $this->maxAgeToExpire($schemaCacheMetadata->getCacheMaxAge());
148
      $cid = $this->getCacheIdentifier($name, $schemaCacheMetadata);
149
150
      // Write the cache entry for the schema cache entries.
151
      $this->schemaCache->set($cid, $this->schemas[$name], $expire, $tags);
152
    }
153
154
    return $this->schemas[$name];
155
  }
156
157
  /**
158
   * Retrieves the schema's cache metadata.
159
   *
160
   * @param string $name
161
   *   The name of the schema.
162
   * @return \Drupal\Core\Cache\CacheableDependencyInterface
163
   *   The cache metadata for the schema.
164
   */
165
  public function getCacheMetadata($name) {
166
    if (array_key_exists($name, $this->metadata)) {
167
      return $this->metadata[$name];
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->metadata[$name]; (Drupal\Core\Cache\Refina...ableDependencyInterface) is incompatible with the return type documented by Drupal\graphql\GraphQL\S...oader::getCacheMetadata of type Drupal\Core\Cache\CacheableDependencyInterface.

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...
168
    }
169
170
    // The cache key is made up of all of the globally known cache contexts.
171
    if (!empty($this->config['schema_cache'])) {
172
      if (($metadataCache = $this->metadataCache->get($name)) && $metadataCache->data) {
173
        return $this->metadata[$name] = $metadataCache->data;
174
      }
175
    }
176
177
    /** @var \Drupal\Core\Cache\RefinableCacheableDependencyInterface $metadata */
178
    $schema = $this->getSchema($name);
179
    $metadata = $this->extractCacheMetadata($schema);
180
    $this->metadata[$name] = $metadata;
181
    if (empty($this->config['schema_cache'])) {
182
      return $this->metadata[$name];
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->metadata[$name]; (Drupal\Core\Cache\Refina...Cache\CacheableMetadata) is incompatible with the return type documented by Drupal\graphql\GraphQL\S...oader::getCacheMetadata of type Drupal\Core\Cache\CacheableDependencyInterface.

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...
183
    }
184
185
    if (($maxAge = $metadata->getCacheMaxAge()) !== 0) {
186
      $tags = $metadata->getCacheTags();
187
      $expire = $this->maxAgeToExpire($maxAge);
188
189
      // Write the cache entry for the schema cache metadata.
190
      $this->metadataCache->set($name, $metadata, $expire, $tags);
191
    }
192
193
    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\CacheableDependencyInterface.

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