Skip to content

Commit 08fa8de

Browse files
pmelabfubhy
authored andcommitted
Respect currently negotiated language. (#670)
1 parent 366b9cd commit 08fa8de

10 files changed

Lines changed: 134 additions & 33 deletions

File tree

src/GraphQL/Execution/QueryProcessor.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ protected function doExecuteOperation(PromiseAdapter $adapter, ServerConfig $con
303303

304304
return $promise->then(function (ExecutionResult $result) use ($context) {
305305
$metadata = (new CacheableMetadata())
306-
->addCacheContexts($this->filterCacheContexts($context->getCacheContexts()))
306+
->addCacheContexts($context->getCacheContexts())
307307
->addCacheTags($context->getCacheTags())
308308
->setCacheMaxAge($context->getCacheMaxAge());
309309

@@ -468,7 +468,7 @@ protected function sanitizeRecursive(array $item) {
468468
*/
469469
protected function cacheIdentifier(OperationParams $params, DocumentNode $document, array $contexts = []) {
470470
// Ignore language contexts since they are handled by graphql internally.
471-
$contexts = $this->filterCacheContexts($contexts);
471+
$contexts = $contexts;
472472
$keys = $this->contextsManager->convertTokensToKeys($contexts)->getKeys();
473473

474474
// Sorting the variables will cause fewer cache vectors.

src/GraphQL/Execution/ResolveContext.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,24 @@ class ResolveContext implements RefinableCacheableDependencyInterface {
2323
*/
2424
protected $contexts = [];
2525

26+
/**
27+
* Root context values that will apply if no more specific context is there.
28+
*
29+
* @var array
30+
*/
31+
protected $rootContext = [];
32+
2633
/**
2734
* ResolveContext constructor.
2835
*
2936
* @param array $globals
3037
* List of global values to expose to field resolvers.
38+
* @param array $rootContext
39+
* The root context values the query will be initialised with.
3140
*/
32-
public function __construct(array $globals = []) {
41+
public function __construct(array $globals = [], $rootContext = []) {
3342
$this->globals = $globals;
43+
$this->rootContext = $rootContext;
3444
}
3545

3646
/**
@@ -61,6 +71,10 @@ public function getContext($name, ResolveInfo $info, $default = NULL) {
6171
array_pop($path);
6272
} while (count($path));
6373

74+
if (isset($this->rootContext[$name])) {
75+
return $this->rootContext[$name];
76+
}
77+
6478
return $default;
6579
}
6680

src/GraphQLLanguageContext.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ class GraphQLLanguageContext {
2323
*/
2424
protected $currentLanguage;
2525

26+
/**
27+
* @var \SplStack
28+
*/
29+
protected $languageStack;
30+
2631
/**
2732
* The language manager service.
2833
*
@@ -38,6 +43,7 @@ class GraphQLLanguageContext {
3843
*/
3944
public function __construct(LanguageManagerInterface $languageManager) {
4045
$this->languageManager = $languageManager;
46+
$this->languageStack = new \SplStack();
4147
}
4248

4349
/**
@@ -67,6 +73,7 @@ public function getCurrentLanguage() {
6773
* Any exception caught while executing the callable.
6874
*/
6975
public function executeInLanguageContext(callable $callable, $language) {
76+
$this->languageStack->push($this->currentLanguage);
7077
$this->currentLanguage = $language;
7178
$this->isActive = TRUE;
7279
$this->languageManager->reset();
@@ -79,7 +86,7 @@ public function executeInLanguageContext(callable $callable, $language) {
7986
}
8087
finally {
8188
// In any case, set the language context back to null.
82-
$this->currentLanguage = NULL;
89+
$this->currentLanguage = $this->languageStack->pop();
8390
$this->isActive = FALSE;
8491
$this->languageManager->reset();
8592
}

src/Plugin/GraphQL/Fields/FieldPluginBase.php

Lines changed: 45 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
use Drupal\graphql\Plugin\GraphQL\Traits\DeprecatablePluginTrait;
1717
use Drupal\graphql\Plugin\GraphQL\Traits\DescribablePluginTrait;
1818
use Drupal\graphql\Plugin\GraphQL\Traits\TypedPluginTrait;
19-
use Drupal\graphql\Plugin\LanguageNegotiation\LanguageNegotiationGraphQL;
2019
use Drupal\graphql\Plugin\SchemaBuilderInterface;
2120
use GraphQL\Deferred;
2221
use GraphQL\Type\Definition\ListOfType;
@@ -44,6 +43,13 @@ abstract class FieldPluginBase extends PluginBase implements FieldPluginInterfac
4443
*/
4544
protected $renderer;
4645

46+
/**
47+
* Static cache for `isLanguageAwareField()`
48+
*
49+
* @var boolean
50+
*/
51+
protected $isLanguageAware = NULL;
52+
4753
/**
4854
* {@inheritdoc}
4955
*/
@@ -118,20 +124,10 @@ public function resolve($value, array $args, ResolveContext $context, ResolveInf
118124
if (array_key_exists($argument, $args) && !is_null($args[$argument])) {
119125
$context->setContext($argument, $args[$argument], $info);
120126
}
121-
else {
122-
$args[$argument] = $context->getContext($argument, $info);
123-
}
127+
$args[$argument] = $context->getContext($argument, $info);
124128
}
125129

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-
}
130+
return $this->resolveDeferred([$this, 'resolveValues'], $value, $args, $context, $info);
135131
}
136132

137133
/**
@@ -143,33 +139,56 @@ public function resolve($value, array $args, ResolveContext $context, ResolveInf
143139
* The fields language awareness status.
144140
*/
145141
protected function isLanguageAwareField() {
146-
return (boolean) count(array_filter($this->getPluginDefinition()['response_cache_contexts'], function ($context) {
147-
return strpos($context, 'languages:') === 0;
148-
}));
142+
if (is_null($this->isLanguageAware)) {
143+
$this->isLanguageAware = (boolean) count(array_filter($this->getPluginDefinition()['response_cache_contexts'], function ($context) {
144+
return strpos($context, 'languages:') === 0;
145+
}));
146+
}
147+
return $this->isLanguageAware;
149148
}
150149

151150
/**
152151
* {@inheritdoc}
153152
*/
154153
protected function resolveDeferred(callable $callback, $value, array $args, ResolveContext $context, ResolveInfo $info) {
154+
$isLanguageAware = $this->isLanguageAwareField();
155+
$languageContext = $this->getLanguageContext();
156+
155157
$renderContext = new RenderContext();
156158

157-
$result = $this->getRenderer()->executeInRenderContext($renderContext, function () use ($callback, $value, $args, $context, $info) {
158-
$result = $callback($value, $args, $context, $info);
159-
if ($result instanceof \Generator) {
160-
$result = iterator_to_array($result);
161-
}
162-
return $result;
163-
});
159+
$executor = function () use ($callback, $renderContext, $value, $args, $context, $info) {
160+
return $this->getRenderer()->executeInRenderContext($renderContext, function () use ($callback, $value, $args, $context, $info) {
161+
$result = $callback($value, $args, $context, $info);
162+
if ($result instanceof \Generator) {
163+
$result = iterator_to_array($result);
164+
}
165+
return $result;
166+
});
167+
};
168+
169+
$result = $isLanguageAware
170+
? $languageContext->executeInLanguageContext($executor, $context->getContext('language', $info))
171+
: $executor();
164172

165173
if (!$renderContext->isEmpty() && $info->operation->operation === 'query') {
166174
$context->addCacheableDependency($renderContext->pop());
167175
}
168176

169177
if (is_callable($result)) {
170-
return new Deferred(function () use ($result, $value, $args, $context, $info) {
171-
return $this->resolveDeferred($result, $value, $args, $context, $info);
172-
});
178+
return new Deferred(
179+
function () use ($result, $value, $args, $context, $info, $isLanguageAware, $languageContext) {
180+
if ($isLanguageAware) {
181+
return $languageContext
182+
->executeInLanguageContext(
183+
function () use ($result, $value, $args, $context, $info) {
184+
return $this->resolveDeferred($result, $value, $args, $context, $info);
185+
},
186+
$context->getContext('language', $info)
187+
);
188+
}
189+
return $this->resolveDeferred($result, $value, $args, $context, $info);
190+
}
191+
);
173192
}
174193

175194
// Only collect cache metadata if this is a query. All other operation types

src/Plugin/GraphQL/Schemas/SchemaPluginBase.php

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Drupal\Component\Plugin\PluginBase;
66
use Drupal\Core\Cache\CacheableDependencyInterface;
7+
use Drupal\Core\Language\LanguageManagerInterface;
78
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
89
use Drupal\Core\Session\AccountProxyInterface;
910
use Drupal\graphql\GraphQL\Execution\ResolveContext;
@@ -111,6 +112,11 @@ abstract class SchemaPluginBase extends PluginBase implements SchemaPluginInterf
111112
*/
112113
protected $logger;
113114

115+
/**
116+
* @var \Drupal\Core\Language\LanguageManagerInterface
117+
*/
118+
protected $languageManager;
119+
114120
/**
115121
* {@inheritdoc}
116122
*/
@@ -126,6 +132,7 @@ public static function create(ContainerInterface $container, array $configuratio
126132
$container->get('graphql.query_provider'),
127133
$container->get('current_user'),
128134
$container->get('logger.channel.graphql'),
135+
$container->get('language_manager'),
129136
$container->getParameter('graphql.config')
130137
);
131138
}
@@ -167,6 +174,7 @@ public function __construct(
167174
QueryProviderInterface $queryProvider,
168175
AccountProxyInterface $currentUser,
169176
LoggerInterface $logger,
177+
LanguageManagerInterface $languageManager,
170178
array $parameters
171179
) {
172180
parent::__construct($configuration, $pluginId, $pluginDefinition);
@@ -178,6 +186,7 @@ public function __construct(
178186
$this->currentUser = $currentUser;
179187
$this->parameters = $parameters;
180188
$this->logger = $logger;
189+
$this->languageManager = $languageManager;
181190
}
182191

183192
/**
@@ -247,8 +256,18 @@ public function getServer() {
247256
// Each document (e.g. in a batch query) gets its own resolve context. This
248257
// allows us to collect the cache metadata and contextual values (e.g.
249258
// inheritance for language) for each query separately.
250-
$context = new ResolveContext($globals);
259+
$context = new ResolveContext($globals, [
260+
'language' => $this->languageManager->getCurrentLanguage()->getId(),
261+
]);
251262
$context->addCacheTags(['graphql_response']);
263+
264+
// Always add the language_url cache context.
265+
$context->addCacheContexts([
266+
'languages:language_url',
267+
'languages:language_interface',
268+
'languages:language_content',
269+
'user.permissions',
270+
]);
252271
if ($this instanceof CacheableDependencyInterface) {
253272
$context->addCacheableDependency($this);
254273
}

tests/src/Kernel/Framework/LanguageContextTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,17 @@ protected function setUp() {
4545
yield \Drupal::languageManager()->getCurrentLanguage()->getId();
4646
});
4747

48+
$this->mockField('deferred', [
49+
'name' => 'deferred',
50+
'parents' => ['Root', 'Node'],
51+
'type' => 'String',
52+
'response_cache_contexts' => ['languages:language_interface'],
53+
], function () {
54+
return function () {
55+
yield \Drupal::languageManager()->getCurrentLanguage()->getId();
56+
};
57+
});
58+
4859
$this->mockField('unaware', [
4960
'name' => 'unaware',
5061
'parents' => ['Root', 'Node'],
@@ -160,6 +171,31 @@ public function testInheritedLanguage() {
160171
], $this->defaultCacheMetaData());
161172
}
162173

174+
/**
175+
* Test deferred language resolvers.
176+
*/
177+
public function testDeferredLanguage() {
178+
$query = <<<GQL
179+
query {
180+
edge(language: "fr") {
181+
deferred
182+
edge(language: "en") {
183+
deferred
184+
}
185+
}
186+
}
187+
GQL;
188+
189+
$this->assertResults($query, [], [
190+
'edge' => [
191+
'deferred' => 'fr',
192+
'edge' => [
193+
'deferred' => 'en',
194+
],
195+
],
196+
], $this->defaultCacheMetaData());
197+
}
198+
163199
/**
164200
* Test overridden language.
165201
*/

tests/src/Kernel/Framework/SecureFieldTest.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ public function testSecureField() {
5757
*/
5858
public function testInsecureField() {
5959
$metadata = $this->defaultCacheMetaData();
60-
$metadata->setCacheContexts([]);
6160
$metadata->setCacheMaxAge(0);
6261
$this->assertErrors('{ insecure }', [], [
6362
'Unable to resolve insecure field \'insecure\'.',

tests/src/Kernel/GraphQLTestBase.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,12 @@ protected function defaultCacheTags() {
8181
* {@inheritdoc}
8282
*/
8383
protected function defaultCacheContexts() {
84-
return ['user.permissions'];
84+
return [
85+
'user.permissions',
86+
'languages:language_url',
87+
'languages:language_interface',
88+
'languages:language_content',
89+
];
8590
}
8691

8792
/**

tests/src/Traits/MockGraphQLPluginTrait.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ protected function mockSchemaFactory($definition, $builder) {
298298
$this->container->get('graphql.query_provider'),
299299
$this->container->get('current_user'),
300300
$this->container->get('logger.channel.graphql'),
301+
$this->container->get('language_manager'),
301302
$this->container->getParameter('graphql.config')
302303
]);
303304

tests/src/Traits/QueryResultAssertionTrait.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ protected function defaultCacheMetaData() {
7777
*/
7878
protected function defaultMutationCacheMetaData() {
7979
$metadata = new CacheableMetadata();
80+
$metadata->setCacheContexts($this->defaultCacheContexts());
8081
$metadata->setCacheMaxAge(0);
8182
$metadata->setCacheTags($this->defaultCacheTags());
8283
return $metadata;

0 commit comments

Comments
 (0)