Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(dataproducer): CurrentUser result is uncacheable #1417

Open
wants to merge 3 commits into
base: 8.x-4.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/install/graphql.settings.yml
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
dataproducer_allow_current_user_caching: true
dataproducer_populate_default_values: true
5 changes: 5 additions & 0 deletions config/schema/graphql.schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ graphql.settings:
label: "GraphQL Settings"
mapping:
# @todo Remove in GraphQL 5.
dataproducer_allow_current_user_caching:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need this setting. We can add caching, as long as we achieve the same behavior this will not be a compatibility break.

type: boolean
label: "Allow the result of the current_user data producer to be cached"
description: "Legacy setting: Allows the results of resolvers that invoke the current_user data producer to be cached. Set this to true to be future-proof. This setting is deprecated and will be removed in a future release."
# @todo Remove in GraphQL 5.
dataproducer_populate_default_values:
type: boolean
label: "Populate dataproducer context default values"
Expand Down
12 changes: 12 additions & 0 deletions graphql.install
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,15 @@ function graphql_update_10400() :void {
->set('dataproducer_populate_default_values', FALSE)
->save();
}

/**
* Preserve caching behavior of current_user data producer for existing sites.
*
* Set dataproducer_allow_current_user_caching to TRUE after you verified that
* your resolvers that use the current_user data producer can handle caching.
*/
function graphql_update_10401() :void {
\Drupal::configFactory()->getEditable('graphql.settings')
->set('dataproducer_allow_current_user_caching', FALSE)
->save();
}
39 changes: 29 additions & 10 deletions src/Plugin/GraphQL/DataProducer/User/CurrentUser.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Drupal\graphql\Plugin\GraphQL\DataProducer\User;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\graphql\GraphQL\Execution\FieldContext;
Expand All @@ -24,10 +25,13 @@ class CurrentUser extends DataProducerPluginBase implements ContainerFactoryPlug

/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
protected AccountInterface $currentUser;

/**
* The config factory.
*/
protected ConfigFactoryInterface $configFactory;

/**
* {@inheritdoc}
Expand All @@ -37,12 +41,13 @@ public static function create(ContainerInterface $container, array $configuratio
$configuration,
$plugin_id,
$plugin_definition,
$container->get('current_user')
$container->get('current_user'),
$container->get('config.factory')
);
}

/**
* CurrentUser constructor.
* Constructs a new CurrentUser data producer.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
Expand All @@ -52,14 +57,17 @@ public static function create(ContainerInterface $container, array $configuratio
* The plugin implementation definition.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
*/
public function __construct(array $configuration, string $plugin_id, array $plugin_definition, AccountInterface $current_user) {
public function __construct(array $configuration, string $plugin_id, array $plugin_definition, AccountInterface $current_user, ConfigFactoryInterface $config_factory) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->currentUser = $current_user;
$this->configFactory = $config_factory;
}

/**
* Returns current user.
* Returns the current user.
*
* @param \Drupal\graphql\GraphQL\Execution\FieldContext $field_context
* Field context.
Expand All @@ -68,9 +76,20 @@ public function __construct(array $configuration, string $plugin_id, array $plug
* The current user.
*/
public function resolve(FieldContext $field_context): AccountInterface {
// Response must be cached based on current user as a cache context,
// otherwise a new user would became a previous user.
$field_context->addCacheableDependency($this->currentUser);
// Response must be cached per user so that information from previously
// logged in users will not leak to newly logged in users.
$field_context->addCacheContexts(['user']);

// Previous versions of this plugin were always returning an uncacheable
// result. In order to preserve backwards compatibility a temporary flag
// has been introduced to allow users to opt-in to caching after verifying
// that the result is safe to cache.
// @todo Remove in 5.x.
$allow_caching = $this->configFactory->get('graphql.settings')->get('dataproducer_allow_current_user_caching') ?? FALSE;
if (!$allow_caching) {
$field_context->mergeCacheMaxAge(0);
}

return $this->currentUser;
}

Expand Down
92 changes: 92 additions & 0 deletions tests/src/Kernel/DataProducer/CurrentUserTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

declare(strict_types=1);

namespace Drupal\Tests\graphql\Kernel\DataProducer;

use Drupal\Core\Session\AccountInterface;
use Drupal\graphql\GraphQL\Execution\FieldContext;
use Drupal\Tests\graphql\Kernel\GraphQLTestBase;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\user\Entity\User;

/**
* Tests the current_user data producer.
*
* @coversDefaultClass \Drupal\graphql\Plugin\GraphQL\DataProducer\User\CurrentUser
* @group graphql
*/
class CurrentUserTest extends GraphQLTestBase {

use UserCreationTrait;

/**
* Test users.
*
* @var \Drupal\Core\Session\AccountInterface[]
*/
protected array $users;

/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();

// Create two test users.
$this->users = [
$this->createUser(),
$this->createUser(),
];

// Log out initially.
$this->container->get('current_user')->setAccount(User::getAnonymousUser());
}

/**
* @covers \Drupal\graphql\Plugin\GraphQL\DataProducer\User\CurrentUser::resolve
*/
public function testCurrentUser(): void {
// Initially no user is logged in.
$result = $this->executeDataProducer('current_user');
$this->assertInstanceOf(AccountInterface::class, $result);
$this->assertEquals(0, $result->id());

// Log in as the first user.
$this->container->get('current_user')->setAccount($this->users[0]);
$result = $this->executeDataProducer('current_user');
$this->assertInstanceOf(AccountInterface::class, $result);
$this->assertEquals($this->users[0]->id(), $result->id());

// Log in as the second user.
$this->container->get('current_user')->setAccount($this->users[1]);
$result = $this->executeDataProducer('current_user');
$this->assertInstanceOf(AccountInterface::class, $result);
$this->assertEquals($this->users[1]->id(), $result->id());

// Log out again.
$this->container->get('current_user')->setAccount(User::getAnonymousUser());
$result = $this->executeDataProducer('current_user');
$this->assertInstanceOf(AccountInterface::class, $result);
$this->assertEquals(0, $result->id());
}

/**
* {@inheritdoc}
*/
protected function executeDataProducer($id, array $contexts = []) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but this does not actually test the caching.

We need to test 2 things:

  1. Repeated calls to get the current user return a cached result
  2. when the user cache context is cleared you get an uncached result

I think just calling the dataproducer is not enough. Check out ResultCacheTest which does some unholy mocking, but should be the right approach.

/** @var \Drupal\graphql\Plugin\DataProducerPluginManager $manager */
$manager = $this->container->get('plugin.manager.graphql.data_producer');

/** @var \Drupal\graphql\Plugin\DataProducerPluginInterface $plugin */
$plugin = $manager->createInstance($id);

// The 'user' cache context should be added so that the results will be
// cached per user.
$context = $this->prophesize(FieldContext::class);
$context->addCacheContexts(['user'])->willReturn($context->reveal())->shouldBeCalled();

return $plugin->resolveField($context->reveal());
}

}