Skip to content

Commit ffd4845

Browse files
Leksatfubhy
authored andcommitted
Prevent leaked cache metadata exceptions in mutations (#701)
1 parent 2437311 commit ffd4845

4 files changed

Lines changed: 156 additions & 95 deletions

File tree

modules/graphql_core/src/Plugin/GraphQL/Mutations/Entity/CreateEntityBase.php

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
use Drupal\Core\Entity\EntityInterface;
88
use Drupal\Core\Entity\EntityTypeManagerInterface;
99
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
10+
use Drupal\Core\Render\RenderContext;
11+
use Drupal\Core\Render\RendererInterface;
1012
use Drupal\Core\StringTranslation\StringTranslationTrait;
1113
use Drupal\graphql\GraphQL\Execution\ResolveContext;
1214
use Drupal\graphql_core\GraphQL\EntityCrudOutputWrapper;
@@ -25,6 +27,13 @@ abstract class CreateEntityBase extends MutationPluginBase implements ContainerF
2527
*/
2628
protected $entityTypeManager;
2729

30+
/**
31+
* The renderer service.
32+
*
33+
* @var \Drupal\Core\Render\RendererInterface
34+
*/
35+
protected $renderer;
36+
2837
/**
2938
* {@inheritdoc}
3039
*/
@@ -33,7 +42,8 @@ public static function create(ContainerInterface $container, array $configuratio
3342
$configuration,
3443
$pluginId,
3544
$pluginDefinition,
36-
$container->get('entity_type.manager')
45+
$container->get('entity_type.manager'),
46+
$container->get('renderer')
3747
);
3848
}
3949

@@ -48,35 +58,45 @@ public static function create(ContainerInterface $container, array $configuratio
4858
* The plugin definition array.
4959
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
5060
* The entity type manager service.
61+
* @param \Drupal\Core\Render\RendererInterface $renderer
62+
* The renderer service.
5163
*/
52-
public function __construct(array $configuration, $pluginId, $pluginDefinition, EntityTypeManagerInterface $entityTypeManager) {
64+
public function __construct(array $configuration, $pluginId, $pluginDefinition, EntityTypeManagerInterface $entityTypeManager, RendererInterface $renderer) {
5365
parent::__construct($configuration, $pluginId, $pluginDefinition);
5466
$this->entityTypeManager = $entityTypeManager;
67+
$this->renderer = $renderer;
5568
}
5669

5770
/**
5871
* {@inheritdoc}
5972
*/
6073
public function resolve($value, array $args, ResolveContext $context, ResolveInfo $info) {
61-
$entityTypeId = $this->pluginDefinition['entity_type'];
62-
63-
// The raw input needs to be converted to use the proper field and property
64-
// keys because we usually convert them to camel case when adding them to
65-
// the schema.
66-
$input = $this->extractEntityInput($value, $args, $context, $info);
67-
68-
$entityDefinition = $this->entityTypeManager->getDefinition($entityTypeId);
69-
if ($entityDefinition->hasKey('bundle')) {
70-
$bundleName = $this->pluginDefinition['entity_bundle'];
71-
$bundleKey = $entityDefinition->getKey('bundle');
72-
73-
// Add the entity's bundle with the correct key.
74-
$input[$bundleKey] = $bundleName;
75-
}
74+
// There are cases where the Drupal entity API calls emit the cache metadata
75+
// in the current render context. In such cases
76+
// EarlyRenderingControllerWrapperSubscriber throws the leaked cache
77+
// metadata exception. To avoid this, wrap the execution in its own render
78+
// context.
79+
return $this->renderer->executeInRenderContext(new RenderContext(), function () use ($value, $args, $context, $info) {
80+
$entityTypeId = $this->pluginDefinition['entity_type'];
81+
82+
// The raw input needs to be converted to use the proper field and property
83+
// keys because we usually convert them to camel case when adding them to
84+
// the schema.
85+
$input = $this->extractEntityInput($value, $args, $context, $info);
86+
87+
$entityDefinition = $this->entityTypeManager->getDefinition($entityTypeId);
88+
if ($entityDefinition->hasKey('bundle')) {
89+
$bundleName = $this->pluginDefinition['entity_bundle'];
90+
$bundleKey = $entityDefinition->getKey('bundle');
91+
92+
// Add the entity's bundle with the correct key.
93+
$input[$bundleKey] = $bundleName;
94+
}
7695

77-
$storage = $this->entityTypeManager->getStorage($entityTypeId);
78-
$entity = $storage->create($input);
79-
return $this->resolveOutput($entity, $args, $info);
96+
$storage = $this->entityTypeManager->getStorage($entityTypeId);
97+
$entity = $storage->create($input);
98+
return $this->resolveOutput($entity, $args, $info);
99+
});
80100
}
81101

82102
/**

modules/graphql_core/src/Plugin/GraphQL/Mutations/Entity/DeleteEntityBase.php

Lines changed: 46 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
use Drupal\Core\Entity\EntityStorageException;
77
use Drupal\Core\Entity\EntityTypeManagerInterface;
88
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
9+
use Drupal\Core\Render\RenderContext;
10+
use Drupal\Core\Render\RendererInterface;
911
use Drupal\Core\StringTranslation\StringTranslationTrait;
1012
use Drupal\graphql\GraphQL\Execution\ResolveContext;
1113
use Drupal\graphql_core\GraphQL\EntityCrudOutputWrapper;
@@ -24,6 +26,13 @@ abstract class DeleteEntityBase extends MutationPluginBase implements ContainerF
2426
*/
2527
protected $entityTypeManager;
2628

29+
/**
30+
* The renderer service.
31+
*
32+
* @var \Drupal\Core\Render\RendererInterface
33+
*/
34+
protected $renderer;
35+
2736
/**
2837
* {@inheritdoc}
2938
*/
@@ -32,7 +41,8 @@ public static function create(ContainerInterface $container, array $configuratio
3241
$configuration,
3342
$pluginId,
3443
$pluginDefinition,
35-
$container->get('entity_type.manager')
44+
$container->get('entity_type.manager'),
45+
$container->get('renderer')
3646
);
3747
}
3848

@@ -47,44 +57,54 @@ public static function create(ContainerInterface $container, array $configuratio
4757
* The plugin definition array.
4858
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
4959
* The entity type manager service.
60+
* @param \Drupal\Core\Render\RendererInterface $renderer
61+
* The renderer service.
5062
*/
51-
public function __construct(array $configuration, $pluginId, $pluginDefinition, EntityTypeManagerInterface $entityTypeManager) {
63+
public function __construct(array $configuration, $pluginId, $pluginDefinition, EntityTypeManagerInterface $entityTypeManager, RendererInterface $renderer) {
5264
parent::__construct($configuration, $pluginId, $pluginDefinition);
5365
$this->entityTypeManager = $entityTypeManager;
66+
$this->renderer = $renderer;
5467
}
5568

5669
/**
5770
* {@inheritdoc}
5871
*/
5972
public function resolve($value, array $args, ResolveContext $context, ResolveInfo $info) {
60-
$entityTypeId = $this->pluginDefinition['entity_type'];
61-
$storage = $this->entityTypeManager->getStorage($entityTypeId);
73+
// There are cases where the Drupal entity API calls emit the cache metadata
74+
// in the current render context. In such cases
75+
// EarlyRenderingControllerWrapperSubscriber throws the leaked cache
76+
// metadata exception. To avoid this, wrap the execution in its own render
77+
// context.
78+
return $this->renderer->executeInRenderContext(new RenderContext(), function () use ($value, $args, $context, $info) {
79+
$entityTypeId = $this->pluginDefinition['entity_type'];
80+
$storage = $this->entityTypeManager->getStorage($entityTypeId);
6281

63-
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
64-
if (!$entity = $storage->load($args['id'])) {
65-
return new EntityCrudOutputWrapper(NULL, NULL, [
66-
$this->t('The requested entity could not be loaded.'),
67-
]);
68-
}
82+
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
83+
if (!$entity = $storage->load($args['id'])) {
84+
return new EntityCrudOutputWrapper(NULL, NULL, [
85+
$this->t('The requested entity could not be loaded.'),
86+
]);
87+
}
6988

70-
if (!$entity->access('delete')) {
71-
return new EntityCrudOutputWrapper(NULL, NULL, [
72-
$this->t('You do not have the necessary permissions to delete this entity.'),
73-
]);
74-
}
89+
if (!$entity->access('delete')) {
90+
return new EntityCrudOutputWrapper(NULL, NULL, [
91+
$this->t('You do not have the necessary permissions to delete this entity.'),
92+
]);
93+
}
7594

76-
try {
77-
$entity->delete();
78-
}
79-
catch (EntityStorageException $exception) {
80-
return new EntityCrudOutputWrapper(NULL, NULL, [
81-
$this->t('Entity deletion failed with exception: @exception.', [
82-
'@exception' => $exception->getMessage(),
83-
]),
84-
]);
85-
}
95+
try {
96+
$entity->delete();
97+
}
98+
catch (EntityStorageException $exception) {
99+
return new EntityCrudOutputWrapper(NULL, NULL, [
100+
$this->t('Entity deletion failed with exception: @exception.', [
101+
'@exception' => $exception->getMessage(),
102+
]),
103+
]);
104+
}
86105

87-
return new EntityCrudOutputWrapper($entity);
106+
return new EntityCrudOutputWrapper($entity);
107+
});
88108
}
89109

90110
}

modules/graphql_core/src/Plugin/GraphQL/Mutations/Entity/UpdateEntityBase.php

Lines changed: 69 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
66
use Drupal\Core\Entity\EntityTypeManagerInterface;
77
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
8+
use Drupal\Core\Render\RenderContext;
9+
use Drupal\Core\Render\RendererInterface;
810
use Drupal\Core\StringTranslation\StringTranslationTrait;
911
use Drupal\graphql\GraphQL\Execution\ResolveContext;
1012
use Drupal\graphql_core\GraphQL\EntityCrudOutputWrapper;
@@ -23,6 +25,13 @@ abstract class UpdateEntityBase extends MutationPluginBase implements ContainerF
2325
*/
2426
protected $entityTypeManager;
2527

28+
/**
29+
* The renderer service.
30+
*
31+
* @var \Drupal\Core\Render\RendererInterface
32+
*/
33+
protected $renderer;
34+
2635
/**
2736
* {@inheritdoc}
2837
*/
@@ -31,7 +40,8 @@ public static function create(ContainerInterface $container, array $configuratio
3140
$configuration,
3241
$pluginId,
3342
$pluginDefinition,
34-
$container->get('entity_type.manager')
43+
$container->get('entity_type.manager'),
44+
$container->get('renderer')
3545
);
3646
}
3747

@@ -46,64 +56,74 @@ public static function create(ContainerInterface $container, array $configuratio
4656
* The plugin definition.
4757
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
4858
* The entity type manager service.
59+
* @param \Drupal\Core\Render\RendererInterface $renderer
60+
* The renderer service.
4961
*/
50-
public function __construct(array $configuration, $pluginId, $pluginDefinition, EntityTypeManagerInterface $entityTypeManager) {
62+
public function __construct(array $configuration, $pluginId, $pluginDefinition, EntityTypeManagerInterface $entityTypeManager, RendererInterface $renderer) {
5163
parent::__construct($configuration, $pluginId, $pluginDefinition);
5264
$this->entityTypeManager = $entityTypeManager;
65+
$this->renderer = $renderer;
5366
}
5467

5568
/**
5669
* {@inheritdoc}
5770
*/
5871
public function resolve($value, array $args, ResolveContext $context, ResolveInfo $info) {
59-
$entityTypeId = $this->pluginDefinition['entity_type'];
60-
$bundleName = $this->pluginDefinition['entity_bundle'];
61-
$storage = $this->entityTypeManager->getStorage($entityTypeId);
62-
63-
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
64-
if (!$entity = $storage->load($args['id'])) {
65-
return new EntityCrudOutputWrapper(NULL, NULL, [
66-
$this->t('The requested @bundle could not be loaded.', ['@bundle' => $bundleName]),
67-
]);
68-
}
69-
70-
if (!$entity->bundle() === $bundleName) {
71-
return new EntityCrudOutputWrapper(NULL, NULL, [
72-
$this->t('The requested entity is not of the expected type @bundle.', ['@bundle' => $bundleName]),
73-
]);
74-
}
75-
76-
if (!$entity->access('update')) {
77-
return new EntityCrudOutputWrapper(NULL, NULL, [
78-
$this->t('You do not have the necessary permissions to update this @bundle.', ['@bundle' => $bundleName]),
79-
]);
80-
}
81-
82-
// The raw input needs to be converted to use the proper field and property
83-
// keys because we usually convert them to camel case when adding them to
84-
// the schema. Allow the other implementations to control this easily.
85-
$input = $this->extractEntityInput($value, $args, $context, $info);
86-
87-
try {
88-
foreach ($input as $key => $value) {
89-
$entity->get($key)->setValue($value);
72+
// There are cases where the Drupal entity API calls emit the cache metadata
73+
// in the current render context. In such cases
74+
// EarlyRenderingControllerWrapperSubscriber throws the leaked cache
75+
// metadata exception. To avoid this, wrap the execution in its own render
76+
// context.
77+
return $this->renderer->executeInRenderContext(new RenderContext(), function () use ($value, $args, $context, $info) {
78+
$entityTypeId = $this->pluginDefinition['entity_type'];
79+
$bundleName = $this->pluginDefinition['entity_bundle'];
80+
$storage = $this->entityTypeManager->getStorage($entityTypeId);
81+
82+
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
83+
if (!$entity = $storage->load($args['id'])) {
84+
return new EntityCrudOutputWrapper(NULL, NULL, [
85+
$this->t('The requested @bundle could not be loaded.', ['@bundle' => $bundleName]),
86+
]);
9087
}
91-
}
92-
catch (\InvalidArgumentException $exception) {
93-
return new EntityCrudOutputWrapper(NULL, NULL, [
94-
$this->t('The entity update failed with exception: @exception.', ['@exception' => $exception->getMessage()]),
95-
]);
96-
}
97-
98-
if (($violations = $entity->validate()) && $violations->count()) {
99-
return new EntityCrudOutputWrapper(NULL, $violations);
100-
}
101-
102-
if (($status = $entity->save()) && $status === SAVED_UPDATED) {
103-
return new EntityCrudOutputWrapper($entity);
104-
}
105-
106-
return NULL;
88+
89+
if (!$entity->bundle() === $bundleName) {
90+
return new EntityCrudOutputWrapper(NULL, NULL, [
91+
$this->t('The requested entity is not of the expected type @bundle.', ['@bundle' => $bundleName]),
92+
]);
93+
}
94+
95+
if (!$entity->access('update')) {
96+
return new EntityCrudOutputWrapper(NULL, NULL, [
97+
$this->t('You do not have the necessary permissions to update this @bundle.', ['@bundle' => $bundleName]),
98+
]);
99+
}
100+
101+
// The raw input needs to be converted to use the proper field and property
102+
// keys because we usually convert them to camel case when adding them to
103+
// the schema. Allow the other implementations to control this easily.
104+
$input = $this->extractEntityInput($value, $args, $context, $info);
105+
106+
try {
107+
foreach ($input as $key => $value) {
108+
$entity->get($key)->setValue($value);
109+
}
110+
}
111+
catch (\InvalidArgumentException $exception) {
112+
return new EntityCrudOutputWrapper(NULL, NULL, [
113+
$this->t('The entity update failed with exception: @exception.', ['@exception' => $exception->getMessage()]),
114+
]);
115+
}
116+
117+
if (($violations = $entity->validate()) && $violations->count()) {
118+
return new EntityCrudOutputWrapper(NULL, $violations);
119+
}
120+
121+
if (($status = $entity->save()) && $status === SAVED_UPDATED) {
122+
return new EntityCrudOutputWrapper($entity);
123+
}
124+
125+
return NULL;
126+
});
107127
}
108128

109129
/**

modules/graphql_core/tests/src/Kernel/EntityMutation/EntityMutationTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ protected function mockMutationFactory($definition, $result = NULL, $builder = N
5252
$definition['id'],
5353
$definition,
5454
$this->container->get('entity_type.manager'),
55+
$this->container->get('renderer'),
5556
])
5657
->setMethods([
5758
'extractEntityInput',

0 commit comments

Comments
 (0)