Skip to content

Commit 92d3ec4

Browse files
authored
fix(DataProducer): Fix entity reference loading by language (#1232)
1 parent 6961eac commit 92d3ec4

File tree

5 files changed

+139
-111
lines changed

5 files changed

+139
-111
lines changed

src/Plugin/GraphQL/DataProducer/Field/EntityReference.php

Lines changed: 3 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
use Drupal\Core\Entity\EntityRepositoryInterface;
77
use Drupal\Core\Entity\EntityTypeManagerInterface;
88
use Drupal\Core\Entity\FieldableEntityInterface;
9-
use Drupal\Core\Entity\TranslatableInterface;
109
use Drupal\Core\Field\EntityReferenceFieldItemListInterface;
1110
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
1211
use Drupal\Core\Session\AccountInterface;
@@ -63,6 +62,8 @@
6362
*/
6463
class EntityReference extends DataProducerPluginBase implements ContainerFactoryPluginInterface {
6564

65+
use EntityReferenceTrait;
66+
6667
/**
6768
* The entity type manager service.
6869
*
@@ -161,41 +162,7 @@ public function resolve(EntityInterface $entity, $field, ?string $language, ?arr
161162

162163
$resolver = $this->entityBuffer->add($type, $ids);
163164
return new Deferred(function () use ($type, $language, $bundles, $access, $accessUser, $accessOperation, $resolver, $context) {
164-
$entities = $resolver() ?: [];
165-
$entities = array_filter($entities, function (EntityInterface $entity) use ($language, $bundles, $access, $accessOperation, $accessUser, $context) {
166-
if (isset($bundles) && !in_array($entity->bundle(), $bundles)) {
167-
return FALSE;
168-
}
169-
170-
// Get the correct translation.
171-
if (isset($language) && $language != $entity->language()->getId() && $entity instanceof TranslatableInterface) {
172-
$entity = $entity->getTranslation($language);
173-
$entity->addCacheContexts(["static:language:{$language}"]);
174-
}
175-
176-
// Check if the passed user (or current user if none is passed) has
177-
// access to the entity, if not return NULL.
178-
if ($access) {
179-
/** @var \Drupal\Core\Access\AccessResultInterface $accessResult */
180-
$accessResult = $entity->access($accessOperation, $accessUser, TRUE);
181-
$context->addCacheableDependency($accessResult);
182-
if (!$accessResult->isAllowed()) {
183-
return FALSE;
184-
}
185-
}
186-
187-
return TRUE;
188-
});
189-
190-
if (empty($entities)) {
191-
$type = $this->entityTypeManager->getDefinition($type);
192-
/** @var \Drupal\Core\Entity\EntityTypeInterface $type */
193-
$tags = $type->getListCacheTags();
194-
$context->addCacheTags($tags);
195-
return NULL;
196-
}
197-
198-
return $entities;
165+
return $this->getReferencedEntities($type, $language, $bundles, $access, $accessUser, $accessOperation, $resolver, $context);
199166
});
200167
}
201168

src/Plugin/GraphQL/DataProducer/Field/EntityReferenceLayoutRevisions.php

Lines changed: 3 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
use Drupal\Core\Entity\EntityInterface;
66
use Drupal\Core\Entity\EntityTypeManager;
77
use Drupal\Core\Entity\FieldableEntityInterface;
8-
use Drupal\Core\Entity\TranslatableInterface;
98
use Drupal\Core\Field\EntityReferenceFieldItemListInterface;
109
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
1110
use Drupal\Core\Session\AccountInterface;
@@ -64,6 +63,8 @@
6463
*/
6564
class EntityReferenceLayoutRevisions extends DataProducerPluginBase implements ContainerFactoryPluginInterface {
6665

66+
use EntityReferenceTrait;
67+
6768
/**
6869
* The entity type manager service.
6970
*
@@ -166,42 +167,7 @@ public function resolve(EntityInterface $entity, string $field, ?string $languag
166167

167168
$resolver = $this->entityRevisionBuffer->add($type, $vids);
168169
return new Deferred(function () use ($type, $language, $bundles, $access, $accessUser, $accessOperation, $resolver, $context) {
169-
$entities = $resolver() ?: [];
170-
171-
$entities = array_filter($entities, function (EntityInterface $entity) use ($language, $bundles, $access, $accessOperation, $accessUser, $context) {
172-
if (isset($bundles) && !in_array($entity->bundle(), $bundles)) {
173-
return FALSE;
174-
}
175-
176-
// Get the correct translation.
177-
if (isset($language) && $language != $entity->language()->getId() && $entity instanceof TranslatableInterface) {
178-
$entity = $entity->getTranslation($language);
179-
$entity->addCacheContexts(["static:language:{$language}"]);
180-
}
181-
182-
// Check if the passed user (or current user if none is passed) has
183-
// access to the entity, if not return NULL.
184-
if ($access) {
185-
/** @var \Drupal\Core\Access\AccessResultInterface $accessResult */
186-
$accessResult = $entity->access($accessOperation, $accessUser, TRUE);
187-
$context->addCacheableDependency($accessResult);
188-
if (!$accessResult->isAllowed()) {
189-
return FALSE;
190-
}
191-
}
192-
193-
return TRUE;
194-
});
195-
196-
if (empty($entities)) {
197-
$type = $this->entityTypeManager->getDefinition($type);
198-
/** @var \Drupal\Core\Entity\EntityTypeInterface $type */
199-
$tags = $type->getListCacheTags();
200-
$context->addCacheTags($tags);
201-
return NULL;
202-
}
203-
204-
return $entities;
170+
return $this->getReferencedEntities($type, $language, $bundles, $access, $accessUser, $accessOperation, $resolver, $context);
205171
});
206172
}
207173

src/Plugin/GraphQL/DataProducer/Field/EntityReferenceRevisions.php

Lines changed: 3 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
use Drupal\Core\Entity\EntityInterface;
66
use Drupal\Core\Entity\EntityTypeManager;
77
use Drupal\Core\Entity\FieldableEntityInterface;
8-
use Drupal\Core\Entity\TranslatableInterface;
98
use Drupal\Core\Field\EntityReferenceFieldItemListInterface;
109
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
1110
use Drupal\Core\Session\AccountInterface;
@@ -64,6 +63,8 @@
6463
*/
6564
class EntityReferenceRevisions extends DataProducerPluginBase implements ContainerFactoryPluginInterface {
6665

66+
use EntityReferenceTrait;
67+
6768
/**
6869
* The entity type manager service.
6970
*
@@ -166,42 +167,7 @@ public function resolve(EntityInterface $entity, string $field, ?string $languag
166167

167168
$resolver = $this->entityRevisionBuffer->add($type, $vids);
168169
return new Deferred(function () use ($type, $language, $bundles, $access, $accessUser, $accessOperation, $resolver, $context) {
169-
$entities = $resolver() ?: [];
170-
171-
$entities = array_filter($entities, function (EntityInterface $entity) use ($language, $bundles, $access, $accessOperation, $accessUser, $context) {
172-
if (isset($bundles) && !in_array($entity->bundle(), $bundles)) {
173-
return FALSE;
174-
}
175-
176-
// Get the correct translation.
177-
if (isset($language) && $language != $entity->language()->getId() && $entity instanceof TranslatableInterface) {
178-
$entity = $entity->getTranslation($language);
179-
$entity->addCacheContexts(["static:language:{$language}"]);
180-
}
181-
182-
// Check if the passed user (or current user if none is passed) has
183-
// access to the entity, if not return NULL.
184-
if ($access) {
185-
/** @var \Drupal\Core\Access\AccessResultInterface $accessResult */
186-
$accessResult = $entity->access($accessOperation, $accessUser, TRUE);
187-
$context->addCacheableDependency($accessResult);
188-
if (!$accessResult->isAllowed()) {
189-
return FALSE;
190-
}
191-
}
192-
193-
return TRUE;
194-
});
195-
196-
if (empty($entities)) {
197-
$type = $this->entityTypeManager->getDefinition($type);
198-
/** @var \Drupal\Core\Entity\EntityTypeInterface $type */
199-
$tags = $type->getListCacheTags();
200-
$context->addCacheTags($tags);
201-
return NULL;
202-
}
203-
204-
return $entities;
170+
return $this->getReferencedEntities($type, $language, $bundles, $access, $accessUser, $accessOperation, $resolver, $context);
205171
});
206172
}
207173

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<?php
2+
3+
namespace Drupal\graphql\Plugin\GraphQL\DataProducer\Field;
4+
5+
use Drupal\Core\Entity\EntityInterface;
6+
use Drupal\Core\Entity\TranslatableInterface;
7+
use Drupal\Core\Session\AccountInterface;
8+
use Drupal\graphql\GraphQL\Execution\FieldContext;
9+
10+
/**
11+
* Entity reference helpers.
12+
*/
13+
trait EntityReferenceTrait {
14+
15+
/**
16+
* Retrieves referenced entities from the given resolver.
17+
*
18+
* May optionally respect bundles/language and perform access checks.
19+
*
20+
* @param string $type
21+
* Entity type ID.
22+
* @param string|null $language
23+
* Optional. Language to be respected for retrieved entities.
24+
* @param array|null $bundles
25+
* Optional. List of bundles to be respected for retrieved entities.
26+
* @param bool $access
27+
* Whether to filter out inaccessible entities.
28+
* @param \Drupal\Core\Session\AccountInterface|null $accessUser
29+
* User entity to check access for. Default is null.
30+
* @param string $accessOperation
31+
* Operation to check access for. Default is view.
32+
* @param \Closure $resolver
33+
* The resolver to execute.
34+
* @param \Drupal\graphql\GraphQL\Execution\FieldContext $context
35+
* The caching context related to the current field.
36+
*
37+
* @return \Drupal\Core\Entity\EntityInterface[]|null
38+
* The list of references entities. Or NULL.
39+
*/
40+
protected function getReferencedEntities(string $type, ?string $language, ?array $bundles, bool $access, ?AccountInterface $accessUser, string $accessOperation, \Closure $resolver, FieldContext $context): ?array {
41+
$entities = $resolver() ?: [];
42+
43+
if (isset($bundles)) {
44+
$entities = array_filter($entities, function (EntityInterface $entity) use ($bundles) {
45+
return in_array($entity->bundle(), $bundles);
46+
});
47+
}
48+
if (isset($language)) {
49+
$entities = $this->getTranslated($entities, $language);
50+
}
51+
if ($access) {
52+
$entities = $this->filterAccessible($entities, $accessUser, $accessOperation, $context);
53+
}
54+
55+
if (empty($entities)) {
56+
$type = $this->entityTypeManager->getDefinition($type);
57+
/** @var \Drupal\Core\Entity\EntityTypeInterface $type */
58+
$tags = $type->getListCacheTags();
59+
$context->addCacheTags($tags);
60+
return NULL;
61+
}
62+
63+
return $entities;
64+
}
65+
66+
/**
67+
* Get the referenced entities in the specified language.
68+
*
69+
* @param \Drupal\Core\Entity\EntityInterface[] $entities
70+
* Entities to process.
71+
* @param string $language
72+
* Language to be respected for retrieved entities.
73+
*
74+
* @return \Drupal\Core\Entity\EntityInterface[]
75+
* Translated entities.
76+
*/
77+
private function getTranslated(array $entities, string $language): array {
78+
return array_map(function (EntityInterface $entity) use ($language) {
79+
if ($language !== $entity->language()->getId() && $entity instanceof TranslatableInterface && $entity->hasTranslation($language)) {
80+
$entity = $entity->getTranslation($language);
81+
}
82+
$entity->addCacheContexts(["static:language:{$language}"]);
83+
return $entity;
84+
}, $entities);
85+
}
86+
87+
/**
88+
* Filter out not accessible entities.
89+
*
90+
* @param \Drupal\Core\Entity\EntityInterface[] $entities
91+
* Entities to filter.
92+
* @param \Drupal\Core\Session\AccountInterface|null $accessUser
93+
* User entity to check access for. Default is null.
94+
* @param string $accessOperation
95+
* Operation to check access for. Default is view.
96+
* @param \Drupal\graphql\GraphQL\Execution\FieldContext $context
97+
* The caching context related to the current field.
98+
*
99+
* @return \Drupal\Core\Entity\EntityInterface[]
100+
* Filtered entities.
101+
*/
102+
private function filterAccessible(array $entities, ?AccountInterface $accessUser, string $accessOperation, FieldContext $context): array {
103+
return array_filter($entities, function (EntityInterface $entity) use ($accessOperation, $accessUser, $context) {
104+
/** @var \Drupal\Core\Access\AccessResultInterface $accessResult */
105+
$accessResult = $entity->access($accessOperation, $accessUser, TRUE);
106+
$context->addCacheableDependency($accessResult);
107+
if (!$accessResult->isAllowed()) {
108+
return FALSE;
109+
}
110+
return TRUE;
111+
});
112+
}
113+
114+
}

tests/src/Kernel/DataProducer/FieldTest.php renamed to tests/src/Kernel/DataProducer/EntityReferenceTest.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
*
1818
* @group graphql
1919
*/
20-
class FieldTest extends GraphQLTestBase {
20+
class EntityReferenceTest extends GraphQLTestBase {
2121
use EntityReferenceTestTrait;
2222
use QueryResultAssertionTrait;
2323

@@ -58,6 +58,9 @@ public function setUp(): void {
5858
'type' => 'test2',
5959
]);
6060
$this->referenced_node->save();
61+
$this->referenced_node
62+
->addTranslation('fr', ['title' => 'Dolor2 French'])
63+
->save();
6164

6265
$this->node = Node::create([
6366
'title' => 'Dolor',
@@ -74,9 +77,21 @@ public function testResolveEntityReference(): void {
7477
$result = $this->executeDataProducer('entity_reference', [
7578
'entity' => $this->node,
7679
'field' => 'field_test1_to_test2',
80+
'access' => TRUE,
81+
'access_operation' => 'view',
7782
]);
83+
$this->assertEquals($this->referenced_node->id(), reset($result)->id());
84+
$this->assertEquals('Dolor2', reset($result)->label());
7885

86+
$result = $this->executeDataProducer('entity_reference', [
87+
'entity' => $this->node,
88+
'field' => 'field_test1_to_test2',
89+
'access' => TRUE,
90+
'access_operation' => 'view',
91+
'language' => 'fr',
92+
]);
7993
$this->assertEquals($this->referenced_node->id(), reset($result)->id());
94+
$this->assertEquals('Dolor2 French', reset($result)->label());
8095
}
8196

8297
}

0 commit comments

Comments
 (0)