Skip to content

Commit 8b362e0

Browse files
authored
Contextual language handling. (#550)
* Intermediate commit, not functional. * Don't abort context retrieval on falsy path elements. * Removed all implicit language contexts. * Contextual language negotiation. * Enforcing graphql language negotiation through config overrides. * Fixing language negotiator. * Fixed negotiation. * Ignore language cache contexts. * Added upgrade instructions for languages. * Docs fix. * Fix to also work without language module enabled. * Fixed wrong return, has to be yield. * Added core 8.5 tests. * Docs fix. * Moved contextual language to service. * Travis debug. * Removed travis debug. * Renamed test class according to file. * More tests and resulting fixes. * Simplified language context. * Explicit callable execution. * More fine grained tests. * Test negotiator initialization result. * Test cleanup. * Test disable negotiator fix. * Debugging ... * Debugging ... * Debugging ... * Debugging ... * Check negotiator instance. * Changed module initialization order. * Depending on language module. * Revert "Depending on language module." This reverts commit f8df099 * Revert "Changed module initialization order." This reverts commit bbaa77f * Proper service provider classname. * Properly injecting the language context. * Annotation style fixes. * Revert "Properly injecting the language context." This reverts commit c358b53 * Real setter injection. * Moved property to top. * Use language cache contexts to conditionally set the graphql language context. * Enabled multilingual features for all tests to catch potential context leaks. * Ignore language contexts when creating the cache key. * Reproducing leaking translation context. * Add language_content cache context to RouteEntity since the access handler emits it for some reason. * Fixed exception expectation.
1 parent 962268c commit 8b362e0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+895
-155
lines changed

doc/upgrade/beta6.md

+27
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,33 @@
22
## Schema changes
33
These changes affect you if you are using the schema automatically generated by the `graphql_core` module.
44

5+
6+
### Language handling
7+
8+
Multilingual queries changed drastically. The endpoints language negotiation is ignored entirely, instead all language handling is left to the query and its arguments. A couple of fields accept a "language" argument, and whenever this argument is filled explicitly, it's value will be inherited to subsequent occurrences. The `route` field will set this context implicitly from the paths language prefix.
9+
10+
```graphql
11+
query {
12+
route(path: "/node/1") {
13+
... on EntityCanonicalUrl {
14+
entity {
15+
# Will emit the default language.
16+
entityLabel
17+
}
18+
}
19+
}
20+
route(path: "/fr/node/1") {
21+
... on EntityCanonicalUrl {
22+
entity {
23+
# Will emit the french translation.
24+
entityLabel
25+
}
26+
}
27+
}
28+
}
29+
30+
```
31+
532
### Url Interfaces
633
The type structure of the `Url` object changed. While before there have been just the `InternalUrl` and `ExternalUrl` types, the `InternalUrl` has become an interface that can resolve to different Url types, depending on the underlying route. The `DefaultInternalUrl` has the fields for context resolving and other generic rout information. The `EntityCanonicalUrl` has access to the underlying entity.
734

graphql.services.yml

+15-4
Original file line numberDiff line numberDiff line change
@@ -244,8 +244,19 @@ services:
244244

245245
# Buffers.
246246
graphql.buffer.entity:
247-
class: Drupal\graphql\GraphQL\Buffers\EntityBuffer
248-
arguments: ['@entity_type.manager']
247+
class: Drupal\graphql\GraphQL\Buffers\EntityBuffer
248+
arguments: ['@entity_type.manager']
249249
graphql.buffer.subrequest:
250-
class: Drupal\graphql\GraphQL\Buffers\SubRequestBuffer
251-
arguments: ['@http_kernel', '@request_stack']
250+
class: Drupal\graphql\GraphQL\Buffers\SubRequestBuffer
251+
arguments: ['@http_kernel', '@request_stack']
252+
253+
graphql.language_context:
254+
class: Drupal\graphql\GraphQLLanguageContext
255+
arguments: ['@language_manager']
256+
257+
graphql.config_factory_override:
258+
class: Drupal\graphql\Config\GraphQLConfigOverrides
259+
arguments: ['@config.storage']
260+
tags:
261+
- { name: config.factory.override, priority: -253 }
262+

modules/graphql_core/src/Plugin/Deriver/Interfaces/EntityTypeDeriver.php

-4
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,6 @@ public function getDerivativeDefinitions($basePluginDefinition) {
3939
$derivative['response_cache_contexts'][] = 'user.node_grants:view';
4040
}
4141

42-
if ($type->isTranslatable()) {
43-
$derivative['response_cache_contexts'][] = 'languages:language_content';
44-
}
45-
4642
$this->derivatives[$typeId] = $derivative;
4743
}
4844

modules/graphql_core/src/Plugin/Deriver/Types/EntityBundleDeriver.php

-4
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,6 @@ public function getDerivativeDefinitions($basePluginDefinition) {
9090
$derivative['response_cache_contexts'][] = 'user.node_grants:view';
9191
}
9292

93-
if ($type->isTranslatable()) {
94-
$derivative['response_cache_contexts'][] = 'languages:language_content';
95-
}
96-
9793
$this->derivatives[$typeId . '-' . $bundle] = $derivative;
9894
}
9995
}

modules/graphql_core/src/Plugin/Deriver/Types/EntityTypeDeriver.php

-4
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,6 @@ public function getDerivativeDefinitions($basePluginDefinition) {
4040
$derivative['response_cache_contexts'][] = 'user.node_grants:view';
4141
}
4242

43-
if ($type->isTranslatable()) {
44-
$derivative['response_cache_contexts'][] = 'languages:language_content';
45-
}
46-
4743
$this->derivatives[$typeId] = $derivative;
4844
}
4945

modules/graphql_core/src/Plugin/GraphQL/Fields/Entity/EntityById.php

+4-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Drupal\Core\Entity\EntityRepositoryInterface;
77
use Drupal\Core\Entity\EntityTypeManagerInterface;
88
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
9+
use Drupal\Core\TypedData\TranslatableInterface;
910
use Drupal\graphql\GraphQL\Buffers\EntityBuffer;
1011
use Drupal\graphql\GraphQL\Cache\CacheableValue;
1112
use Drupal\graphql\GraphQL\Execution\ResolveContext;
@@ -20,6 +21,7 @@
2021
* arguments = {
2122
* "id" = "String!"
2223
* },
24+
* contextual_arguments = {"language"},
2325
* deriver = "Drupal\graphql_core\Plugin\Deriver\Fields\EntityByIdDeriver"
2426
* )
2527
*/
@@ -109,8 +111,8 @@ protected function resolveValues($value, array $args, ResolveContext $context, R
109111
$access = $entity->access('view', NULL, TRUE);
110112

111113
if ($access->isAllowed()) {
112-
if (isset($args['language']) && $args['language'] != $entity->language()->getId()) {
113-
$entity = $this->entityRepository->getTranslationFromContext($entity, $args['language']);
114+
if (isset($args['language']) && $args['language'] != $entity->language()->getId() && $entity instanceof TranslatableInterface) {
115+
$entity = $entity->getTranslation($args['language']);
114116
}
115117

116118
yield $entity->addCacheableDependency($access);

modules/graphql_core/src/Plugin/GraphQL/Fields/Entity/EntityRevisionById.php

+4-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Drupal\Core\Entity\EntityRepositoryInterface;
88
use Drupal\Core\Entity\EntityTypeManagerInterface;
99
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
10+
use Drupal\Core\TypedData\TranslatableInterface;
1011
use Drupal\graphql\GraphQL\Cache\CacheableValue;
1112
use Drupal\graphql\GraphQL\Execution\ResolveContext;
1213
use Drupal\graphql\Plugin\GraphQL\Fields\FieldPluginBase;
@@ -20,6 +21,7 @@
2021
* arguments = {
2122
* "id" = "String!"
2223
* },
24+
* contextual_arguments = {"language"},
2325
* deriver = "Drupal\graphql_core\Plugin\Deriver\Fields\EntityRevisionByIdDeriver"
2426
* )
2527
*/
@@ -98,8 +100,8 @@ protected function resolveValues($value, array $args, ResolveContext $context, R
98100
}
99101
/** @var \Drupal\Core\Access\AccessResultInterface $access */
100102
else if (($access = $entity->access('view', NULL, TRUE)) && $access->isAllowed()) {
101-
if (isset($args['language']) && $args['language'] != $entity->language()->getId()) {
102-
$entity = $this->entityRepository->getTranslationFromContext($entity, $args['language']);
103+
if ($entity instanceof TranslatableInterface && isset($args['language']) && $args['language'] != $entity->language()->getId()) {
104+
$entity = $entity->getTranslation($args['language']);
103105
}
104106

105107
yield new CacheableValue($entity, [$access]);

modules/graphql_core/src/Plugin/GraphQL/Fields/Entity/EntityTranslation.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public function __construct(array $configuration, $pluginId, $pluginDefinition,
6868
*/
6969
public function resolveValues($value, array $args, ResolveContext $context, ResolveInfo $info) {
7070
if ($value instanceof EntityInterface && $value instanceof TranslatableInterface && $value->isTranslatable()) {
71-
yield $this->entityRepository->getTranslationFromContext($value, $args['language']);
71+
yield $value->getTranslation($args['language']);
7272
}
7373
}
7474

modules/graphql_core/src/Plugin/GraphQL/Fields/Entity/EntityTranslations.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public function resolveValues($value, array $args, ResolveContext $context, Reso
7272
if ($value instanceof ContentEntityInterface && $value instanceof TranslatableInterface && $value->isTranslatable()) {
7373
$languages = $value->getTranslationLanguages();
7474
foreach ($languages as $language) {
75-
yield $this->entityRepository->getTranslationFromContext($value, $language->getId());
75+
yield $value->getTranslation($language->getId());
7676
}
7777
}
7878
}

modules/graphql_core/src/Plugin/GraphQL/Fields/EntityFieldBase.php

+7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
namespace Drupal\graphql_core\Plugin\GraphQL\Fields;
44

55
use Drupal\Component\Render\MarkupInterface;
6+
use Drupal\Core\Entity\ContentEntityInterface;
7+
use Drupal\Core\Entity\Entity;
8+
use Drupal\Core\Entity\EntityInterface;
69
use Drupal\Core\Field\FieldItemInterface;
710
use Drupal\graphql\GraphQL\Execution\ResolveContext;
811
use Drupal\graphql\Plugin\GraphQL\Fields\FieldPluginBase;
@@ -31,6 +34,10 @@ protected function resolveItem($item, array $args, ResolveContext $context, Reso
3134
$result = $type->serialize($result);
3235
}
3336

37+
if ($result instanceof ContentEntityInterface && $result->isTranslatable() && $language = $context->getContext('language', $info)) {
38+
$result = $result->getTranslation($language);
39+
}
40+
3441
return $result;
3542
}
3643
}

modules/graphql_core/src/Plugin/GraphQL/Fields/EntityQuery/EntityQueryEntities.php

+3-2
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
* parents = {"EntityQueryResult"},
2929
* arguments = {
3030
* "language" = "LanguageId"
31-
* }
31+
* },
32+
* contextual_arguments = {"language"}
3233
* )
3334
*/
3435
class EntityQueryEntities extends FieldPluginBase implements ContainerFactoryPluginInterface {
@@ -195,7 +196,7 @@ protected function resolveEntities(array $entities, $metadata, array $args, Reso
195196
foreach ($entities as $entity) {
196197
// Translate the entity if it is translatable and a language was given.
197198
if ($language && $entity instanceof TranslatableInterface && $entity->isTranslatable()) {
198-
$entity = $this->entityRepository->getTranslationFromContext($entity, $language);
199+
yield $entity->getTranslation($language);
199200
}
200201

201202
$access = $entity->access('view', NULL, TRUE);

modules/graphql_core/src/Plugin/GraphQL/Fields/LanguageSwitch/LanguageSwitchLinks.php

+20-23
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
88
use Drupal\Core\Url;
99
use Drupal\graphql\GraphQL\Buffers\SubRequestBuffer;
10-
use Drupal\graphql\GraphQL\Cache\CacheableValue;
1110
use Drupal\graphql\GraphQL\Execution\ResolveContext;
1211
use Drupal\graphql\Plugin\GraphQL\Fields\FieldPluginBase;
1312
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -20,10 +19,14 @@
2019
* name = "languageSwitchLinks",
2120
* type = "[LanguageSwitchLink]",
2221
* parents = {"InternalUrl"},
22+
* arguments = {
23+
* "language" = "LanguageId"
24+
* },
2325
* response_cache_contexts = {
2426
* "languages:language_url",
25-
* "languages:language_interface"
26-
* }
27+
* "languages:language_interface",
28+
* },
29+
* contextual_arguments = {"language"}
2730
* )
2831
*/
2932
class LanguageSwitchLinks extends FieldPluginBase implements ContainerFactoryPluginInterface {
@@ -75,29 +78,23 @@ public function __construct(
7578
*/
7679
protected function resolveValues($value, array $args, ResolveContext $context, ResolveInfo $info) {
7780
if ($value instanceof Url) {
78-
$resolve = $this->subRequestBuffer->add($value, function (Url $url) {
79-
$links = $this->languageManager->getLanguageSwitchLinks(LanguageInterface::TYPE_URL, $url);
80-
$current = $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_URL);
8181

82-
return [$current, $links];
83-
});
82+
$links = $this->languageManager->getLanguageSwitchLinks(LanguageInterface::TYPE_URL, $value);
83+
$current = $this->languageManager->getLanguage($args['language']);
84+
if (!$current) {
85+
$current = $this->languageManager->getDefaultLanguage();
86+
}
8487

85-
return function () use ($resolve) {
86-
/** @var \Drupal\graphql\GraphQL\Cache\CacheableValue $response */
87-
$response = $resolve();
88-
list($current, $links) = $response->getValue();
89-
90-
if (!empty($links->links)) {
91-
foreach ($links->links as $link) {
92-
// Yield the link array and the language object of the language
93-
// context resolved from the sub-request.
94-
yield new CacheableValue([
95-
'link' => $link,
96-
'context' => $current,
97-
], [$response]);
98-
}
88+
if (!empty($links->links)) {
89+
foreach ($links->links as $link) {
90+
// Yield the link array and the language object of the language
91+
// context resolved from the sub-request.
92+
yield [
93+
'link' => $link,
94+
'context' => $current,
95+
];
9996
}
100-
};
97+
}
10198
}
10299
}
103100

modules/graphql_core/src/Plugin/GraphQL/Fields/Menu/MenuByName.php

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Drupal\graphql_core\Plugin\GraphQL\Fields\Menu;
44

55
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
6+
use Drupal\Core\Entity\EntityType;
67
use Drupal\Core\Entity\EntityTypeManagerInterface;
78
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
89
use Drupal\graphql\GraphQL\Execution\ResolveContext;
@@ -22,7 +23,8 @@
2223
* type = "Menu",
2324
* arguments = {
2425
* "name" = "String!"
25-
* }
26+
* },
27+
* response_cache_contexts = {"languages:language_interface"}
2628
* )
2729
*/
2830
class MenuByName extends FieldPluginBase implements ContainerFactoryPluginInterface {

modules/graphql_core/src/Plugin/GraphQL/Fields/Routing/Route.php

+30-4
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22

33
namespace Drupal\graphql_core\Plugin\GraphQL\Fields\Routing;
44

5-
use Drupal\Component\Utility\UrlHelper;
65
use Drupal\Core\Path\PathValidatorInterface;
76
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
8-
use Drupal\Core\Url;
97
use Drupal\graphql\GraphQL\Cache\CacheableValue;
108
use Drupal\graphql\GraphQL\Execution\ResolveContext;
119
use Drupal\graphql\Plugin\GraphQL\Fields\FieldPluginBase;
10+
use Drupal\language\LanguageNegotiator;
1211
use GraphQL\Type\Definition\ResolveInfo;
1312
use Symfony\Component\DependencyInjection\ContainerInterface;
13+
use Symfony\Component\HttpFoundation\Request;
1414

1515
/**
1616
* Retrieve a route object based on a path.
@@ -35,6 +35,13 @@ class Route extends FieldPluginBase implements ContainerFactoryPluginInterface {
3535
*/
3636
protected $pathValidator;
3737

38+
/**
39+
* The language negotiator service.
40+
*
41+
* @var \Drupal\language\LanguageNegotiator
42+
*/
43+
protected $languageNegotiator;
44+
3845
/**
3946
* {@inheritdoc}
4047
*/
@@ -43,7 +50,8 @@ public static function create(ContainerInterface $container, array $configuratio
4350
$configuration,
4451
$plugin_id,
4552
$plugin_definition,
46-
$container->get('path.validator')
53+
$container->get('path.validator'),
54+
$container->has('language_negotiator') ? $container->get('language_negotiator') : NULL
4755
);
4856
}
4957

@@ -58,18 +66,36 @@ public static function create(ContainerInterface $container, array $configuratio
5866
* The plugin definition.
5967
* @param \Drupal\Core\Path\PathValidatorInterface $pathValidator
6068
* The path validator service.
69+
* @param \Drupal\language\LanguageNegotiator|null $languageNegotiator
70+
* The language negotiator.
6171
*/
62-
public function __construct(array $configuration, $pluginId, $pluginDefinition, PathValidatorInterface $pathValidator) {
72+
public function __construct(
73+
array $configuration,
74+
$pluginId,
75+
$pluginDefinition,
76+
PathValidatorInterface $pathValidator,
77+
$languageNegotiator
78+
) {
6379
parent::__construct($configuration, $pluginId, $pluginDefinition);
6480
$this->pathValidator = $pathValidator;
81+
$this->languageNegotiator = $languageNegotiator;
6582
}
6683

6784
/**
6885
* {@inheritdoc}
6986
*/
7087
public function resolveValues($value, array $args, ResolveContext $context, ResolveInfo $info) {
7188
if (($url = $this->pathValidator->getUrlIfValidWithoutAccessCheck($args['path'])) && $url->access()) {
89+
90+
// For now we just take the "url" negotiator into account.
91+
if ($this->languageNegotiator) {
92+
if ($negotiator = $this->languageNegotiator->getNegotiationMethodInstance('language-url')) {
93+
$context->setContext('language', $negotiator->getLangcode(Request::create($args['path'])), $info);
94+
}
95+
}
96+
7297
yield $url;
98+
7399
}
74100
else {
75101
yield (new CacheableValue(NULL))->addCacheTags(['4xx-response']);

0 commit comments

Comments
 (0)