Skip to content

Commit

Permalink
Contextual language handling. (#550)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
pmelab authored Mar 19, 2018
1 parent 962268c commit 8b362e0
Show file tree
Hide file tree
Showing 42 changed files with 895 additions and 155 deletions.
27 changes: 27 additions & 0 deletions doc/upgrade/beta6.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,33 @@
## Schema changes
These changes affect you if you are using the schema automatically generated by the `graphql_core` module.


### Language handling

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.

```graphql
query {
route(path: "/node/1") {
... on EntityCanonicalUrl {
entity {
# Will emit the default language.
entityLabel
}
}
}
route(path: "/fr/node/1") {
... on EntityCanonicalUrl {
entity {
# Will emit the french translation.
entityLabel
}
}
}
}

```

### Url Interfaces
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.

Expand Down
19 changes: 15 additions & 4 deletions graphql.services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -244,8 +244,19 @@ services:

# Buffers.
graphql.buffer.entity:
class: Drupal\graphql\GraphQL\Buffers\EntityBuffer
arguments: ['@entity_type.manager']
class: Drupal\graphql\GraphQL\Buffers\EntityBuffer
arguments: ['@entity_type.manager']
graphql.buffer.subrequest:
class: Drupal\graphql\GraphQL\Buffers\SubRequestBuffer
arguments: ['@http_kernel', '@request_stack']
class: Drupal\graphql\GraphQL\Buffers\SubRequestBuffer
arguments: ['@http_kernel', '@request_stack']

graphql.language_context:
class: Drupal\graphql\GraphQLLanguageContext
arguments: ['@language_manager']

graphql.config_factory_override:
class: Drupal\graphql\Config\GraphQLConfigOverrides
arguments: ['@config.storage']
tags:
- { name: config.factory.override, priority: -253 }

Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,6 @@ public function getDerivativeDefinitions($basePluginDefinition) {
$derivative['response_cache_contexts'][] = 'user.node_grants:view';
}

if ($type->isTranslatable()) {
$derivative['response_cache_contexts'][] = 'languages:language_content';
}

$this->derivatives[$typeId] = $derivative;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,6 @@ public function getDerivativeDefinitions($basePluginDefinition) {
$derivative['response_cache_contexts'][] = 'user.node_grants:view';
}

if ($type->isTranslatable()) {
$derivative['response_cache_contexts'][] = 'languages:language_content';
}

$this->derivatives[$typeId . '-' . $bundle] = $derivative;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,6 @@ public function getDerivativeDefinitions($basePluginDefinition) {
$derivative['response_cache_contexts'][] = 'user.node_grants:view';
}

if ($type->isTranslatable()) {
$derivative['response_cache_contexts'][] = 'languages:language_content';
}

$this->derivatives[$typeId] = $derivative;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\TypedData\TranslatableInterface;
use Drupal\graphql\GraphQL\Buffers\EntityBuffer;
use Drupal\graphql\GraphQL\Cache\CacheableValue;
use Drupal\graphql\GraphQL\Execution\ResolveContext;
Expand All @@ -20,6 +21,7 @@
* arguments = {
* "id" = "String!"
* },
* contextual_arguments = {"language"},
* deriver = "Drupal\graphql_core\Plugin\Deriver\Fields\EntityByIdDeriver"
* )
*/
Expand Down Expand Up @@ -109,8 +111,8 @@ protected function resolveValues($value, array $args, ResolveContext $context, R
$access = $entity->access('view', NULL, TRUE);

if ($access->isAllowed()) {
if (isset($args['language']) && $args['language'] != $entity->language()->getId()) {
$entity = $this->entityRepository->getTranslationFromContext($entity, $args['language']);
if (isset($args['language']) && $args['language'] != $entity->language()->getId() && $entity instanceof TranslatableInterface) {
$entity = $entity->getTranslation($args['language']);
}

yield $entity->addCacheableDependency($access);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\TypedData\TranslatableInterface;
use Drupal\graphql\GraphQL\Cache\CacheableValue;
use Drupal\graphql\GraphQL\Execution\ResolveContext;
use Drupal\graphql\Plugin\GraphQL\Fields\FieldPluginBase;
Expand All @@ -20,6 +21,7 @@
* arguments = {
* "id" = "String!"
* },
* contextual_arguments = {"language"},
* deriver = "Drupal\graphql_core\Plugin\Deriver\Fields\EntityRevisionByIdDeriver"
* )
*/
Expand Down Expand Up @@ -98,8 +100,8 @@ protected function resolveValues($value, array $args, ResolveContext $context, R
}
/** @var \Drupal\Core\Access\AccessResultInterface $access */
else if (($access = $entity->access('view', NULL, TRUE)) && $access->isAllowed()) {
if (isset($args['language']) && $args['language'] != $entity->language()->getId()) {
$entity = $this->entityRepository->getTranslationFromContext($entity, $args['language']);
if ($entity instanceof TranslatableInterface && isset($args['language']) && $args['language'] != $entity->language()->getId()) {
$entity = $entity->getTranslation($args['language']);
}

yield new CacheableValue($entity, [$access]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public function __construct(array $configuration, $pluginId, $pluginDefinition,
*/
public function resolveValues($value, array $args, ResolveContext $context, ResolveInfo $info) {
if ($value instanceof EntityInterface && $value instanceof TranslatableInterface && $value->isTranslatable()) {
yield $this->entityRepository->getTranslationFromContext($value, $args['language']);
yield $value->getTranslation($args['language']);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public function resolveValues($value, array $args, ResolveContext $context, Reso
if ($value instanceof ContentEntityInterface && $value instanceof TranslatableInterface && $value->isTranslatable()) {
$languages = $value->getTranslationLanguages();
foreach ($languages as $language) {
yield $this->entityRepository->getTranslationFromContext($value, $language->getId());
yield $value->getTranslation($language->getId());
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
namespace Drupal\graphql_core\Plugin\GraphQL\Fields;

use Drupal\Component\Render\MarkupInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\Entity;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\graphql\GraphQL\Execution\ResolveContext;
use Drupal\graphql\Plugin\GraphQL\Fields\FieldPluginBase;
Expand Down Expand Up @@ -31,6 +34,10 @@ protected function resolveItem($item, array $args, ResolveContext $context, Reso
$result = $type->serialize($result);
}

if ($result instanceof ContentEntityInterface && $result->isTranslatable() && $language = $context->getContext('language', $info)) {
$result = $result->getTranslation($language);
}

return $result;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
* parents = {"EntityQueryResult"},
* arguments = {
* "language" = "LanguageId"
* }
* },
* contextual_arguments = {"language"}
* )
*/
class EntityQueryEntities extends FieldPluginBase implements ContainerFactoryPluginInterface {
Expand Down Expand Up @@ -195,7 +196,7 @@ protected function resolveEntities(array $entities, $metadata, array $args, Reso
foreach ($entities as $entity) {
// Translate the entity if it is translatable and a language was given.
if ($language && $entity instanceof TranslatableInterface && $entity->isTranslatable()) {
$entity = $this->entityRepository->getTranslationFromContext($entity, $language);
yield $entity->getTranslation($language);
}

$access = $entity->access('view', NULL, TRUE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Url;
use Drupal\graphql\GraphQL\Buffers\SubRequestBuffer;
use Drupal\graphql\GraphQL\Cache\CacheableValue;
use Drupal\graphql\GraphQL\Execution\ResolveContext;
use Drupal\graphql\Plugin\GraphQL\Fields\FieldPluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
Expand All @@ -20,10 +19,14 @@
* name = "languageSwitchLinks",
* type = "[LanguageSwitchLink]",
* parents = {"InternalUrl"},
* arguments = {
* "language" = "LanguageId"
* },
* response_cache_contexts = {
* "languages:language_url",
* "languages:language_interface"
* }
* "languages:language_interface",
* },
* contextual_arguments = {"language"}
* )
*/
class LanguageSwitchLinks extends FieldPluginBase implements ContainerFactoryPluginInterface {
Expand Down Expand Up @@ -75,29 +78,23 @@ public function __construct(
*/
protected function resolveValues($value, array $args, ResolveContext $context, ResolveInfo $info) {
if ($value instanceof Url) {
$resolve = $this->subRequestBuffer->add($value, function (Url $url) {
$links = $this->languageManager->getLanguageSwitchLinks(LanguageInterface::TYPE_URL, $url);
$current = $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_URL);

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

return function () use ($resolve) {
/** @var \Drupal\graphql\GraphQL\Cache\CacheableValue $response */
$response = $resolve();
list($current, $links) = $response->getValue();

if (!empty($links->links)) {
foreach ($links->links as $link) {
// Yield the link array and the language object of the language
// context resolved from the sub-request.
yield new CacheableValue([
'link' => $link,
'context' => $current,
], [$response]);
}
if (!empty($links->links)) {
foreach ($links->links as $link) {
// Yield the link array and the language object of the language
// context resolved from the sub-request.
yield [
'link' => $link,
'context' => $current,
];
}
};
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Drupal\graphql_core\Plugin\GraphQL\Fields\Menu;

use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Entity\EntityType;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\graphql\GraphQL\Execution\ResolveContext;
Expand All @@ -22,7 +23,8 @@
* type = "Menu",
* arguments = {
* "name" = "String!"
* }
* },
* response_cache_contexts = {"languages:language_interface"}
* )
*/
class MenuByName extends FieldPluginBase implements ContainerFactoryPluginInterface {
Expand Down
34 changes: 30 additions & 4 deletions modules/graphql_core/src/Plugin/GraphQL/Fields/Routing/Route.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

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

use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Path\PathValidatorInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Url;
use Drupal\graphql\GraphQL\Cache\CacheableValue;
use Drupal\graphql\GraphQL\Execution\ResolveContext;
use Drupal\graphql\Plugin\GraphQL\Fields\FieldPluginBase;
use Drupal\language\LanguageNegotiator;
use GraphQL\Type\Definition\ResolveInfo;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;

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

/**
* The language negotiator service.
*
* @var \Drupal\language\LanguageNegotiator
*/
protected $languageNegotiator;

/**
* {@inheritdoc}
*/
Expand All @@ -43,7 +50,8 @@ public static function create(ContainerInterface $container, array $configuratio
$configuration,
$plugin_id,
$plugin_definition,
$container->get('path.validator')
$container->get('path.validator'),
$container->has('language_negotiator') ? $container->get('language_negotiator') : NULL
);
}

Expand All @@ -58,18 +66,36 @@ public static function create(ContainerInterface $container, array $configuratio
* The plugin definition.
* @param \Drupal\Core\Path\PathValidatorInterface $pathValidator
* The path validator service.
* @param \Drupal\language\LanguageNegotiator|null $languageNegotiator
* The language negotiator.
*/
public function __construct(array $configuration, $pluginId, $pluginDefinition, PathValidatorInterface $pathValidator) {
public function __construct(
array $configuration,
$pluginId,
$pluginDefinition,
PathValidatorInterface $pathValidator,
$languageNegotiator
) {
parent::__construct($configuration, $pluginId, $pluginDefinition);
$this->pathValidator = $pathValidator;
$this->languageNegotiator = $languageNegotiator;
}

/**
* {@inheritdoc}
*/
public function resolveValues($value, array $args, ResolveContext $context, ResolveInfo $info) {
if (($url = $this->pathValidator->getUrlIfValidWithoutAccessCheck($args['path'])) && $url->access()) {

// For now we just take the "url" negotiator into account.
if ($this->languageNegotiator) {
if ($negotiator = $this->languageNegotiator->getNegotiationMethodInstance('language-url')) {
$context->setContext('language', $negotiator->getLangcode(Request::create($args['path'])), $info);
}
}

yield $url;

}
else {
yield (new CacheableValue(NULL))->addCacheTags(['4xx-response']);
Expand Down
Loading

0 comments on commit 8b362e0

Please sign in to comment.